1 面向对象程序设计概述
面向对象程序设计(简称OOP) 是当今主流的程序设计范型。
传统的结构化程序设计通过设计一系列的过程(即算法)来求解问题。一旦确定了这些过程, 就要开始考虑存储数据的方式。这就是Pascal 语言的设计者Niklaus Wirth 将其著作命名为《算法+ 数据结构= 程序》(Algorithms + Data Structures = Programs, Prentice Hall, 1975 )的原因。需要注意的是,在Wirth 命名的书名中, 算法是第一位的, 数据结构是第二位的,这就明确地表述了程序员的工作方式。首先要确定如何操作数据, 然后再决定如何组织数据, 以便于数据操作。
OOP 却调换了这个次序, 将数据放在第一位, 然后再考虑操作数据的算法。
1.1 类
类( class ) 是构造对象的模板或蓝图。
我们可以将类想象成制作小甜饼的切割机,将对象想象为小甜饼。由类构造(construct ) 对象的过程称为创建类的实例(instance ) .
封装( encapsulation , 有时称为数据隐藏) 是与对象有关的一个重要概念。
- 从形式上看,封装不过是将数据和行为组合在一个包中, 并对对象的使用者隐藏了数据的实现方式。
- 对象中的数据称为实例域( instance field ), 操纵数据的过程称为方法( method )。
- 对于每个特定的类实例(对象)都有一组特定的实例域值。这些值的集合就是这个对象的当前状态( state )。
- 封装给对象赋予了“ 黑盒” 特征, 这是提高重用性和可靠性的关键。
OOP 的另一个原则会让用户自定义Java 类变得轻而易举, 这就是: 可以通过扩展一个类来建立另外一个新的类。事实上, 在Java 中, 所有的类都源自于一个“ 神通广大的超类”,它就是Object。
1.2 对象
对象的三个主要特性:
- 对象的行为(behavior)—可以对对象施加哪些操作, 或可以对对象施加哪些方法?
- 对象的状态(state )—当施加那些方法时,对象如何响应?
- 对象标识(identity )—如何辨别具有相同行为与状态的不同对象?
作为一个类的实例, 每个对象的标识永远是不同的, 状态常常也存在着差异。
1.3 识别类
传统的过程化程序设计, 必须从顶部的main 函数开始编写程序。在面向对象程序设计时没有所谓的“ 顶部”。对于学习OOP 的初学者来说常常会感觉无从下手。答案是:首先从设计类开始,然后再往每个类中添加方法。
1.4 类之间的关系
在类之间, 最常见的关系有
- 依赖(“ uses-a”)
- 聚合(“ has-a”)
- 继承(“ is-a”)
2 预定义类
2.1 对象与对象变量
要想使用对象,就必须首先构造对象, 并指定其初始状态。然后,对对象应用方法。
在Java 程序设计语言中, 使用构造器( constructor ) 构造新实例。构造器是一种特殊的方法, 用来构造并初始化对象。
在对象与对象变量之间存在着一个重要的区别。例如, 语句
Date deadline; // deadline doesn't refer to any object
定义了一个对象变量deadline, 它可以引用Date 类型的对象。但是, 一定要认识到: 变量deadline 不是一个对象, 实际上也没有引用对象。此时,不能将任何Date 方法应用于这个变量上。语句
s = deadline.toString(); // not yet
将产生编译错误。
一定要认识到: 一个对象变量并没有实际包含一个对象,而仅仅引用一个对象。
2.2 更改器方法与访问器方法
只访问对象而不修改对象的方法有时称为访问器方法。
3 自定义类
这些类没有main 方法, 却有自己的实例域和实例方法。
EmployeeTest/EmployeeTest.java
import java.util.*;
/**
* This program tests the Employee class.
* @version 1.11 2004-02-19
* @author Cay Horstmann
*/
public class EmployeeTest
{
public static void main(String[] args)
{
// fill the staff array with three Employee objects
Employee[] staff = new Employee[3];
staff[0] = new Employee("Carl Cracker", 75000, 1987, 12, 15);
staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
staff[2] = new Employee("Tony Tester", 40000, 1990, 3, 15);
// raise everyone's salary by 5%
for (Employee e : staff)
e.raiseSalary(5);
// print out information about all Employee objects
for (Employee e : staff)
System.out.println("name=" + e.getName() + ",salary=" + e.getSalary() + ",hireDay="
+ e.getHireDay());
}
}
class Employee
{
public Employee(String n, double s, int year, int month, int day)
{
name = n;
salary = s;
GregorianCalendar calendar = new GregorianCalendar(year, month - 1, day);
// GregorianCalendar uses 0 for January
hireDay = calendar.getTime();
}
public String getName()
{
return name;
}
public double getSalary()
{
return salary;
}
public Date getHireDay()
{
return hireDay;
}
public void raiseSalary(double byPercent)
{
double raise = salary * byPercent / 100;
salary += raise;
}
private String name;
private double salary;
private Date hireDay;
}
在这个程序中,构造了一个Employee 数组, 并填人了三个雇员对象:
Employee staff = new Employee[3];
staff[0] = new Employee("Carl Cracker", ...);
staff[1] = new Employee("Harry Hacker", . . .);
staff[2] = new Employee("Tony Tester", . . .);
接下来, 利用 Employee 类的 raiseSalary 方法将每个雇员的薪水提高5%:
for (Employee e : staff)
e.raiseSalary(5);
最后,调用 getName 方法、getSalary 方法和 getHireDay 方法将每个雇员的信息打印出来:
for (Employee e : staff)
System.out.println("name=" + e.getName()
+ ",sala「y=" + e.getSalaryO
+ ",hireDay=" + e.getHi reDayO) ;
注意,在这个示例程序中包含两个类:Employee 类和带有 public 访问修饰符的 EmployeeTest类。EmployeeTest 类包含了main 方法。
源文件名是 EmployeeTest.java,这是因为文件名必须与 public 类的名字相匹配。在一个源文件中, 只能有一个公有类,但可以有任意数目的非公有类。
接下来, 当编译这段源代码的时候, 编译器将在目录下创建两个类文件: EmployeeTest.class 和 Employee.class。
将程序中包含main 方法的类名提供给字节码解释器, 以便启动这个程序:
java EmployeeTest
字节码解释器开始运行 EmployeeTest 类的main 方法中的代码。在这段代码中,先后构造了三个新Employee 对象, 并显示它们的状态。
3.1 多个源文件的使用
在前面 EmployeeTest/EmployeeTest.java
这个程序中,一个源文件包含了两个类。许多程序员习惯于将每一个类存在一个单独的源文件中。例如,将Employee 类存放在文件 Employee.java 中, 将 EmployeeTest 类存放在文件EmployeeTest.java 中。
如果喜欢这样组织文件, 将可以有两种编译源程序的方法。一种是使用通配符调用 Java 编译器:
javac Employee*.java
于是, 所有与通配符匹配的源文件都将被编译成类文件。或者键入下列命令:
javac EmployeeTest.java
使用第二种方式,并没有显式地编译 Employee.java。然而, 当Java 编译器发现 EmployeeTest.java 使用 Employee 类时会查找名为 Employee.class 的文件。如果没有找到这个文件, 就会自动地搜 Employee.java, 然后,对它进行编译。更重要的是:如果 Employee.java 版本较已有的 Employee.class 文件版本新, Java 编译器就会自动地重新编译这个文件。
3.2 对类进行解剖
对前面的 Employee 类进行解剖。从类的方法开始,这个类包含一个构造器和 4 个方法。
public Employee(String n, double s, int year, int month, int day)
public String getName()
public double getSalary()
public Local Date getHireDay()
public void raiseSalary(double byPercent)
这个类的所有方法都被标记为public。关键字public 意味着任何类的任何方法都可以调用这些方法。
接下来,需要注意在Employee 类的实例中有三个实例域用来存放将要操作的数据:
private String name;
private double salary;
private LocalDate hireDay;
关键字private 确保只有Employee 类自身的方法能够访问这些实例域, 而其他类的方法不能够读写这些域。
可以用public 标记实例域, 但这是一种极为不提倡的做法, public 数据域允许程序中的任何方法对其进行读取和修改。这就完全破坏了封装。 任何类的任何方法都可以修改public 域, 从我们的经验来看, 某些代码将使用这种存取权限, 而这并不我们所希望的, 因此, 这里强烈建议将实例域标记为private。
请注意, 有两个实例域本身就是对象: name 域是String 类对象, hireDay 域是LocalDate 类对象。这种情形十分常见:类通常包括类型属于某个类类型的实例域。
3.2.1 从构造器开始
public Employee(String n, double s, int year, int month, int day)
{
name = n;
salary = s;
LocalDate hireDay = LocalDate.of(year , month, day) ;
}
可以看到, 构造器与类同名。在构造 Employee 类的对象时, 构造器会运行,以便将实例域初始化为所希望的状态。
例如, 当使用下面这条代码创建 Employee 类实例时:
new Employee("James Bond", 100000, 1950, 1, 1)
将会把实例域设置为:
name = "James Bond";
salary = 100000;
hireDay = LocalDate.of(1950, 1, 1); // January 1, 1950
构造器与其他的方法有一个重要的不同。构造器总是伴随着new 操作符的执行被调用,而不能对一个已经存在的对象调用构造器来达到重新设置实例域的目的。例如,
janes.EmployeeCJames Bond", 250000, 1950, 1, 1) // ERROR
将产生编译错误。
构造器的特点:
- 构造器与类同名
- 每个类可以有一个以上的构造器
- 构造器可以有0 个、1 个或多个参数
- 构造器没有返回值
- 构造器总是伴随着new 操作一起调用
3.2.2 隐式参数与显式参数
方法用于操作对象以及存取它们的实例域。例如,方法:
public void raiseSalary(double byPercent)
{
double raise = salary * byPercent / 100;
salary += raise;
}
将调用这个方法的对象的salary 实例域设置为新值。看看下面这个调用:
number007.raiseSalary(5) ;
它的结果将number007.salary 域的值增加5%。具体地说,这个调用将执行下列指令:
double raise = nuaber007.salary * 5 / 100;
nuiber007.salary += raise;
raiseSalary 方法有两个参数。第一个参数 salary 称为隐式( implicit ) 参数, 是出现在方法名前的Employee 类对象。第二个参数 byPercent 位于方法名后面括号中的数值, 这是一个显式( explicit ) 参数( 有些人把隐式参数称为方法调用的目标或接收者。)
可以看到, 显式参数是明显地列在方法声明中的, 例如double byPercent。隐式参数没有出现在方法声明中。
在每一个方法中, 关键字 this 表示隐式参数。如果需要的话, 可以用下列方式编写 raiseSalary 方法:
public void raiseSalary(double byPercent)
{
double raise = this.salary * byPercent / 100;
this.salary += raise;
}
有些程序员更偏爱这样的风格,因为这样可以将实例域与局部变量明显地区分开来。
3.2.3 封装的优点
看一下非常简单的getName 方法、getSalary 方法和 getHireDay 方法。
public String getName()
{
return name;
}
public double getSalaryO
{
return salary;
}
public LocalDate getHireDay()
{
return hireDay;
}
这些都是典型的访问器方法。由于它们只返回实例域值, 因此又称为域访问器。
将name、salary 和hireDay 域标记为public , 以此来取代独立的访问器方法会不会更容易些呢?
关键在于name 是一个只读域。一旦在构造器中设置完毕, 就没有任何一个办法可以对它进行修改,这样来确保name 域不会受到外界的破坏。
虽然salary 不是只读域,但是它只能用raiseSalary 方法修改。特别是一旦这个域值出现了错误, 只要调试这个方法就可以了。如果salary 域是public 的, 破坏这个域值的捣乱者有可能会出没在任何地方
在有些时候, 需要获得或设置实例域的值。因此,应该提供下面三项内容:
- 一个私有的数据域;
- 一个公有的域访问器方法;
- 一个公有的域更改器方法。
这样做要比提供一个简单的公有数据域复杂些, 但是却有着下列明显的好处:
- 首先, 可以改变内部实现,除了该类的方法之外, 不会影响其他代码。
- 其次更改器方法可以执行错误检查, 然而直接对域进行赋值将不会进行这些处理。
注意不要编写返回引用可变对象的访问器方法。在Employee 类中就违反了这个设计原则, 其中的getHireDay方法返回了一个Date 类对象:
class Employee
{
private Date hireDay;
...
public Date getHireDay()
{
return hireDay; // Bad
}
...
}
LocalDate 类没有更改器方法, 与之不同, Date 类有一个更改器方法setTime, 可以 在这里设置毫秒数。 Date 对象是可变的,这一点就破坏了封装性! 请看下面这段代码:
Employee harry = . .
Date d = harry.getHireDay();
double tenYearsInMilliSeconds = 10*365.25*24*60*60*1000;
d.setTime(d.getTime() - (long) tenYearsInMilliSeconds);
// let 's give Harry ten years of added seniority
出错的原因很微妙。d 和harry.hireDay 引用同一个对象。更改器方法就可以自动地改变这个雇员对象的私有状态!
如果需要返回一个可变对象的引用, 应该首先对它进行克隆( clone )。对象clone 是
指存放在另一个位置上的对象副本。下
面是修改后的代码:
class Employee
{
...
public Date getHireDay()
{
return (Date)hireDay.clone(); // OK
}
...
}
3.3 基于类的访问权限
从前面已经知道,方法可以访问所调用对象的私有数据。一个方法可以访问所属类的所有
对象的私有数据。
class Employee
{
public boolean equals(Employee other)
{
return name.equals(other.name) ;
}
}
典型的调用方式是
if (harry.equals(boss)) . . .
这个方法访问harry 的私有域, 这点并不会让人奇怪,然而, 它还访问了boss 的私有域。这是合法的, 其原因是boss 是Employee 类对象, 而Employee 类的方法可以访问Employee 类的任何一个对象的私有域。
3.4 私有方法
在实现一个类时,由于公有数据非常危险, 所以应该将所有的数据域都设置为私有的。然而,方法又应该如何设计呢?
尽管绝大多数方法都被设计为公有的,但在某些特殊情况下,也可能将它们设计为私有的。有时,可能希望将一个计算代码划分成若干个独立的辅助方法。通常, 这些辅助方法不应该成为公有接口的一部分,这是由于它们往往与当前的实现机制非常紧密, 或者需要一个特别的协议以及一个特别的调用次序。最好将这样的方法设计为private 的。
在Java 中,为了实现一个私有的方法, 只需将关键字public 改为private 即可。
3.5 final 实例域
可以将实例域定义为final。构建对象时必须初始化这样的域。也就是说, 必须确保在每一个构造器执行之后, 这个域的值被设置, 并且在后面的操作中, 不能够再对它进行修改。
例如, 可以将Employee 类中的name 域声明为final , 因为在对象构建之后, 这个值不会再被修改, 即没有setName 方法。
class Employee
{
private final String name;
...
}
final 修饰符大都应用于基本(primitive ) 类型域, 或不可变(immutable) 类的域(如果类中的每个方法都不会改变其对象, 这种类就是不可变的类。例如,String 类就是一个不可变的类)。
3.6 静态域与静态方法
3.6.1 静态域(静态变量)
如果将域定义为static, 每个类中只有一个这样的域。而每一个对象对于所有的实例域却都有自己的一份拷贝。
例如, 假定需要给每一个雇员賦予唯一的标识码。这里给Employee 类添加一个实例域id 和一个静态域nextId:
class Employee
{
private static int nextId = 1;
private int id;
...
}
现在, 每一个雇员对象都有一个自己的id 域, 但这个类的所有实例将共享一个 nextId 域。换句话说, 如果有1000 个Employee 类的对象, 则有1000 个实例域id。但是, 只有一个静态域 nextld。即使没有一个雇员对象, 静态域nextld 也存在。它属于类,而不属于任何独立的对象。
3.6.2 静态常量
静态变量使用得比较少, 但静态常量却使用得比较多。例如, 在Math 类中定义了一个
静态常量:
public class Hath
{
public static final double PI = 3.14159265358979323846;
}
在程序中,可以采用Math.PI 的形式获得这个常量。
如果关键字static 被省略, PI 就变成了Math 类的一个实例域。需要通过Math 类的对象访问PI,并且每一个Math 对象都有它自己的一份PI 拷贝。
3.6.3 静态方法
静态方法是一种不能向对象实施操作的方法。例如, Math 类的 pow 方法就是一静态方法。表达式
Math.pow(x, a)
计算幂 X^a。 在运算时, 不使用任何 Math 对象。换句话说,没有隐式的参数。
可以 静态方法是没有this 参数的方法,它不能操作实例域。但是,静态方法可以访问自身类中的静态域。下面是使用这种静态方法的一个示例:
public static int getNextId()
{
return nextId; // returns static field
}
可以通类名调用这个方法:
int n = Employee.getNextld();
在下面 2 种情况下使用静态方法:
- 一方法不需要访问对象状态,其所需参数都是通过显式参数提供(例如: Math.pow) 。
- 一个方法只需要访问类的静态域(例如:Employee.getNextld)
3.6.4 工厂方法
// TODO:待完善
3.6.5 main 方法
需要注意, 不需要使用对象调用静态方法。例如,不需要构造Math 类对象就可以调用Math.pow。
同理, main 方法也是一个静态方法。
public class Application
{
public static void main(StringD args)
{
// construct objects here
...
}
}
每一个类可以有一个main 方法。这是一个常用于对类进行单元测试的技巧。例如, 可以在 Employee 类中添加一个 main 方法:
class Employee
{
public Employee(String n, double s, int year, int month, int day)
{
name = n;
salary = s;
Local Date hireDay = Local Date.now(year, month, day) ;
}
...
public static void main(St ri ng[] args) // unit test
{
Employee e = new Employee("Romeo" , 50000, 2003, 3, 31) ;
e.raiseSalary(10);
System.out .println(e .getName() + " " + e.getSalary()) ;
}
...
}
如果想要独立地测试 Employee 类, 只需要执行
java Employee
如果Employee 类是一个更大型应用程序的一部分, 就可以使用下面这条语句运行程序
java Application
Employee 类的 main 方法永远不会执行。
3.6.6 小结
下面的程序包含了Employee 类的一个简单版本, 其中有一个静态域 nextld和一个静态方法 getNextId 这里将 5 个Employee 对象写入数组, 然后打印雇员信息。最后,打印出下一个可用的员工标识码来展示静态方法。
/**
* This program demonstrates static methods.
* @version 1.01 2004-02-19
* @author Cay Horstmann
*/
public class StaticTest
{
public static void main(String[] args)
{
// fill the staff array with three Employee objects
Employee[] staff = new Employee[3];
staff[0] = new Employee("Tom", 40000);
staff[1] = new Employee("Dick", 60000);
staff[2] = new Employee("Harry", 65000);
// print out information about all Employee objects
for (Employee e : staff)
{
e.setId();
System.out.println("name=" + e.getName() + ",id=" + e.getId() + ",salary="
+ e.getSalary());
}
int n = Employee.getNextId(); // calls static method
System.out.println("Next available id=" + n);
}
}
class Employee
{
public Employee(String n, double s)
{
name = n;
salary = s;
id = 0;
}
public String getName()
{
return name;
}
public double getSalary()
{
return salary;
}
public int getId()
{
return id;
}
public void setId()
{
id = nextId; // set id to next available id
nextId++;
}
public static int getNextId()
{
return nextId; // returns static field
}
public static void main(String[] args) // unit test
{
Employee e = new Employee("Harry", 50000);
System.out.println(e.getName() + " " + e.getSalary());
}
private String name;
private double salary;
private int id;
private static int nextId = 1;
}
需要注意, Employee 类也有一个静态的main 方法用于单元测试。试试运行
java Employee
和
java StaticTest
执行两个main 方法。
4 方法参数
程序设计语言中有关将参数传递给方法(或函数)的一些专业术语:
- 按值调用( call by value) 表示方法接收的是调用者提供的值。
- 按引用调用( call by reference )表示方法接收的是调用者提供的变量地址。
Java 程序设计语言对对象采用的不是引用调用,实际上, 对象引用是按值传递的。
5 对象构造
Java 提供了多种编写构造器的机制。下面将详细地介绍这些机制。
5.1 重载
有些类有多个构造器。例如, 可以如下构造一个空的 StringBuilder 对象:
StringBuilder messages = new StringBuilder();
或者, 可以指定一个初始字符串:
StringBuilder todoList = new StringBuilder("To do:\n")
这种特征叫做重载( overloading)。 如果多个方法(比如, StringBuilder 构造器方法)有相同的名字、不同的参数,便产生了重载。
编译器必须挑选出具体执行哪个方法,它通过用各个方法给出的参数类型与特定方法调用所使用的值类型进行匹配来挑选出相应的方法。如果编译器找不到匹配的参数, 就会产生编译时错误, 因为根本不存在匹配, 或者没有一个比其他的更好。(这个过程被称为重载解析(overloading resolution)。)
Java 允许重载任何方法, 而不只是构造器方法。因此,要完整地描述一个方法,需要指出方法名以及参数类型。这叫做方法的签名(signature)。
5.2 默认阈初始化
如果在构造器中没有显式地给域赋予初值, 那么就会被自动地赋为默认值: 数值为0、布尔值为false、对象引用为null。然而, 只有缺少程序设计经验的人才会这样做。确实, 如果不明确地对域进行初始化,就会影响程序代码的可读性。
5.3 无参数的构造器
很多类都包含一个无参数的构造函数, 对象由无参数构造函数创建时, 其状态会设置为适当的默认值。例如, 以下是Employee 类的无参数构造函数:
public Employee()
{
name = "";
salary = 0;
hireDay = LocalDate.now();
}
如果在编写一个类时没有编写构造器, 那么系统就会提供一个无参数构造器。这个构造器将所有的实例域设置为默认值。于是, 实例域中的数值型数据设置为0、布尔型数据设置为false、所有对象变量将设置为null。
如果类中提供了至少一个构造器, 但是没有提供无参数的构造器, 则在构造对象时如果 没有提供参数就会被视为不合法。
5.4 显式域初始化
通过重载类的构造器方法, 可以采用多种形式设置类的实例域的初始状态。确保不管怎样调用构造器, 每个实例域都可以被设置为一个有意义的初值,这是一种很好的设计习惯。
可以在类定义中, 直接将一个值赋给任何域。例如:
class Employee
{
private String name = "";
...
}
在执行构造器之前,先执行赋值操作。当一个类的所有构造器都希望把相同的值赋予某个特定的实例域时,这种方式特别有用。
初始值不一定是常量值。在下面的例子中, 可以调用方法对域进行初始化。仔细看一下 Employee 类,其中每个雇员有一个id 域。可以使用下列方式进行初始化:
class Employee
{
private static int nextld;
private int id = assignld();
private static int assignld()
{
int r = nextld;
nextld++;
return r;
}
...
}
5.5 参数名
在编写很小的构造器时(这是十分常见的),常常在参数命名上出现错误。通常, 参数用单个字符命名:
public Employee(String n, double s)
{
name = n;
salary = s;
}
但这样做有一个缺陷: 只有阅读代码才能够了解参数n 和参数s 的含义。
一种常用的技巧, 它基于这样的事实: 参数变量用同样的名字将实例域屏蔽起来。例如, 如果将参数命名为 salary, salary 将引用这个参数, 而不是实例域。但是,可以采用 this.salary 的形式访问实例域。this 指示隐式参数, 也就是所构造的对象。下面是一个示例:
public Employee(String name, double salary)
{
this.name = name;
this.salary = salary;
}
5.6 调用另一个构造器
关键字 this 引用方法的隐式参数。然而,这个关键字还有另外一个含义。
如果构造器的第一个语句形如this(…), 这个构造器将调用同一个类的另一个构造器。下面是一个典型的例子:
public Employee(double s)
{
// calls Employee(String, double)
this("Employee #" + nextld, s);
nextld++;
}
当调用 new Employee(60000) 时, Employee(double) 构造器将调用 Employee(String,double)构造器。
采用这种方式使用this 关键字非常有用, 这样对公共的构造器代码部分只编写一次即可。
5.7 初始化块
前面已经讲过两种初始化数据域的方法:
- 在构造器中设置值
- 在声明中赋值
实际上,Java 还有第三种机制, 称为初始化块(initializationblock)。在一个类的声明中,可以包含多个代码块。只要构造类的对象,这些块就会被执行。例如,
class Employee
{
private static int nextId;
private int id;
private String name;
private double salary;
// object initialization block
{
id = nextId;
nextId++;
}
public Employee(String n, double s)
{
name = n;
salary = s;
}
public Employee()
{
name = "";
salary = 0;
}
...
}
在这个示例中, 无论使用哪个构造器构造对象, id 域都在对象初始化块中被初始化。首先运行初始化块,然后才运行构造器的主体部分。
这种机制不是必需的,也不常见。通常会直接将初始化代码放在构造器中。
5.8 对象析构与 finalize 方法
有些面向对象的程序设计语言, 特别是 C++, 有显式的析构器方法, 其中放置一些当对象不再使用时需要执行的清理代码。在析构器中, 最常见的操作是回收分配给对象的存储空间。
由于Java 有自动的垃圾回收器,不需要人工回收内存, 所以Java 不支持析构器。
可以为任何一个类添加finalize 方法。finalize 方法将在垃圾回收器清除对象之前调用。
在实际应用中,不要依赖于使用finalize 方法回收任何短缺的资源, 这是因为很难知道这个方法什么时候才能够调用。
如果某个资源需要在使用完毕后立刻被关闭, 那么就需要由人工来管理。对象用完时,可以应用一个close 方法来完成相应的清理操作。
6 包
Java 允许使用包( package) 将类组织起来。借助于包可以方便地组织自己的代码,并将自己的代码与别人提供的代码库分开管理。
从编译器的角度来看, 嵌套的包之间没有任何关系。例如,java.utU 包与java.util.jar 包毫无关系。每一个都拥有独立的类集合。
标准的 Java 类库分布在多个包中, 包括 java.lang、java.util 和 java.net 等。标准的 Java包具有一个层次结构。如同硬盘的目录嵌套一样,也可以使用嵌套层次组织包。所有标准的 Java 包都处于 java 和 javax 包层次中。
6.1 类的导入
可以使用下面这条语句导人java.util 包中所有的类。
import java.util .*;
使用星号(*) 导入一个包, 而不能使用
import java.*
或
import java.*.*
导入以 java 为前缀的所有包。
还可以导人一个包中的特定类:
import java.time.Local Date;
在发生命名冲突的时候,java.util 和 java.sql 包都有日期( Date) 类。如果在程序中导入了这两个包:
import java.util.*;
import java.sql.*;
在程序使用Date 类的时候, 就会出现一个编译错误:
Date today; // Error java.util .Date or java.sql .Date?
此时编译器无法确定程序使用的是哪一个Date 类。可以采用增加一个特定的import 语句来解决这个问题:
import java.util .*;
import java.sql .*;
import java.util .Date;
如果这两个Date 类都需要使用, 又该怎么办呢? 答案是,在每个类名的前面加上完整的包名。
java.util .Date deadline = new java.util .Date();
java.sql .Date today = new java.sql .Date(...);
在包中定位类是编译器( compiler) 的工作
6.2 静态导入
import 语句不仅可以导人类,还增加了导人静态方法和静态域的功能。
例如,如果在源文件的顶部, 添加一条指令:
import static java.lang.System.*;
就可以使用System 类的静态方法和静态域,而不必加类名前缀:
out.println("Goodbye, World!"); // i.e., System.out
exit(9); //i.e., System.exit
6.3 将类放入包中
7 类的路径
8 文档注释
javadoc 可以由源文件生成一个HTML 文档。
- 类注释:类注释必须放在import 语句之后,类定义之前。例子:
/**
* A {©code Card} object represents a playing card , such
* as "Queen of Hearts". A card has a suit (Diamond, Heart ,
* Spade or Club) and a value (1 = Ace, 2 . . . 10, 11 = Jack,
* 12 = Queen , 13 = King)
*/
public class Card
{
...
}
- 方法注释:每一个方法注释必须放在所描述的方法之前。
- @param 变量描述
这个标记将对当前方法的“ param ” (参数)部分添加一个条目。这个描述可以占据多
行, 并可以使用HTML 标记。一个方法的所有@param 标记必须放在一起。- @return 描述
这个标记将对当前方法添加“ return” (返回)部分。这个描述可以跨越多行, 并可以
使用HTML 标记。- @throws 类描述
这个标记将添加一个注释, 用于表示这个方法有可能抛出异常。有关异常的详细内容
将在第10 章中讨论。
示例如下:
严
* 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 raiseSal ary(double byPercent)
{
double raise = salary * byPercent / 100;
salary += raise;
return raise;
}
- 域注释
只需要对公有域(通常指的是静态常量)建立文档。例如,
/**
* The "Hearts" card suit
*/
public static final int HEARTS = 1;
- 通用注释
- @author 姓名
作者条目。可以使用多个@author 标记, 每个@
author 标记对应一个作者- @version 文本
版本条目
下面的标记可以用于所有的文档注释中。
-@since 文本
这个标记将产生一个“ since” (始于)条目。这里的text 可以是对引人特性的版本描
述。例如,©since version 1.7.10- @deprecated
将对类、方法或变量添加一个不再使用的注释。文本中给出了取代的建议。例如,
@deprecated Use <code> setVIsible(true)</code> instead
- @see 引用 和 @link 引用
提供类、方法或变量的名字,javadoc 就在文档中插入一个超链接。例如,
@see com.horstraann.corejava.Employee#raiseSalary(double)
一定要使用井号(#),而不要使用句号(.)分隔类名与方法名, 或类名与变量名。@see 与 @link 标记规则一样
- 注释的抽取
9 小结:类的设计技巧
- 一定要保证数据私有
绝对不要破坏封装性。
- 一定要对数据初始化
- 不要在类中使用过多的基本类型
用其他的类代替多个相关的基本类型的使用。这样会使类更加易于理解且易于修改
- 不是所有的域都需要独立的域访问器和域更改器
- 将职责过多的类进行分解
- 类名和方法名要能够体现它们的职责
- 优先使用不可变的类