文章目录
第四章 对象与类
4.1 面向对象程序设计概述
面向对象程序设计(object-oriented programming,OOP)是当今主流的程序设计范型,它取代了20世纪70年到的结构化或过程式编程技术。
面向对象的程序是由对象组成的,每个对象包含对用户公开的特定功能部分和隐藏的实现部分。程序中的很多对象来自标准库,还有一些是自定义的。究竟是自己构造对象,还是从外界购买对象完全取决于开发项目的预算和时间。但是从根本上来说,只要对象能够满足要求,就不必关心其功能到底是如何实现的。
传统的结构化程序设计通过设计一系列的过程(即算法)来求解问题。一旦确定了这些过程,就要开始考虑存储数据的适当方式,这就是著名的算法+数据结构=程序的著名公式,首先要确定如何操作数据,然后再决定如何组织数据的结构,以便于操作数据。而OOP调换了这个次序,将数据放在第一位,再考虑操作数据的算法。
4.1.1 类
类(class)是构造对象的模板或蓝图。我们可以将类想象成制作小甜饼的模具,将小甜饼想象成对象。由类构造(construct)对象的过程称为创建类的实例(instance)。
封装(encapsulation,有时称为数据隐藏)是处理对象的一个重要概念。从形式上看,封装就是将数据和行为组合在一个包中,并对对象的使用者隐藏具体的实现方式。对象中的数据称为实例字段(instance field),操作数据的过程称为方法(method)。作为一个类的实例,特定对象都有一组特定的实例字段值。这些值的集合就是这个对象的当前状态(state)。无论何时,只要在对象上调用一个方法,它的状态就有可能发生改变。
**实现封装的关键在于,绝对不能让类中的方法直接访问其他类的实例字段。程序只能通过对象的方法与对象数据进行交互。**封装给对象赋予了“黑盒”特征,这是提高重用性和可靠性的关键。这意味着一个类可以完全改变存储数据的方式,只要依旧使用同样的方法操作数据,其他对象就不会知道也不用关心这个类所发生的变化。
OOP的另一个原则会让用户自定义Java类变得更为容易:可以通过扩展其他类来构建新类。事实上,在Java中。所有类都源自一个神通广大的超类Object。所有其他的类都扩展自这个Object类。
在扩展一个已有的类时,这个扩展后的新类具有被扩展的类的全部属性和方法。只需要在新类中提供适用于这个新类的新方法和数据字段就可以了。通过扩展一个类来建立另外一个类的过程称为继承(inheritance)。
4.1.2 对象
要想适用OOP,一定要清楚对象的三个主要特性:
- 对象的行为(behavior)——可以对对象完成哪些操作,或者可以对对象应用哪些方法。
- 对象的状态(state)——当调用那些方法时,对象会如何响应。
- 对象的标识(identity)——如何区分具有相同行为与状态的不同对象。
同一个类的所有对象实例,由于支持相同的行为而具有家族式的相似性。对象的行为是用可调用的方法来定义的。
此外,每个对象都保存着描述当前状况的信息,这就是对象的状态。对象的状态可能会随着时间而发生改变,但这种改变不会是自发的。对象状态的改变必须通过调用方法实现(如果不经过方法调用就可以改变对象状态,只能说明破坏了封装性)。
但是对象的状态并不能完全描述一个对象。每个对象都有一个唯一的标识(identity,或称身份)。作为同一个类的实例,每个对象的标识符总是不同的,状态也往往存在着差异。
对象的这些关键特性会彼此相互影响。例如,对象的状态影响它的行为(如果一个订单"已送货"或"已付款",就应该拒绝调用要求增删订单中商品的方法。反过来,如果订单是空的,即还没有预订任何商品,这个订单就不应该"送货")。
4.1.3 识别类
面向对象程序设计没有所谓的"顶部",在面向对象程序设计时,首先从识别类开始,然后再为各个类添加方法。
识别类的一个简单经验是在分析问题的过程中寻找名词,而方法对应着动词。
例如:在订单处理系统中,有这样一些名词:
- 商品(Item)
- 订单(Order)
- 送货地址(Shipping address)
- 付款(Payment)
- 账户(Account)
从这些名词就可以得到类Item,Order等。
接下来查看动词,商品被添加到订单中,订单会发货或取消,另外可以对订单完成付款。对于每一个动词,如"添加"、“发货”、“取消”、“完成付款”,都要识别出负责完成响应动作的对象。例如,当一个新的商品添加到订单中时,那个订单对象就是负责的对象,因为它知道如何存储商品以及如何对商品进行排序。也就是说,add应该是Order类的一个方法,它要取Item对象作为参数。
4.1.4 类之间的关系
在类之间,最常见的关系有:
-
依赖(“uses-a”):
依赖(dependence),即"uses-a"关系,是一种最明显的、最常见的关系。一个类的方法使用或操纵另一个类的对象,我们就说一个类依赖于另一个类。应该尽可能地将相互依赖的类减至最少。这里的关键是:如果类A不知道B的存在,它就不会关心B的任何改变(这意味着B的改变不会导致A产生任何Bug)。用软件工程的话来说,就是尽可能减少类之间的耦合。
-
聚合(“has-a”):
聚合(aggregation),即"has-a"关系,一个Order对象包含一些Item对象,包容关系意味着类A的对象包含类B的对象。 -
继承(“is-a”)
继承(inheritance),即"is-a"关系,表示一个更特殊的类与一个更一般的类之间的关系。一般而言,如果类A扩展类B,类A不但包含从类B继承的方法,还会有一些额外的功能。
表示类关系的UML符号:
4.2 使用预定义类
4.2.1 对象与对象变量
要想使用对象,首先必须构造对象,并指定其初始状态。然后对对象应用方法。
在Java中,要使用构造器(constructor,或称构造函数)构造新实例。构造器是一种特殊的方法,用来构造并初始化对象。
构造器的名字应该与类名相同,要想构造一个Date对象,需要在构造器前面加上new操作符:
new Date()
这个表达式构造了一个新对象,这个对象被初始化为当前的日期和时间。
也可以将这个对象传递给一个方法:
System.out.println(new Date());
或者,也可以对刚刚创建的对象应用一个方法。Date类中有一个toString()方法。这个方法将返回日期的字符串描述:
String s = new Date().toString();
在这两个例子中,构造的对象仅用了一次。通常会希望构造的对象多次使用,因此,需要将对象存放在一个变量中:
Date birthday = new Date();
在对象与对象变量之间存在着一个重要的区别:
Date deadline; // deadline doesn't refer to any object
该语句定义了一个对象变量deadline,它可以引用Date类型的对象。但是,一定要认识到deadline并不是一个对象,而且实际上它也没有引用任何对象。此时还不能在这个变量上使用任何Date方法。
必须首先初始化变量deadline,这里有两个选择:
// 1.引用新构造的对象
deadline = new Date();
// 2.引用一个已有的对象
deadline = birthday;
现在,这两个变量都引用同一个对象:
对象变量并没有实际包含一个对象,它只是引用一个对象。
在Java中,任何对象变量的值都是对存储在另外一个地方的某个对象的引用。new操作符的返回值也是一个引用:
Date deadline = new Date();
表达式new Date()构造了一个Date类型的对象,它的值是对新创建对象的一个引用。这个引用存储在变量deadline中。
可以显式地将对象变量设置为null,指示这个对象变量目前没有引用任何对象:
deadline = null;
...
if(deadline != null){
System.out.println(deadline);
}
Java中的对象变量可以类似看作于C++的对象指针:
Date birthday; // Java
等同于
Date *birthday; // C++
所有的Java对象都存储在堆中。当一个对象包含另一个对象变量时,它只是包含着另一个堆对象的指针。、
4.2.3 更改器方法和访问器方法
更改器方法(mutator method):调用后改变对象状态的方法。
public void setXXX(int a){
this.a = a;
}
访问器方法(accessor method):只访问对象而不修改对象的方法。
public int getXXX(){
return a;
}
4.3 用户自定义类
4.3.4 构造器
构造器与类同名。在构造Employee类的对象时,构造器会运行,以便将实例域初始化为所希望的状态。
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;
LocalDate hireDay = LocalDate.of(year,month,day);
}
}
构造器总是伴随着new操作符的执行被调用,而不能对一个已经存在的对象调用构造器来达到重新设置实例域的目的。
james.Employee("James Bond",25000,1950,1,1) // ERROR
将会产生编译错误。
- 构造器与类同名
- 每个类可以有一个以上的构造器
- 构造器可以有0个、1个或多个参数
- 构造器没有返回值
- 构造器总是伴随着new操作一起调用
注:不要在构造器中定义与实例域重名的局部变量,例如,下面的构造器将无法设置salary
public Employee(String n,double s,...){
String name = n; // Error
double salary = s; // Error
...
}
这个构造器声明了局部变量name和salary。但这些变量只能在构造器内部访问,因为这些变量屏蔽了同名的实例域。
4.3.5 隐式参数和显式参数
方法用于操作对象以及存取它们的实例域,例如:
public void raiseSalary(double byPercent){
double raise = salary * byPercemt / 100;
salary += raise;
}
raiseSalary方法有两个参数,第一个参数称为隐式(implicit)参数,是出现在方法名前的Employee对象。第二个参数位于方法名后面括号中的数值,这是一个显式(explicit)参数。(隐式参数也可称为方法调用的目标或接收者)。
显示参数是明显地列在方法声明中的,而隐式参数没有出现在方法声明中。
在每一个方法中,this表示隐式参数:
public void raiseSalary(double byPercent){
double raise = this.salary * byPercent / 100;
this.salary += raise;
}
4.3.6 封装的优点
在有些时候,需要获得或设置实例域的值,因此,应该提供以下内容:
- 一个私有的数据域。
- 一个公有的域访问器方法
- 一个公有的域更改器方法
这样做要比提供一个简单的公有数据域复杂些,但是却有着下列明显的好处:
- 可以改变内部实现,除了该类的方法之外,不会影响其他代码。
- 更改器方法可以执行错误检查,然而直接对域进行赋值将不会进行这些处理。
不要编写返回引用可变对象的访问器方法,这样会破坏封装性。
如果需要返回一个可变对象的引用,应该首先对它进行克隆(clone)。对象克隆是指存放在另一个位置上的对象副本。
4.3.7 基于类的访问权限
一个方法可访问所属类的所有对象的私有数据。
4.3.8 私有方法
在Java中,为了实现一个私有的方法,只需要将关键字public改为private即可。
对于私有方法,如果改用其他的方法实现相应的操作,则不必保留原有的方法。如果数据的表达方式发生变化,这个方法可能会变得难以实现,或者不再需要。只要方法是私有的,就不会被外部的其他类操作调用,可以将其删去。如果方法是共有的,就不能将其删去,因为其他的代码很可能依赖它。
4.3.9 final实例域
可以将实例域定义为final。构建对象时必须初始化这样的域。即必须确保在每一个构造器执行之后,这个域的值被设置,并且在后面的操作中,不能够再对它进行修改。
final修饰符大都应用于基本(primitive)类型域,或不可变(immutable)类的域(如果类中的每个方法都不会改变其对象,这种类就是不可变的类。例如,String类就是一个不可变的类)。
4.4 静态域和方法
4.4.1 静态域
如果将域定义为static,每个类中只有一个这样的域。而每一个对象对于所有的实例域都有自己的一份拷贝。
4.4.2 静态常量
public static final String name = "123";
private static final String name = "123";
4.4.3 静态方法
静态方法是一种不能向对象实施操作的方法。例如,Math类的pow方法就是一个静态方法:
Math.pow(x,a);
在运算时,不使用任何的Math对象,换句话说,没有隐式的参数。
Employee类的静态方法不能访问Id实例域,因为它不能操作对象。但是,静态方法可以访问自身类中的静态域:
public static int getNextId(){
return nextId; // returns static field
}
通过类名调用这个方法:
int n = Employee.getNextId();
在下面两种情况下使用静态方法:
- 一个方法不需要访问对象状态,其所需参数都是通过显式参数提供。
- 一个方法只需要访问类的静态域。
建议通过类名来调用静态方法,而不是对象。
4.4.4 工厂方法
静态方法还有另外一种常见的用途。类似LocalDate和NumberFormat的类使用静态工厂方法(factor method)来构造对象。
NumberFormat类使用工厂方法生成不同风格的格式化对象:
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类不利用构造器完成这些操作呢?
- 无法命名构造器。构造器的名字必须与类名相同,但是,这里希望将得到的货币实例和百分比实例采用不同的名字。
- 当使用构造器时,无法改变所构造的对象类型。而Factory方法将返回一个DecimalFormat类对象,这是NumberFormat的子类。
4.4.5 main方法
main方法是一个静态方法:
public class Application{
public static void main(String[] args){
// construct objects here
...
}
}
main方法不对任何对象进行操作。事实上,在启动程序时还没有任何一个对象。静态的main方法将执行并创建程序所需要的对象。
每一个类可以有一个main方法,这是一个常用于对类进行单元测试的技巧。
4.5 方法参数
按值调用(call by value):方法接收的是调用者提供的值。
按引用调用(call by reference):方法接受的是调用者提供的变量地址。
Java总是采用按值调用,也就是说,方法得到的是所有参数值的一个拷贝,特别是,方法不能修改传递给它的任何参数变量的内容。
double percent = 10;
harry.raiseSalary(percent);
方法调用后,percent的值还是10。
假定一个方法试图将一个参数的值增加3倍:
public static void tripleValue(double x){
x = 3 * x;
}
调用这个方法:
double percent = 10;
tripleValue(percent);
不过,调用这个方法之后,percent的值还是10。执行过程:
- x被初始化为percent值得一个拷贝。
- x被乘以3后等于30。但是percent仍然为10。
- 这个方法结束后,参数变量x不再被使用。
方法参数共有两种类型:
- 基本数据类型(数字、布尔值)。
- 对象引用
一个方法不可能修改一个基本数据类型的参数。而对象引用作为参数就不同了:
public static void tripleSalary(Employee x){
x.raiseSalary(200);
}
当调用:
harry = new Employee(...);
tripleSalary(harry);
具体的执行过程:
- x被初始化为harry值的拷贝,这里是一个对象的引用。
- raiseSalary方法应用于这个对象引用。x和harry同时引起的那个Employee对象的薪资提高了200%
- 方法结束后,参数变量x不再使用。当然,对象变量harry继续引用那个薪金增至三倍的雇员对象。
方法得到的是对象引用的拷贝,对象引用及其他的拷贝同时引用同一个对象。
Java对对象采用的不是引用调用,实际上,对象引用是按值传递的。
Java中方法参数的使用情况:
- 一个方法不能修改一个基本数据类型的参数。
- 一个方法可以改变一个对象参数的状态。
- 一个方法不能让对象参数引用一个新的对象。
4.6 对象构造
4.6.1 重载
如果多个方法有相同的名字,不同的参数,便产生了重载。编译器必须挑选出具体执行哪个方法,通过用各个方法给出的参数类型与特定方法调用所使用的值类型进行匹配来挑选出相应的方法。如果编译器找不到匹配的参数,就会产生编译时错误,因为根本不存在匹配,或者没有一个比其他的更好。(这个过程被称为重载解析(overloading resolution)。)
Java允许重载任何方法。要完整地描述一个方法,需要指出方法名以及参数类型。这叫做方法的签名。例如String类有4个被称为indexOf的公有方法:
indexOf(int);
indexOf(int ,int);
indexOf(String);
indexOf(String,int);
返回类型并不是方法签名的一部分。也就是说,不能有两个名字相同、参数类型也相同却返回不同类型值的方法。
4.6.2 默认域初始化
如果**在构造器中没有显式地给域赋予初值,那么就会被自动地赋为默认值:数值为0、布尔值为false、对象引用为null。**然而只有缺少程序设计经验的人才会这么做,如果不明确地对域进行初始化,就会影响程序代码的可读性。
**这是域与局部变量的主要不同点。必须明确地初始化方法中的局部变量。**但是,如果没有初始化类中的域,将会被自动初始化为默认值(0,false或null)。
如果不对域进行初始化,在使用访问器方法时就会得到一个null引用,这不是我们所希望的。
4.6.3 无参数的构造器
对象由无参数构造函数创建时,其状态会设置为适当的默认值:
public Employee(){
name = "";
salary = 0;
hireDay = LocalDate.now();
}
如果在编写一个类时没有编写构造器,那么系统就会提供一个无参数构造器。这个构造器将所有的实例域设置为默认值。
如果类中提供了至少一个构造器,但是没有提供无参数的构造器,则在构造对象时如果没有提供参数就会被视为不合法。
4.6.4 显式域初始化
通过重载类的构造器方法,可以采用多种形式设置类的实例域的初始状态。确保不管怎样调用构造器,每个实例域都可以被设置为一个有意义的初值,这是一种很好的设计习惯。
可以在类定义中,直接将一个值赋给任何域:
class Employee{
private String name = "";
...
}
在执行构造器之前,先执行赋值操作。当一个类的所有构造器都希望把相同的值赋予某个特定的实例域时,这种方式特别有用。
初始值不一定是常量值,也可以调用方法对域进行初始化。
4.6.5 参数名
在编写很小的构造器时,常常在参数命名中出现错误。
通常,参数用单个字符命名:
public Employee(String n,double s){
name = n;
salary = s;
}
但这样命名有一个缺陷,就是只有阅读代码才能够了解参数n和参数s的含义。
有些程序员在每个参数前面加上一个前缀"a":
public Employee(String aName,double sSalary){
name = aName;
salary = aSalary;
}
还有一种常用的技巧:参数变量用同样的名字将实例域屏蔽起来。例如,如果将参数命名为salary,salary将引用这个参数,而不是实例域。但是,可以采用this.salary的形式访问实例域:
public Employee(String name,double salary){
this.name = name;
this.salary = salary;
}
4.6.6 调用另一个构造器
关键字this引用方法的隐式参数。然而这个关键字还有另外一个含义。
如果构造器的第一个语句形如this(…),这个构造器将调用同一个类的另一个构造器。
public Employee(double s){
// calls Employee(String,double)
this("Employee #" + nextId,s);
nextId++;
}
采用这种方式使用this关键字非常有用,这样对公共的构造器代码部分只编写一次即可。
4.6.7 初始化块
在一个类的声明中,可以包含多个代码块。只要构造类的对象,这些块就会被执行:
import java.util.*;
public class ConstructorTest{
public static void main(String[] args){
// fill the staff array with three Employee objects
Employee[] staff = new Employee[3];
staff[0] = new Employee("Harry",40000);
staff[1] = new Employee(60000);
staff[2] = new Employee();
// prints out information about all Employee objects
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 initialization block
static
{
Random generator = new Random();
// set nextId to a random number between 0 and 9999
nextId = generator.nextInt(10000);
}
// object initialization block
{
id = nextId;
nextId++;
}
// three overloaded constructors
public Employee(String n,double s){
name = n;
salary = s;
}
public Employee(double s){
// calls the Employee(String,double) constructor
this("Employee #" + nextId,s);
}
// the default constructor
public Employee(){
// name initialized to "" --see above
// salary not explocitly set--initialized to 0
// id initialized in initialization block
}
public String getName(){
return name;
}
public double getSalary(){
return salary;
}
public int getId(){
return id;
}
}
为了避免循环定义,不要读取在后面初始化的域,建议将初始化块放在域定义之后。
调用构造器的具体处理步骤:
- 所有数据域被初始化为默认值(0,false或null)。
- 按照在类声明中出现的次序。依次执行所有域初始化语句和初始化块。
- 如果构造器第一行调用了第二个构造器,则执行第二个构造器主体。
- 执行这个构造器的主体。
可以通过提供一个初始化值,或者使用一个静态的初始化块来对静态域进行初始化。
将代码放在一个块中,并标记关键字static。在类第一次加载的时候,将会进行静态域的初始化。与实例域一样,除非将它们显式地设置成其他值,否则默认的初始值是0、false或null。所有地静态初始化语句以及静态初始化块都将按照类定义地顺序执行。
4.6.8 对象析构与finalize方法
有些面向对象的程序设计语言,特别是C++,有显式的析构器方法,其中放置一些当对象不再使用时需要执行的清理代码。在析构器中,最常见的操作是回收分配给对象的存储空间。由于Java有自动的垃圾回收器,不需要人工回收内存,所以Java不支持析构器。
某些对象使用了内存之外的其他资源,如文件或使用了系统资源的另一个对象的句柄。在这种情况下,当资源不再需要时,将其回收和再利用将显得十分重要。
可以为任何一个类添加finalize方法。finalize方法将在垃圾回收器清除对象之前调用。在实际应用中,不要依赖于使用finalize方法回收任何短缺的资源,这是因为很难知道这个方法什么时候才能够调用。
如果某个资源需要在使用完毕后立刻被关闭,那么就需要由人工来管理。对象用完时,可以应用一个close方法来完成相应的清理操作。(7.2.5)。
有个System.runFinalizersOnExit(true)的方法能够确保finalizer方法在Java关闭前被调用。不过,这个方法并不安全,也不鼓励使用。有一种替代的方法Runtime.addShutdownHook添加"关闭钩"(shutdown hook)。
4.7 包
Java允许使用包(package)将类组织起来。借助于包可以方便地组织自己的代码,并将自己的代码和别人提供的代码库分开管理。
标准的Java类库分布在多个包中,包括java.lang、java.util、java.net等。标准的Java包具有一个层次结构。如同硬盘的目录嵌套一样,也可以使用嵌套层次组织包。所有标准的Java包都处于Java和javax包层次中。
**使用包的主要原因是确保类名的唯一性。假如两个程序员不约而同地建立了Employee类。只要将这些类放置在不同的包中,就不会产生冲突。**事实上,为了保证包名地绝对唯一性,Sun公司建议将公司地因特网域名以逆序地形式作为包名,并且对于不同地项目使用不同的子包。
4.9 文档注释
JDK包含一个很有用的工具,叫做javadoc,它可以由源文件生成一个HTML文档。事实上,联机API文档就是通过对标准的Java类库的源代码运行javadoc生成的。
如果在源代码中添加以专用的定界符/**开始的注释,那么可以很容易地生成一个看上去具有专业水准的文档。这种方式可以将代码与注释保存在一个地方。如果将文档存入一个独立的文件中,就有可能会随着时间的推移,出现代码和注释不一致的问题。然而,由于文档注释与源代码在同一个文件中,在修改源代码的同时,重新运行javadoc就可以轻而易举地保持两者的一致性。
4.9.1 注释的插入
javadoc实用程序(utility)从下面几个特性中抽取信息:
- 包
- 公有类与接口
- 公有的和受保护的构造器及方法
- 公有的和受保护的域
应该为上面几部分编写注释。注释应该放置在所描述特性的前面。
每个文档注释在标记之后紧跟着自由格式文本(free-form text)。标记由@开始,如@author或@parm。
自由格式文本的第一句应该是一个概要性的句子。javadoc实用程序自动地将这些句子抽取出来形成概要页。
在自由格式文本中,可以使用HTML修饰符。若要键入等宽代码,需使用{@code}而不是,这样一来就不用操心对代码中的<字符转义了。
4.9.2 类注释
类注释必须放在import语句之后,类定义之前。
4.9.3 方法注释
每一个方法注释必须放在所描述的方法之前。除了通用标记之外,还可以使用下面的标记:
- @param变量描述
这个标记将对当前方法的"param"部分添加一个条目。这个描述可以占据多行,并可以使用HTML标记。一个方法的所有@param标记必须放在一起。 - @return描述
这个标记将对当前方法添加"return"部分。这个描述可以跨越多行,并可以使用HTML标记。 - @throws类描述
这个标记将添加一个注释,用于表示这个方法有可能抛出异常。
4.9.4 域注释
只需要对公有域(通常指的是静态常量)建立文档。
/**
* The "Hearts" card suit
*/
public static final int HEARTS = 1;
4.9.5 通用注释
-
@author姓名
这个标记将产生一个"author"条目。可以使用多个@author标记,每个@author标记对应一个作者 -
@version文本
这个标记将产生一个"version"条目。这里的文本可以是对当前版本的任何描述。下面的标记可以用于所有的文档注释中。
-
@since文本
这个标记将产生一个"since"条目。这里的text可以是对引入特性的版本描述。例如,@since version 1.7.1。 -
@deprecated文本
这个标记将对类、方法或变量添加一个不再使用的注释。文本中给出了取代的建议:@deprecated Use <code> setVisible(true) </code> instead
通过@see和@link标记,可以使用超级链接,链接到javadoc文档的相关部分或外部文档。
-
@see引用
这个标记将在"see also"部分增加一个超级链接。它可以用于类中,也可以用于方法中。这里的引用可以选择下列情形之一:package.class#feature label <a href="...">label</a> "text"
第一种情况是最常见的,只要提供类、方法或变量的名字,javadoc就在文档中插入一个超链接:
@see com.horstmann.corejava.Employee#raiseSalary(double)
建立一个链接到com.horstmann.corejava.Employee类的raiseSalary(double)方法的超链接。可以省略包名,甚至把包名和类名都省去,此时,链接将定位于当前包或当前类。
需要注意一定要使用#,而不要使用句号(.)分隔类名与方法名,或类名与变量名。Java编译器本身可以熟练地断定句点在分隔包、子包、类、内部类与方法和变量时的不同含义。但是javadoc实用程序就没有像java编译器那样聪明了。
如果@see标记后面有一个<字符,就需要指定一个超链接。可以超链接到任何URL:
@see <a href="www.horstmann.com/corejava.html">The Core Java home page</a>
在上述各种情况下,都可以指定一个可选的标签(label)作为链接锚(link anchor)。如果省略了label,用户看到的锚的名称就是目标代码名或URL。
如果@see标记后面有一个双引号("")字符,文本就会显示在"see also"部分:
@see "Core Java 2 volume 2"
可以为一个特性添加多个@see标记,但必须将它们放在一起。
-
可以在注释中的任何位置指向其他类或方法的超级链接,以及插入一个专用的标记:
{@link package.class#feature label}
这里的特性描述规则与@see 标记规则一致。
4.9.6 包与概述注释
可以直接将类、方法和变量的注释放置在Java源文件中。只要用文档注释界定即可。但是想要产生包注释,就需要在每一个包目录中添加一个单独的文件。可以提供如下两种选择:
- 提供一个以package.html命名的HTML文件。在标记之间的所有文本都会被抽取出来。
- 提供一个以package-info.java命名的Java文件。这个文件必须包含一个初始的以/** 和 */界定的Javadoc注释,跟随在一个包语句之后。它不应该包含更多的代码或注释。
还可以为所有的源文件提供一个概述性的注释。这个注释将被放置在一个名为overview.html的文件中,这个文件位于包含所有源文件的父目录中。标记之间的所有文本将被抽取出来,当用户从导航栏中选择"Overview"时,就会显示出这些注释内容。
4.9.7 注释的抽取
这里假设HTML文件将被存放在目录docDirectory下,执行以下步骤:
-
切换到包含想要生成文档的源文件目录。如果有嵌套的包要生成文档,例如com.horstmann.corejava。就必须切换到包含子目录com的目录(如果存在overview.html文件的话,这也是它的所在目录)。
-
如果是一个包,应该运行命令:
javadoc -d docDirectory nameOfPackage
或对于多个包生成文档,运行:
javadoc -d docDirectory nameOfPackage nameOfPackage...
如果文件在默认包中,就应该运行:
javadoc -d docDirectory *.java
如果省略了-d docDirectory选项,那么HTML文件就会被提取到当前目录下。这样有可能会带来混乱,因此不提倡这种方式。
可以使用多种形式的命令行选项对javadoc程序进行调整。例如,可以使用-author和-version选项在文档中包含@author和@version标记(默认情况下,这些标记会被省略)。另一个很有用的选项是-link,用来为标准类添加超链接:
javadoc -link http://docs.oracle.com/javase/8/docs/api *.java
那么,所有的标准类库类都会自动地链接到Oracle网站地文档。
如果使用-linksource选项,则每个源文件被转换成HTML(不对代码着色,但包含行编号),并且每个类和方法名将转变为指向源代码的超链接。
4.10 类设计技巧
- 一定要保证数据私有
这是最重要的:绝对不要破坏封装性,有时候需要编写一个访问器方法或更改器方法,但是最好还是保持实例域的私有性。数据的表示形式很可能会改变,但它们的使用方式却不会经常发生变化。当数据保持私有时,它们的表示形式的变化不会对类的使用者产生影响,即使出现bug也易于检测。 - 一定要对数据初始化
Java不对局部变量进行初始化,但是会对对象的实例域进行初始化。最好不要依赖于系统的默认值,而是应该显式地初始化所有的数据,具体的初始化方式可以是提供默认值,也可以是在所有的构造器中设置默认值。 - 不要在类中使用过多的基本类型
用其他类代替多个相关的基本类型的使用。这样会使类更加易于理解且易于修改。 - 不是所有的域都需要独立的域访问器和域更改器
- 将职责过多的类进行分解
- 类名和方法名要能够体现它们的职责
- 优先使用不可变的类
LocalDate类以及java.time包中的其他类是不可变的——没有方法能修改对象的状态。类似plusDays的方法并不是更改对象,而是返回状态已修改的新对象。
更改对象的问题在于,如果多个线程试图同时更新一个对象,就会发生并发更改。其结果是不可预测的,如果类是不可变的,就可以安全地在多个线程间共享其对象。