4.1 面向对象程序设计概述
4.1.1 类
- 类(class)是构造对象的模版或蓝图。由类构造(construct)对象的过程称为创建类的实例(instance)。
- 标准Java库提供几千个类供使用,但还是需要创建自己的类,以描述应用程序所对应的问题域中的对象。
- 封装(encapsulation,也称为数据隐藏),对对象的使用者隐藏了数据的实现方式。实现封装的关键就是绝不能让其他类的方法访问本类的实例域。封装给对象赋予了黑盒特性。这是提高重用性和可靠性的关键。
- 对象状态:每个特定对象都有一组特定的实例域(instance field)值,这些值的集合就是这个对象的当前状态。
- 对象的三个主要特征
2)对象的状态(state) ---对象状态的改变,必须通过调用方法实现
3)对象的标识(identity)
4.1.3 识别类
- 名次对应类
- 动词对应类的方法
4.1.4 类之间的关系
- 依赖 dependence(uses-a):如果一个类的方法操作另一个类的对象,我们就说一个类依赖于另一个类。
2)如果类A不知道B的存在,B的改变不会引起A的错误。
- 聚合aggregation(has-a):类A的对象包含类B的对象。
4.2 使用预定义类
4.2.1 对象与对象变量
- 首先构造对象 -> 指定其初始状态 -> 对对象应用方法
- 使用构造器(constructor)构造新实例。用来构造和初始化对象。
- new操作符返回的也是一个对象的引用。
- 方法中的局部变量不会自动地初始化为null,而必须通过调用new或将他们设置为null进行初始化。
- 标准Java类库分别包含了两个类:一个是用来表示时间点的Date类,另一个是用来表示日历表示法的GregorianCalendar
- 对实例域进行修改的方法称为更改器方法。
- 仅访问实例域而不进行修改的方法称为访问器方法。
4.3 用户自定义类
- 在一个Java源文件中,只能有一个public类,但可以有任意多个非公有类。源文件的文件名必须和public类的类名相同。
- 实例域通常标记为private
- 类通常包括类型属于某个类类型的实例域
- 构造器与类同名,并在构造类的对象时,将实例域初始化为希望的状态。
- 每个类可以有一个以上的构造器
- 构造器可以有0个、1个及1个以上的参数
- 构造器没有返回值
- 构造器总是伴随着new操作一起调用
- 在类的所有方法中(包括构造器),不要命名与实例域同名的变量,否则在方法内部,会屏蔽掉实例域。
- 隐式参数与显示参数
{
double raise = this.salary * byPercent / 100;
this.salary += raise;
}
//raiseSalary方法有两个参数。第一个参数为隐式(implicit)参数,表示该对象本身,第二个参数是在方法名后的参数列表中的数值,称为显示(explicit)参数。在一个方法中,关键字this表示隐式参数。使用this可以将实例域和局部变量明显的区分开来。
- 不要编写返回引用可变对象的访问器方法,否则会破坏封装性。
{
private Date hireDay;
.....
public Date getHireDay(){
return hireDay;
}
}
如果需要返回一个可变对象的引用,应该首先对它进行克隆(clone),对象克隆是指存放在另一个位置上的对象副本。
class Employee
{
......
public Date getHireDay(){
return hireDay.clone();
}
}
- 基于类的访问权限
class Employee
{
..........
public boolean equals(Employee other){
return name.equals(other.name);
}
}
典型的调用方法,if(harry.equals(boss))....
这个方法访问了harry的私有域,而且还访问了boss的私有域,这是合法的,因为boss是Employee对象,而Employee类的方法可以访问Employee类的任何一个对象的私有域。
- 只要方法是私有的,类的设计者可以确信,它不会被外部的其他类操作调用,可以将其删除。如果方法是公有的,就不能将其删除,因为其他的代码很可能已经依赖它了。
- final实例域
final修饰符大都应用于基本(primitive)类型域,或不可变类的域。用于可变的类,通常会引起混乱。
- 不可变(immutable)类的域:如果类中的每个方法都不会改变其对象,这种类就是不可变类。如String类就是不可变类。
4.4 静态域与静态方法
4.4.1 静态域
- 如果将域定义为static,每个类只有一个这样的域。而每个对象对于所有的实例域却都有自己的一份拷贝。
{
private static int nextId = 1; //没这个类的所有实例所共享
private int id; //每一个雇员对象都有一个自己的id域
public setId(){
id = nextId;
nextId++;
}
}
4.4.2 静态常量
- 静态变量用的比较少,但静态常量用得比较多
......
public static final double PI = 3.1415926;
......
}
//程序中可以直接采用Math.PI的形式获得这个常量。
- 另一个经常使用的静态常量是System.out
{
public static final PrintStream out = ...;
}
4.4.3 静态方法
- 静态方法是一种不能向对象实施操作的方法。
- 静态方法没有隐式的参数。
- 可以认为静态方法是没有this参数的方法。
- 因为静态方法不能操作对象,所以不能在静态方法中访问实例域。但是静态方法可以访问自身类中的静态域。
{
return nextid; //return static field
}
可以通过类名调用这个方法:
int n= Employee.getNextId();
- 可以使用对象调用静态方法,但不推荐这样做,容易引起混淆,建议使用类名来调用静态方法。
- 在下面两种情况下使用静态方法:
2)一个方法只需要访问类的静态域。
4.4.4 工厂方法
- 静态方法还有一个常见的用途,使用工厂方法产生不同风格的格式对象。
4.4.5 main方法
- main方法也是一个静态方法。main方法不对任何对象进行操作。事实上,在启动程序时,还没有任何一个对象。静态的main方法将执行并创建程序所需的对象。
- 每一个类都可以有一个main方法,这是一个常用于对类进行单元测试的技巧。
4.5 方法参数
- Java总是采用按值调用,即,方法得到的是所有参数值的一个拷贝,特别的,方法不能修改传递给它的任何参数变量的内容。
4.6 对象构造
4.6.1 重载
- 重载(overloading):多个方法,有相同的名字,不同的参数,便产生了重载。
- 方法签名(signature):Java允许重载任何方法,而不只是构造器方法。因此完整地描述一个方法,需要指出方法名以及参数类型。这叫做方法签名。
- 返回类型不是方法签名的一部分,因此,不能有两个名字相同、参数类型也相同,但返回类型不同的方法。
- 如果在构造器中,没有显示的给域赋初始值,那么就会被自动地赋予默认值:0、false、null,但这不是好的编程习惯。
- 但方法中的局部变量,则必须明确的进行初始化,不会像域变量那样被赋予初始值。否则会报错。
- 如果在编写一个类时,没有编写构造器,那么系统就会提供一个无参数的构造器。这个构造器将所有的实例域设置为默认值(0,false,null)。
- 如果在类中已经提供了至少一个非无参数的构造器,则系统不再自动为该类提供一个无参数的构造器。此时,就不能使用无参数构造器来构造对象。除非自己在类中手动显示地添加一个构造器。
{
}
//上述构造器构造对象时,会将所有域赋予默认值(0,false,null)
4.6.4 显式域初始化
- 由于类的构造方法可以重载,所以可以采用多种形式设置类的实例域的初始状态。确保不管怎样调用构造器,每个实例域都可以被设置为一个有意义的初值。
- 可以在类定义中,直接将一个值赋给任何域。
{
private string name = "";
...........
}
//在构造器之前,先执行赋值操作
- 当一个类的所有构造器,都希望将一个特定的值赋予某个特定的实例域时,这种方式特别有用。
1) 构造器的参数名用单个字符命名
public Employee(String n, double s)
{
name = n;
salary = s;
}
//这种方式的缺点是,参数的可读性不佳
2) 构造器的参数名在域名称的基础上加上一个前缀 a
public Employee(String aName,double aSalary)
{
name = aName;
salary = aSalary;
}
3) 构造器的参数名和域名称完全一样
public Employee(String name,double salary)
{
this.name = name;
this.salary = salary;
}
//这种方式会使参数将实例域在构造器内部屏蔽起来,但可以采用this.salary的形式访问实例域。this指示的是方法调用的隐式对象,也就是被构造的对象。
4.6.6 调用另一个构造器
- 如果构造器的第一个语句形如this(...),这个构造器将调用同一个类的另一个构造器。
{
this("emp" + nextId, salary);
nextId++;
}
- 一般都是参数个少的构造器调用参数个数多的构造器。
- 采用这种方式非常有用,这样对公共的构造器代码编写一次即可。
- 第三种初始化域的机制,初始化块(initialization block),在一个类声明中,可以包含多个代码块,只要构造类的对象,这些块就会被执行。
{
private static int nextId;
private int id;
private String name;
private int salary;
{
id = nextId;
nextId++;
}
public Employee(String n, double s)
{
name = n;
salary =s;
}
public Employee()
{
name = "";
salary = 0;
}
}
//无论使用哪个构造器构造对象,id域都在对象初始化块中被初始化。首先运行初始化块,然后再运行构造器的主体部分。
4.6.8 对象析构与finalize方法
- 由于Java有自动的垃圾回收器,不需要人工回收内存,所以Java不支持析构器。
- 但,某些对象使用了内存之外的其他资源,如文件或系统资源的另一个对象的句柄。在这种情况下,当资源不再需要时,将其回收或再利用显得十分的重要。
- 可以为任何一个类添加finalize方法。finalize方法将在垃圾回收器清除对象之前调用。在实际应用中,不要依赖于使用finalize方法回收任何短缺的资源。这是因为很难知道这个方法什么时候才能够调用。
- 如果某个资源需要在使用完毕后立刻被关闭,那么就需要由人工来管理。对象用完时,可以应用close方法来完成相应的清理操作。
4.7 包
- Java允许使用包(package)将类组织起来。借助于包可以方便地组织自己的代码,并将自己的代码与别人提供的代码库分开管理。
- 所有标准的Java包都处于java和javax包层次中。
- 使用包的主要原因是确保类名的唯一性。Sun公司建议将公司的英特网域名(显然是独一无二的)以逆序的形式作为包名,并且对于不同的项目使用不同的子包。
- 从编译器的角度来看,嵌套的包之前没有任何关系。如:java.util和java.util.jar包毫无关系,每一个都拥有独立的类集合。
- 一个类能够使用所属包中的所有类,以及其他包中的公有类(public class)。
- import语句是一种引用包含在包中的类的简明描述。一旦使用了import语句,在使用类时,就不必写出包的全名了。
- import语句不仅可以导入类,还增加了导入静态方法和静态域的功能。
- 想将一个类放入包中,就必须将包的名字放在源文件的开头,包中定义类的代码之前。
public class Employee
{
.....
}
- 如果没有在源文件中放置package语句,这个源文件中的类就被放置在一个默认包(default package)中。默认包是一个没有名字的包。
- 将包中的源文件放到与完整的包名匹配的子目录中。例如,com.horstmann.corejava包中的所有源文件应该被放置到com/horstmann.corejava目录中。编译器将class类文件也放在相同的目录结构中。
- 在默认情况下,包不是一个封闭的实体。也就是说,任何人都可以向包中添加更多的类。
- 可以通过包密封(package sealing)机制将一个包密封起来,就不能向这个包添加类了。
- 制作包含密封包的JAR文件的方法。
4.8 类路径--目的是让Java程序在运行时,JVM能够顺利找到各个.class类文件
- 类存储在文件系统的子目录中,类的路径必须和包名匹配
- 类文件也可以存储在JAR(Java归档)文件中,在一个JAR文件中,可以包含多个压缩形式的类文件和子目录。在程序中用到第三方(third-party)库文件时,通常给出一个或多个需要包含的JAR文件。
- JDK也提供了许多JAR文件。在jre/lib/rt.jar中包含数千个类库文件。
- 为了使类能被多个程序共享,需要做到下面几点:
2)将JAR文件放在一个目录中,例如:/home/user/archives
3)设置类路径,类路径是所有包含类文件的路径的集合。
UNIX环境(用冒号分割):
/home/user/classdir:.:/home/user/archives/archive.jar
Windows环境(用分号分割):
c:classdir;.;c:archivesarchive.jar
上述,句点(.)表示当前目录
- 可以在JAR文件目录中,指定通配符:表示在归档目录中的所有JAR文件(不包含.class文件)都包含在类路径中。
c:classdir;.;c:archives*
- 由于运行时库文件(rt.jar和在jre/lib与jre/lib/ext目录下的一些其他的JAR文件)会被自动搜索,所以不必将他们显示的列在类路径中。
- javac编译器总是在当前目录中查找文件,但Java虚拟机仅在类路径中有“.”时才查看当前目录。如果没有设置类路径,那也不会产生什么问题,默认的类路径包含“.”目录。但如果设置了类路径,却忘记包含“.”目录,则程序任然可以编译通过,但不能运行。
- 类路径所列出的目录和归档文件是搜索类的起点。假设虚拟机要寻找某个类com.hostmann.corejava.Employee类文件:
2)/home/user/classdir/com/hostmann/corejava/Employee.class
3)com/hostmann/corejava/Employee.class 从当前目录开始
4)com/hostmann/corejava/Employee.class inside /home/user/archives/archive.jar
- 设置类路径
java -classpath /home/user/classdir:.:/home/user/archives/archive.jar MyProg
java -classpath c:classidr;.;c:archivesarchive.jar MyProg
2)也可以通过设置classpath环境变量完成这个操作
export CLASSPATH=/home/user/classdir:.:/home/user/archives/archive.jar
set CLASSPATH=c:classidr;.;c:archivesarchive.jar
知道shell退出,类路径都有效。
4.9 文档注释
- JDK提供了javadoc工具,用于有源文件生成一个HTML文档。
- 如果在源文件中添加了以专用的定界符/**开始的注释,就可以很容易的生产一个专业的文档。
- javadoc工具从下面几个特性中抽取信息
2)公有类和接口
3)公有的和受保护的构造器和方法
4)公有的和受保护的域
- 注释以/**开始,以*/结束
- 每个/**.....*/文档注释在标记之后紧跟着自由格式文本,标记由@开始,如@author 或 @param
- 必须放在import语句之后,类定义之前
- 必须放在所描述的方法之前,除了通用的标记外,还可以使用如下标记
2)@return 描述
3)@throws 类描述
4.9.4 域注释
- 只需要对公有域(通常指的是静态常量)建立文档
- @author姓名
- @version文本
- @since文本
- @deprecated文本
- @see 引用
4.9.7 注释的抽取
4.10 类的设计技巧
- 一定要保证数据私有,绝对不要破坏封装性
- 一定要对数据初始化
- 不要在类中使用过多的基本类型
- 不是所有的域都需要独立的域访问器和域更改器
- 将职责过多的类进行分解
- 类名和方法名要能够体现他们的职责