对象与类
4.1 面向对象程序设计概述
面向对象程序设计(OOP)是当今主流的程序设计范型。Java是完全面向对象的
面向对象的程序由对象组成,每个对象包含对用户公开的特定功能和隐藏的实现部分。在OOP中,不必关心对象的具体实现,只要能满足用户需求即可
4.1.1 类
类是构造对象的模板,由类构造对象的过程称为创建类的实例
封装从形式上看,是将数据和行为组合在一个包中,并对对象的使用者隐藏了数据的实现方式,对象中的数据称为实例域,操纵数据的过程叫做方法。对每个特定的类实例都有一组特定的实例域值,这些值的集合就是这个对象的当前状态
实现封装的关键在于绝不能让类中的方法直接访问其他类的实例域。程序仅仅通过对象的方法和对象数据进行交互。封装给对象赋予了“黑盒”特征。
继承是OOP的另一个工具。继承的类拥有被继承类的全部方法和属性,在新类中只需提供适用于这个新类的方法和数据域即可。
4.1.2 对象
4.1.3 识别类
4.1.4 类之间的关系
- uses-a 依赖
- has-a 聚合
- is-a 继承
4.2 使用预定义类
Java中,没有类就无法做任何事。例如前面接触的Math
类,但它不具有面向对象特征。下面给出更典型的Date
类为例
4.2.1 对象与对象变量
要使用对象,就必须先构造对象,指定初始状态。然后应用对象方法。
Java中使用构造器构造新实例。构造器是一种特殊的方法,用来构造并初始化对象。
构造器的名字和类名相同,因此Date
类的构造器名为Date
。要构造一个Date
对象,需要在构造器前面加上new
操作符
Date birthday = new Date();
// Date有toString方法 返回日期的字符串描述
System.out.println(birthday.toString());
4.2.2 Java类库中的LocalDate
类
标准Java类库包含两个类:用来表示具体时间点的Date
类,以及日历表示法的LocalDate
类
应当使用静态工厂方法调用构造器
LocalDate.now() // 返回调用这个构造器时的时间
LocalDate newYearsEve = LocalDate.of(1999, 12, 31); // of方法,用年月日构造一个特定日期的对象
可以获取LocalDate
对象的信息,修改信息
int year = newYearsEve.getYear(); // 1999
newYearsEve = newYearsEve.plusDay(1000); // 增加1000天
4.2.3 更改器方法和访问器方法
更改器方法:改变原先对象的状态,而不是生成新的对象
访问器方法:只访问而不修改对象状态
一个简单的例子,感受使用类的接口编程
import java.time.DayOfWeek;
import java.time.LocalDate;
public class Test {
public static void main(String[] args) {
LocalDate date = LocalDate.now();
int month = date.getMonthValue();
int today = date.getDayOfMonth();
date = date.minusDays(today-1); // 月初
DayOfWeek weekday = date.getDayOfWeek(); // WEDNESDAY (...
int value = weekday.getValue(); // 星期几 3-Wed
System.out.println("Mon\tTue\tWed\tThu\tFri\tSat\tSun");
for(int i=0; i<value-1; ++i){
System.out.print("\t"); // 空出前面的部分
}
while(date.getMonthValue() == month){
System.out.print(date.getDayOfMonth());
if(date.getDayOfMonth() == today){
System.out.print("*\t");
}else{
System.out.print("\t");
}
date = date.plusDays(1);
if(date.getDayOfWeek().getValue() == 1) System.out.println(); // 换行
}
if(date.getDayOfWeek().getValue() != 1) System.out.println();
}
}
4.3 用户自定义类
现在开始设计复杂应用程序所需的各种类,这些类通常没有main
方法,但有自己的实例域和实例方法。
创建一个完整的程序,应当将若干类组合在一起,其中只有一个类有main
方法
import java.time.LocalDate;
public class Test {
public static void main(String[] args){
Employee[] staff= new Employee[3];
// 构造
staff[0] = new Employee("Carl Cracker", 75000, 1987, 12, 15);
staff[1] = new Employee("Harry Cracker", 50000, 1989, 10, 1);
staff[2] = new Employee("Tony Tester", 40000, 1990, 3, 15);
// 调用raiseSalary方法
for(Employee e : staff){
e.raiseSalary(5);
}
// 调用访问器
for(Employee e :staff){
System.out.println("name = "+e.getName()+", salary = "+e.getSalary()+", hireDay = "+e.getHireDay());
}
}
}
class Employee {
private String name;
private double salary;
private LocalDate hireDay;
// 构造器
public Employee(String n, double s, int year, int month, int day) {
name = n;
salary = s;
hireDay = LocalDate.of(year, month, day);
}
public String getName() {
return name;
}
public double getSalary() {
return salary;
}
public LocalDate getHireDay() {
return hireDay;
}
public void raiseSalary(double byPercent) {
double raise = salary * byPercent / 100;
salary += raise;
}
}
- 方法标记为
public
意味着任何类的任何方法都可以调用这些方法 private
实例域确保只有Employee
类自身的方法能够访问这些实例域,其他类的方法不可以- 构造器与类同名。构造对象时,构造器会运行,将实例域初始化为所希望的状态。构造器总是伴随着
new
操作符的调用而执行,不能对一个已经存在的对象调用构造器来达到重新设置实例域的目的。构造器没有返回值,一个类可以有多个构造器
4.3.5 隐式参数
public void raiseSalary(double byPercent) {
double raise = this.salary * byPercent / 100;
this.salary += raise;
}
}
- 每一个方法中,关键字
this
表示隐式参数,为调用该方法的对象 - Java中所有方法都必须在类的内部定义。不同于C++,这些方法不一定
inline
。这取决于Java虚拟机
4.3.6 封装的优点
4.3.7 基于类的访问权限
一个方法可以访问所属类的所有对象的私有数据
class Employee{
...
public boolean equals(Employee other){
return name.equals(other.name);
}
}
// 调用
if(harry.equals(boss)){...}
这个方法访问harry
和boss
的私有域,这是合法的,他们都是Employee
对象
4.3.8 私有方法
private
方法指出。
不会被外部的其他类调用
4.3.9 final
实例域
将实例域定义为final
,在构建对象时必须初始化,并且之后不能再修改。
4.4 静态域和静态方法
4.4.1 静态域
如果将域设置为static
,每个类中只有一个这样的域。静态域属于类,不属于任何独立对象
4.4.2 静态常量
例如
public class Math{
...
public static final double PI = 3.14159265358979323846;
...
}
// 可以直接用 Math.PI 获得这个常量
public class System{
...
public static final PrintStream out = ...;
...
}
4.4.3 静态方法
静态方法是一种不能向对象实施操作的方法,例如Math
类的pow
方法就是一个静态方法。
Math.pow(x, a); // 不使用任何Math对象
静态方法不能访问实例域,因为他不能操作对象;但是静态方法可以访问自身类中的静态域。可以通过类名调用
以下情况需要使用静态方法
- 一个方法不需要访问对象状态,其参数都是显式提供,例如
Math.pow(x,a)
- 一个方法只需访问类的静态域
4.4.4 工厂方法
静态方法常见用途:静态工厂方法。例如LocalDate.now
和LocalDate.of
方法构造对象
为什么不用构造器呢?
- 无法命名构造器,构造器的名字必须和类名相同,有时候希望采用不同的名字
- 使用构造器时无法改变对象的类型,在继承时可能需要构造子类对象
4.4.5 main
方法
main
方法也是一个静态方法,他不对任何对象进行操作。事实上在启动程序时没有任何一个对象,静态main
方法将执行并创建程序所需的对象
每一个类都可以有一个main
方法,常用来对类进行单元测试。
以下代码总结了static
用法以及main
方法做单元测试
public class Test {
public static void main(String[] args) {
Employee[] staff = new Employee[3];
staff[0] = new Employee("Tom", 40000);
staff[1] = new Employee("Dick", 60000);
staff[2] = new Employee("Harry", 65000);
for (Employee e :
staff) {
e.setId();
System.out.println("name="+e.getName()+", id="+e.getId()+", salary="+e.getSalary());
}
int n = Employee.getNextId();
System.out.println("Next available id="+n);
}
}
class Employee{
private static int nextId = 1;
private String name;
private double salary;
private int id;
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
public String getName() {
return name;
}
public double getSalary() {
return salary;
}
public int getId() {
return id;
}
public void setId(){
id = nextId ++;
}
public static int getNextId() {
return nextId;
}
public static void main(String[] args) {
Employee e = new Employee("Harry", 50000);
System.out.println(e.getName()+" "+e.getSalary());
}
}
4.5 方法参数
Java程序设计语言总是采用按值调用
然而方法参数有两种类型:基本数据类型、对象引用。
一个方法不可能修改一个基本数据类型的参数,而对象作为引用就不一样了。
方法得到对象引用的拷贝,对象引用以及它的拷贝同时引用一个对象
方法不能让对象参数引用一个新的对象
下面程序演示了参数调用
public class Test{
public static void main(String[] args) {
/*
Test1: Methods can't modify numeric parameters
*/
System.out.println("Testing tripleValue:");
double percent = 10;
System.out.println("Before: percent="+percent);
tripleValue(percent);
System.out.println("After: percent="+percent);
/*
Test2: Methods can change the state of object parameters
*/
System.out.println("\nTesting tripleSalary:");
Employee harry = new Employee("Harry", 50000);
System.out.println("Before: salary="+harry.getSalary());
tripleSalary(harry);
System.out.println("After: salary="+harry.getSalary());
/*
Test3: Methods can't attach new objects to object parameters
*/
System.out.println("\nTesting swap:");
Employee a = new Employee("Alice", 70000);
Employee b = new Employee("Bob", 60000);
System.out.println("Before: a="+a.getName());
System.out.println("Before: b="+b.getName());
swap(a, b);
System.out.println("After: a="+a.getName());
System.out.println("After: b="+b.getName());
}
public static void tripleValue(double x){ // doesn't work
x = 3*x;
System.out.println("End of method: x="+x);
}
public static void tripleSalary(Employee x){ // works
x.raiseSalary(200);
System.out.println("End of method: salary="+x.getSalary());
}
public static void swap(Employee x, Employee y){
Employee temp = x;
x = y;
y = temp;
System.out.println("End of method: x="+x.getName());
System.out.println("End of method: y="+y.getName());
}
}
class Employee{
private String name;
private double salary;
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
public String getName() {
return name;
}
public double getSalary() {
return salary;
}
public void raiseSalary(double byPercent){
double raise = salary*byPercent/100;
salary += raise;
}
}
4.6 对象构造
4.6.1 重载
多个方法有相同的名字,不同的参数,便产生了重载。编译器根据各个方法给出的参数类型与特定方法调用所使用的值类型进行匹配挑出相应的方法。
Java允许重载任何方法而不只是构造器方法。完整描述一个方法需要指出方法名和参数类型,称为方法的签名。返回类型不属于签名的一部分,因此不能有两个名字相同、参数类型相同却返回不同类型值的方法。
4.6.2 默认域初始化
构造器中没有显式地给实例域赋值,那么会自动地赋为默认值:数值为0,布尔值为false
,对象引用为null
不明确地对域初始化,不是一种好习惯。
4.6.3 无参数的构造器
- 当且仅当类没有提供任何构造器时,系统会自动提供一个无参数构造器,默认初始化实例域
- 如果提供了至少一个构造器,但没有提供无参数的构造器,那么构造对象时没有提供参数不合法。
4.6.4 显式域初始化
可以在类定义中,直接将一个值赋给任何域,例如
class Employee{
private String name = "";
...
}
在执行构造器之前,先执行赋值操作。
初始值不一定是常量值,可以调用方法对域进行初始化
class Employee{
private static int nextId = 0;
// 调用方法赋值初始化实例域
private int id = assignId();
...
private static int assignId(){
int r = nextId;
nextId ++;
return r;
}
...
}
4.6.5 参数名
为了可读性,建议参数变量用同样的名字将实例域屏蔽起来,实例域采用this.
访问
4.6.6 调用另一个构造器
this
的另一个含义是调用同一个类的另一个构造器,例如
public Employee(double s){
// 调用 Employee(String, double)
this("Employee #"+nextId, s);
nextId ++;
}
4.6.7 初始化块
前面讲过两种初始化数据域的方法:
- 在构造器中设置
- 在声明中赋值
实际上Java提供第三种机制,称为初始化块
在一个类的声明中可以包含多个代码块,只要在构造类的对象,这些块就会被执行。例如
class Employee{
private static int nextId;
private int id;
// 初始化块,建议放在域定义后面
{
id = nextId;
nextId ++;
}
// 方法
...
}
如果对静态域进行初始化的代码比较复杂,可以使用静态的初始化块
static{
Random generator = new Random();
nextId = generator.nextInt(10000);
}
下面的程序演示了上面几种特性
import java.util.Random;
public class Test {
public static void main(String[] args) {
Employee[] staff = new Employee[3];
staff[0] = new Employee("Harry", 40000);
staff[1] = new Employee(60000);
staff[2] = new Employee();
for (Employee e : staff){
System.out.println("name = "+e.getName()+", id = "+e.getId()+", salary = "+e.getSalary());
}
}
}
class Employee{
// 静态域
private static int nextId;
// 实例域
private int id;
private String name = ""; // 声明中初始化
private double salary;
// 静态初始化块
static{
Random generator = new Random();
nextId = generator.nextInt(10000); // 0-9999
}
// 初始化块
{
id = nextId;
++ nextId;
}
// 三个重载的构造器
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
public Employee(double salary) {
this("Employee #"+nextId, salary); // 通过this调用另一个构造器
}
public Employee() {
// 默认初始化
// name-""
// salary-0
// id-初始化块
}
public int getId() {
return id;
}
public String getName() {
return name;
}
public double getSalary() {
return salary;
}
}
4.6.8 对象析构和finalize
方法
Java有自动的垃圾回收器,不支持析构器
某些对象使用了内存资源之外的其他资源,资源不再被需要时,需要回收。
可以为任何一个类添加finalize
方法。他将在垃圾回收器清除对象之前被调用。实际中不建议使用
如果某个资源在使用完毕之后需要立刻被关闭,那么需要人工管理,可以用close
方法完成。(7.2.5)
4.7 包
Java允许使用包将类组织起来。借助包可以方便地组织自己的代码,并将自己的代码与别人提供的代码库分开管理
标准Java类库分布在多个包中,包括java.lang
、java.util
、java.net
等。标准Java包具有一个层次结构
如同硬盘目录嵌套,也可以使用嵌套层次组织包。所有Java包都处于java
和javax
包层次中
使用包的主要目的是确保类名的唯一性。建议将公司的因特网域名以逆序形式作为包名
从编译器角度,嵌套的包没有任何关系,例如java.util
包和java.util.jar
毫无关系,每一个都有独立的类集合
4.7.1 类的导入
一个类可以使用所属包中的所有类,以及其他包中的共有类
可以使用import
语句导入类,就不用每次都写出类的全称
import
语句位于源文件顶部,但在package
语句后面
例如import java.util.*
导入java.util
包中所有的类
需要注意的是,只能用星号导入一个包,而不能import java.*
导入以java
为前缀的所有包
发生命名冲突时,就需要考虑包的名字了。例如java.util
和java.sql
都有Date
类
如果只需要用一个Date
,那么可以
import java.util.*;
import java.sql.*;
import java.util.Date;
如果都需要用,那么只能在类名前加完整包名了 TAT
4.7.2 静态导入
import
除了可以导入类,还可以导入静态方法和静态域
import static java.lang.System.*;
out.println("Goodbye, world"); // System.out
exit(0); // System.exit
4.7.3 类放入包中(待补充
package
语句。如果不添加,那么就是放在一个默认包中