[Java核心技术1] 第4章 对象和类
面向对象程序设计(OOP)与面向过程的程序设计在思维方式上有很大的差别,OOP将数据放在第一位,然后再考虑操作数据的算法。OOP中不必关注具体的实现,只要能满足需求即可。
4.1 面向对象的程序概述
4.1.1 类
类是什么?一类事物的抽象描述?
类(class)是构造对象的模板或蓝图,由类构造(construct) 对象的过程称为创建类的实例 (instance )。
封装( encapsulation)关键在于绝对不能让类中的方法直接地访问其他类的实例域。
继承(inheritance)通过扩展一个类来建立另外一个类的过程称。
4.1.2 对象
对象中的数据称为实例域( instance field ),操纵数据的过程称为方法( method ) 对于每个特定的类实例(对象)都有一组特定的实例域值。这些值的集合就是这个对象的当前状态( state )。
对象的主要特性:
- 对象的行为(behavior)——用可调用的方法定义
- 对象的状态(state)——对象的状态通过调用方法实现(否则,封装性被破坏)
- 对象标识(identity)——对象的标识是不同的,状态也存在差异
4.1.3 标识类
OOP首先从设计类开始,然往类中添加方法。
设计一个类时,名词可能成为类,如:类Item,Order等。
动词可能成为方法,如add是Order类的一个方法,而Item对象是一个参数。
4.1.4 类之间的关系
类之间常见的关系有:
- 依赖(Dependency) 如果一个类的方法操纵另一个类的对象,我们就说一个类依赖于另一个类。一般而言,依赖关系在Java语言中体现为局域变量、方法的形参,或者对静态方法的调用。
Try to minimize the number of classes that depend on each other. The point is, if a class A is unaware of the existence of a class B, it is also unconcerned about any changes to B. (And this means that changes to B do not introduce bugs into A.) In software engineering terminology, you want to minimize the coupling between classes.
应该尽可能地将相互依赖的类减至最少。如果类 A 不知道 B 的存在, 它就不会关心 B的任何改变(这意味着 B 的改变不会导致 A 产生任何 bug )。用软件工程的术语来说,就是让类之间的耦合度最小。
- 聚合(Aggregation) 聚合关系意味着类 A 的对象包含类 B 的对象。例如, 一个Order 对象包含一些 Item 对象。聚合关系通过实例变量实现的。但两个类是处在不平等层次上的,一个代表整体,另一个代表部分。
- 继承(Inheritance) 类A扩展类B,类A包含从类B继承的方法,并且还会有一些其他的功能。类A中包含一些优先处理的特殊方法。
类与类之间的几种关系
4.2预定义类
Java中并不是所有类都具有面向对象的特征,比如Math类。Math类只封装了功能,如Math.random方法,由于没有数据,因此不必担心生成对象以及初始化实例域。
4.2.1 对象和对象变量
Java中使用构造器(constructor)构造新实例。构造器与类名相同,在构造器前面加上new操作符构造一个新类。
new Date();//构造一个新对象,初始化为当前时间
System.out.println(new Date());//也可以将对象传递个一个方法
String s=new Date().toString();//也可以将一个方法应用于刚刚创建的对象
Date birthdayday=new Date();//通常,构造的对象存放在一个变量中,会被多次使用。
对象与对象变量间的区别:
Date deadline;//定义一个对象变量deadline
s=deadline.toString();
//报错,因为deadline不是一个对象,也没有引用对象,不能将任何Date方法应用这个变量
初始化变量deadline有两种方法
deadline=new Date(); //构造新的对象
deadline=birthday;//引用已经存在的对象
It is important to realize that an object variable doesn’t actually contain an object. It only refers to an object.
In Java, the value of any object variable is a reference to an object that is stored elsewhere. The return value of the new operator is also a reference.一定要认识到 : 一个对象变量并没有实际包含一个对象, 而仅仅引用一个对象 。
在Java中,任何对象变量的值都是对存储在另一个地方的对象的引用。new操作符返回的也是一个引用
局部变量不会自动初始化null。必须调用new 或者设置为null,表明这个对象变量目前没有引用任何对象。
4.2.2 Java类库中的LocalDate类
在Java中,Date类用来表示时间点,LocalDate类是表示日历。将时间和日历分开是一种很好的面向对象设计,使用不同的类表示不同的概念。
不要使用构造器来构造 LocalDate 类的对象。实际上,应当使用静态工厂方法 (factorymethod) 。
考虑用静态工厂方法代替构造器
LocalDate.now();//构造一个新的LocalDate对象
LocalDate.of(1998.2.16);//构造一个特定日期的对象
LocalDate newYearsEve=LocalDate.of(1999,12.31);//构造的对象存在对象变量中
4.2.3 更改器方法和访问器方法
更该器方法,是一种用来控制变量变化的方法。它们也被称为setter方法。返回私有成员变量的值。
GregorianCalendar.add方法是一个更改器方法,调用这个方法后someDay会改变。
GregorianCalendar someDay=new GregorianCalendar(1999.11.31);
//这个类的月是从0到11
someDay.add(alendar.DAY_OF_MONTH,1000);
访问器方法,只访问对象而不修改对象。
LocalDate.getYear和GregorianCalendar.get是访问器方法。
int year=someDay.get(Calendar.YEAR);
因为 LocalDate 被设计为不可变对象,这样的话每个修改当前 LocalDate 对象的方法(比如 plusDays),都会返回一个新的 LocalDate 对象,而原有的 LocalDate 对象不会发生改变。
LocalDate aThousandDays=newYearsEve.plusDays(1000);
//plusDays方法会生成一个新的对象,plusDays方法没有更改调用这个方法的对象。
日历程序
4.3 用户自定义
设计复杂应用程序需要各种主力类(workhorse class),这些类通常没有main方法。
一个完整的应用程序,需要将若干类组合起来,其中只有一个main方法。
4.3.1 Employee类
在Java中定义类,定义一个Employee类
class ClassName{
field1
field2
...
constructor1
constructor2
...
method1
method2
}
4.3.2 多个源文件的使用
在一个源文件中,只能有一个公有类,但可以有任意数量的非公有类。编译源代码时,每个类都会生称一个对应的class文件。
如果将每一个类存在一个单独的源文件中。当编译器发现A类使用了B类时,会查找B.class文件,如果没找到,就会搜索B.java并编译;如果B.java比B.class新,也会重新编译。
4.3.3 解剖Employee类
类通常包含一个构造器(或者多个)和若干方法,方法标记为public,任何类都可以调用public修饰的方法。
实例域由private修饰,类自身的方法能够访问这些实例域,其他类的方法不能。
类通常包含类类型的字段(如String)。
4.3.4 从构造器开始
构造器与类名相同。构造器总是伴随着new操作符的执行被调用,以便将实例域初始化为所希望的状态。
- 构造器与类名相同
- 每个类可以有一个以上的构造器
- 构造器可以有0个,1个或多个
- 构造器没有返回值
- 构造器总是伴随着new操作一起调用
注意在构造器中不要定义与实例域同名的变量,因为这些变量只能在构造器内部访问。
4.3.5 隐式参数和显示参数
在每一个方法中,出现在方法前的类对象称为隐式(implicit)参数,方法括号中的数值是显示(explicit)参数。
关键字this表示隐式参数。
4.3.6 封装的优点
getXX方法是典型的域访问器,它们只返回实例值域。
如果域不是是public的,修改这个域值的捣乱者可能在任何地方。
在有些时候, 需要获得或设置实例域的值,应当提供下面的内容
- 一个私有的数据域
- 一个公有的域访问器方法
- 有个公有的域修改器方法
这么做的好处是,可以改变内部的实现,而不影响其他代码;二是更改器可以执行错误检测,而赋值是不会进行这种处理的。
请注意不要编写返回可变对象引用的访问器方法
4.3.7 基于类的访问权限
一个方法可以访问所属类的对象的私有数据。
典型的调用方式
a.equals(b)...
方法可以访问所属类的私有特性(feature),而不仅限于访问调用对象(隐式参数)的私有特性。
4.3.8 私有方法
有时,希望一个计算代码可以划分为若干独立的辅助方法。它们与实现机制非常紧密,或者有特别的协议或调用次序。这种方法最好设计为private。
对于私有方法,可以进行改变;如果改用其他的方法实现相应的操作,可以将其删除。如果方法是公有的, 就不能将其删去,因为其他的代码很可能依赖它。
4.3.9 final实例域
定义为final的实例域,构建对象的时候必须初始化。并且之后不能再修改。
final大多应用于基本(primitive)类型域,或不可变(immutable)类的域
不可变类中没有改变对象的方法,如String,每次对于String对象的修改都将产生一个新的String对象,而原来的对象保持不变 。
StringBuilder是可变类,因为每次对于它的对象的修改都作用于该对象本身,并没有产生新的对象。
final修饰的可变的类,表示存储在变量中的对象引用不会再指向其他的类,不过这个可变对象可以更改。
private final StringBuilder evaluations= new StringBuilder();
public void giveGoldStar()
{
evaluations.append(LocalDate.now() + ": Gold star!\n");
}
4.4 静态域与静态方法
main方法都被标记为static,这个符号有什么含义呢?
4.4.1 静态域
如果将域定义为static,这个域属于类,不属于类对象。这样的域只存在类,而对于实例域却在每个对象中都有拷贝。
class Employee
{
private static int nextId = 1;
private int id;
. . .
}
public void setId()
{
id = nextId;
nextId++;
}
. . .
//harry 的 id 域被设置为静态域 nextld 当前的值,并且静态域 nextld 的值加 1:
harry.id = Employee.nextId;
Employee.nextId++;
类的所以实例共享一个nextId,每个对象都有自己的id。
4.4.2 静态常量
静态常量使用比静态变量多。如在Math类中的PI
public class Math
{
. . .
public static final double PI = 3.14159265358979323846;
. . .
}
虽然类中的域最好不要设计成public,但是公有常量(final域)可以。
4.4.3 静态方法
静态方法同样属于类且不属于类对象,或者说静态方法没有隐式参数(this)。静态方法不能访问实例域,因为它不能操作对象:不过它可以访问静态域。
使用静态方法时,建议使用类名而不是对象名调用静态方法。
使用静态方法的情况:
- 方法不访问对象状态,其所需参数通过显示参数提供(例如:Math.pow)。
- 方法只访问静态域。
4.4.4 工厂方法
静态方法的另一用途,如LocalDate和NumberFormat类使用的静态工厂方法(factory method)来构造对象。
NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance();
NumberFormat percentFormatter = NumberFormat.getPercentInstance();
double x = 0.1;
System.out.println(currencyFormatter.format(x)); // prints $0.10
System.out.println(percentFormatter.format(x)); // prints 10%
为什么用工厂方法而不用构造器?
- 无法重命名构造器。而工厂方法可以采用不同的名字。
- 构造器无法改变所构造的对象类型。而NumberFormat的工厂方法可以返回一个DecimalFormat类对象(NumberFormat的子类)
Java 设计模式 -- 工厂方法模式
4.4.5 main方法
main方法也是静态方法,不对任何对象进行操作。而且在启动程序时还没有任何对象,只有静态的main方法将执行并创建对象。
在类中添加一个main方法,可进行单元测试。
4.5 方法参数
Java采用的是按值调用,即方法得到的是参数值的一个拷贝,方法不能修改传递给它的参数变量的内容:
The term call by value means that the method gets just the value that the caller provides. In contrast, call by reference means that the method gets the location of the variable that the caller provides. Thus, a method can modify the value stored in a variable that is passed by reference but not in one that is passed by value.
按值调用意味着该方法只获取调用者提供的值。相反,引用的调用意味着该方法获取调用者提供的变量的位置。因此,一个方法可以修改被引用而不是通过值传递的变量中存储的值。
public static void tripleValue(double x) // doesn't work
{
x = 3 * x;
}
double percent = 10;
tripleValue(percent);
//执行后percent的值还是10;
方法参数有两种类型:基本数据类型、对象引用。
虽然不能修改基本数据类型,但是对象引用作参数就不同了:
public static void tripleSalary(Employee x) // works
{
x.raiseSalary(200);
}
harry = new Employee(. . .);
tripleSalary(harry);
/*
1 )X 被初始化为 harry 值的拷贝,这里是一个对象的引用。
2 ) raiseSalary 方法应用于这个对象引用。x 和 harry 同时引用的那个 Employee 对象的薪金提高了 200%。
3 ) 方法结束后,参数变量 x 不再使用。当然,对象变量 harry 继续引用那个薪金增至3倍的雇员对象
*/
看起来Java对对象采用的是引用调用,但实际上并不是:
public static void swap(Employee x, Employee y) // doesn't work
{
Employee temp = x;
x = y;
y = temp;
}
Employee a = new Employee("Alice", . . .);
Employee b = new Employee("Bob", . . .);
swap(a, b);
//交换的是x和y两个对象的引用拷贝,a和b的引用仍然没有变
Java 对对象采用的不是引用调用,实际上,对象引用是按值传递的。
Java中方法参数的使用情况:
- 方法不能修改基本数据类型的参数。
- 方法可以改变对象参数的状态。
- 方法不能让对象参数引用新的变量。
Java方法参数实例
4.6 对象构造
Java提供了多种编写构造器的机制。
4.6.1 重载
有些类可以有多个构造器
有相同的名字,不同的参数,这种特征叫做重载(overloading)。
编译器通过各方法的参数类型和返回类型匹配相应的方法,如果找不到匹配的参数,则产生编译错误。(这个过程被称为重载解析(overloading resolution)。)
Java允许重载任何方法,而不只是构造器方法。
方法的方法名和参数类型叫做方法的签名(signature)。返回类型不是方法签名的一部分。也就是说,不能有两个方法签名相同,返回类型不同的方法。
4.6.2 默认域的初始化
如果构造器没有显示的给域赋值,则默认为:数值为 0、布尔值为 false、 对象引用为 null。
This is an important difference between fields and local variables.You must always explicitly initialize local variables in a method. But in a class, if you don’t initialize a field, it is automatically initialized to a default (0, false, or null).
这是域与局部变量的主要不同点。 必须明确地初始化方法中的局部变量。但是,如果没有初始化类中的域,会被自动初始化为默认值( 0、false 或 null )。
如果不明确的对域进行初始化,会影响代码的可读性,这并不是一种良好的编程习惯。
4.6.3 无参数的构造器
当类中没有任何构造器时,系统才会提供一个默认的无参数的构造器,并将所有实例域设置为默认值。
如果类中提供了至少一个构造器,但是没有无参数的构造器,则构造对象不提供参数会被视为不合法。
4.6.4 显示域的初始化
在类定义时可以直接赋值给任何域。
在构造器之前,先执行赋值操作。如果类的所有构造器都需要将特定的实例字段设置为相同的值,那么这种语法特别有用。
也可以调用方法对域进行初始化。
class Employee{
private static int nextId;
private int id = assignId();
. . .
private static int assignId(){
int r = nextId;
nextId++;
return r;
}
. . .
}
4.6.5 参数名
编写构造器时,参数命名也是有讲究的。
public Employee(String n,double s){
name=n;
salary=s;
}
//这种的缺陷是要阅读源码才能理解n和s的含义
可以在参数前加上"a"
public Employee(String aName,double aSalary){
name=aName;
salary=aSalary;
}
//一目了然
参数变量使用相同的名字将实例域屏蔽起来。采用this访问实例域。
public Employee(String naie, double salary)
{
this.name = name;
this.salary = salary;
}
//this指隐式参数,即所构造的对象
4.6.6 调用另一个构造器
如果在构造器的第一个语句是this(...),这个构造器将调用同类中的另一个构造器。
public Employee(double s)
{
// calls Employee(String, double)
this("Employee #" + nextld, s);
nextld++;
}
使用this,对公共的构造器代码只编写一次
4.6.7 初始化块
前面已经讲了初始化数据域的两种方法:
- 在构造器中设置
- 在声明时赋值
还有第三种机制,初始化代码块(initialization block)。只要构造类的对象,这些块就会被执行。首先运行初始化代码块,然后才运行构造器的主体部分。
由于初始化数据域有多种途径,下面是调用构造器的具体处理步骤:
- 所有数据域被初始化为默认值。
- 按照在类中声明的次序,依次执行所有域初始化语句和初始化块。
- 如果构造器第一行调用了第二个构造器,则执行第二个构造器主体。
- 执行这个构造器的主体。
如果对类的静态域进行初始化的代码比较复杂,那么可以使用静态的初始化块。将代码放入一个块中,并标记关键字static。静态代码块按照类定义的顺序执行。
对象构造Dome
4.6.8 对象析构和finalize方法
在析构器中,最常见的操作是回收分配给对象的存储空间。由于 Java 有自动的垃圾回收器,不需要人工回收内存, 所以 Java 不支持析构器。
如果对象使用了内存之外的资源,如文件或系统资源的另一个对象的句柄,这时回收不需要的资源就十分重要。
可以为类添加一个finalize方法,此方法在对象被清除前调用。而在实际应用中,不要依赖于使用 finalize 方法回收任何短缺的资源, 因为很难知道这个方法什么时候才能够调用。
如果某个资源在使用完后需要立即关闭,可以应用close方法来完成相应的清理操作。
4.7 包
Java使用包(package)将类组织起来。使用包类确保类名的唯一性。
4.7.1 类的导入
使用import语句导入包,就可以使用包中的类。
使用(*)导入包比较简单,不过如果能够明确指出导入的类,就能显而易见的知道加载了哪些类。
如果包下的类命名冲突,如java.util和java.sql都有Date类,使用时需要在类名前加上完整的包名。
import java.util.*;
import java.sql.*;
import java.util.Date;
java.util.Date deadline = new java.util.Date();
java.sql.Date today = new java.sql.Date(...);
4.7.2 静态导入
import不仅可以导入包,还可以导入静态方法和静态域。
import static java.lang.System.*;
...
out.println("hi,world");
这种编写形式不利于代码的清晰度,不过用起来一定很不错。
4.7.3 将类放入包中
package语句将一个类放入包中,并且必须将包的名字放在源文件的开头。
如果开头没有package语句,这个源文件就被放置在默认包(default package)。
将包中的文件放到域完整的包名匹配的子目录中,编译器也将类文件放到相同的目录结构中。如果包和目录不匹配,虚拟机就找不到类。
4.7.4 包作用域
没有public和private修饰的部分,可以被同一个包中的所有方法访问。但是对于变量来说不合适,会破坏封装性。
视具体情况,包不是一个封闭的实体,可以修改。不过也可以通过包密封(package sealing)来防止再添加类。
4.8 类路径
类储存在文件系统的子目录中,类的路径必须与包名匹配。
为了使类能被多个程序共享,需要:
- 把类放到一个目录中,这个目录是包树状结构的基目录,例如c:\classdir。
- 将jar文件放到一个目录中,例如c:\archives。
- 设置类路径(class path)
- 类路径包括
- 基目录 c:\classdir;
- 当前目录(.);
- Jar文件 c:\archives\archive.jar
Windows 环境中,则以分号(;)分隔不同的项目
c:\classdir;.;c:\archives\archive.jar
4.8.1 设置类路径
最好采用-classpath指定类路径:
java -classpath c:\classdir;.;c:\archives\archive.jar MyProg
利用-classpath选项设置类路径是首选的方法, 也可以通过设置 CLASSPATH 环境变量 完成这个操作
4.9 文档注释
运行javadoc可以生成HTML文档。
以专用的定界符 /**开始的注释,可以很容易地生成一个文档,并且修改时,重新javadoc可以同步。
详细文档注释
4.9.1 注释的插入
javadoc从下面几个特性中抽取信息:
- 包
- 共有类和接口
- 共有的和受保护的构造器及方法
- 共有的和受保护的域
每个 /** . . . */ 文档注释在标记之后紧跟着自由格式文本(free-form text)。标记由@开始, 如@author 或@param。
4.9.2 类注释
类注释放在import语句之后,类定义之前。
4.9.3 方法注释
每一个方法注释必须放所在方法之前,可以使用下面标记:
- @param 变量描述:为当前方法的参数添加描述
- @return 描述:可进行多行描述
- @throws 类描述:此方法可能抛出异常
/**
* Raises the salary of an employee.
* @param byPercent the percentage by which to raise the salary (e.g. 10 means 10%)
* @return the amount of the raise
*/
public double raiseSalary(double byPercent)
{
double raise = salary * byPercent / 100;
salary += raise;
return raise;
}
4.9.4 域注释
只需要对公有域(通常指的是静态常量)建立文档。
4.9.5 通用注释
下面的注释可用在类文档的注释中:
- @author 姓名:作者
- @version 文本:对当前版本的描述
- @since 文本:对引入特性的版本的描述
- @deprecate 文本:此标记表示不再使用这个部分
- @see 引用:超链接
4.9.6 包和概述注释
可以将各种注释用/**. . . */文档注释界定。
但是,要生成单独的文件需要:
- 提供一个以package.html命名的文件。. . . 之间的文件会被抽取出来。
- 提供一个以package-info.java的文件。文件中包语句之后,紧跟/**. . . */注释,不需要其他多余的注释和代码。
- 也可以为所以源文件提供一个概述性的注释,写在overview.html文件中,这个文件位于所有源文件的父目录中。
4.9.7 注释的抽取
假设HTML文件被放在docDirectory下。执行步骤如下:
切换到想要生成文档的源文件目录。
-
运行命令。
//如果是一个包 javadoc -d docDirectory nameOfPackage //多个包 javadoc -d docDirectory nameOfPackage1 nameOfPackage2. . . //默认包 Javadoc -d docDirectory *.java
可以使用多种形式的命令对Javadoc程序进行调整。
4.10 类设计技巧
类设计的一些简单的技巧:
- 保证数据的私有性。不要破坏封装性。
- 初始化数据,Java不会对局部变量初始化,可以提供默认值。
- 不要在类中使用过多的基本类型。用类代替。
- 不是所以的域都需要独立的域访问器和域更改器。
- 分解那些太多职责的类
- 类名和方法名要能够体现它们的职责 ,采用名词是个好习惯。
- 优先使用不可变的类。如LocalDate类没有方法修改对象的状态。不过也不是所有类都要是不可变的。
参考资料和链接:
类与类之间的几种关系
考虑用静态工厂方法代替构造器
日历程序
请注意不要编写返回可变对象引用的访问器方法_
Java 设计模式 -- 工厂方法模式
Java方法参数实例
对象构造Dome
Core Java Volume I--Fundamentals, 10th Edition
Java核心技术 卷1 基础知识 原书第10版 中文版