对象与类

面向对象程序设计概述

面向对象的程序是由对象组成的,每个对象包含对用户公开的特定功能部分和隐藏的实现部分。程序中的很多对象来自标准库,还有一些是自定义的。从根本上说,只要对象能够满足要求,就不必关心其功能到底是如何实现的。

一般而言我们需要首先确定如何操作数据,然后再决定如何组织数据的结构,以便操作数据。而 OOP 却调换了这个次序,将数据放在第一位,然后再考虑操作数据的算法。

类是构造对象的模板或蓝图。由类构造 construct 对象的过程称为创建类的实例 instance

封装 encapsulation 是处理对象的一个重要概念。对象中的数据称为实例字段 instance field ,操作数据的过程称为方法 method 。每个对象都有一组特定的实例字段值。这些值的集合就是这个对象的当前状态 state 。无论何时,只要在对象上调用一个方法,它的状态就可能发生改变。

实现封装的关键在于,绝对不能让类中的方法直接访问其他类的实例字段。程序只能通过对象的方法与对象数据进行交互。

OOP 的另一个原则会让用户自定义 Java 类变得更为容易,可以通过扩展其他类来构建新类。事实上,在 Java 中,所有的类都源自于一个“神通广大“的超类,它就是 Object 。所有的其他类都扩展自这个 Object 类。

在扩展一个已有类时,这个扩展后的新类具有被扩展的类的全部属性和方法,你只需要在新类中提供适用于这个新类的新方法和数据字段就行了。通过扩展一个类来建立另外一个类的过程称为继承 inheritance

对象

想要使用 OOP,一定要清楚对象的三个主要特性:

  • 对象的行为behavior:可以对对象完成哪些操作,或者可以对对象应用那些方法?
  • 对象的状态state:当调用那些方法时,对象会如何响应?
  • 对象的表示identity:如何区分具有相同行为与状态的不同对象?

对象的状态并不能完全描述一个对象。每个对象都有一个唯一的标识 identity 。作为同一个类的实例,每个对象的标识总是不同的,状态也存在差异。

识别类

我们首先需要从识别类开始,然后再为各个类添加方法。

识别类的一个简单经验是在分析问题的过程中寻找名词,而方法对应动词

类之间的关系

在类之间,最常见的关系有

  • 依赖uses-a
  • 聚合has-a
  • 继承is-a

依赖 dependence ,是一种最明显、最常见的关系。因此如果一个类的方法使用或操纵另一个类的对象,我们就说一个类依赖于另一个类。应该尽可能地将互相依赖的类减至最少。

聚合 aggregation ,比如一个 Order 对象包含一些 Item 对象。

继承 inheritance ,表示一个更特殊的类与一个更一般的类之间的关系

使用预定义类

对象与对象变量

要想使用变量,首先必须构造对象,并指定其初始状态。然后对对象应用初始方法。

在 Java 中,通过使用构造器 constructor 构造新实例。构造器的名字应该与类名相同。比如想要构造一个 Date 对象,需要在构造器前加上 new 操作符

new Date()

这个表达式构造了一个新对象。这个对象被初始化为当前的日期和时间

可以将对象放在一个变量中:

Date birthday = new Date()

另外,在对象和对象变量之间存在一个重要的区别

Date deadline 定义了一个对象变量 deadline ,它可以引用 Date 类型的对象,但是它没有引用任何对象。

在 Java 中,任何对象变量的值都是对存储在另一个地方的某个对象的引用。 new 操作符的返回值也是一个引用。可以显示地将对象设置为 null ,指示这个变量没有引用任何对象

Java 类库中的 LocaoDate 类

标准 Java 类库包含了两个类:一个用来表示时间点的 Date 类,另一个是使用大家熟悉的日历表示法表示日期的 LocalDate 类。

不要使用构造器来构造 LocalDate 类的对象,应该使用静态工厂方法,它会代表你调用构造器。

LocalDate.now()

更改器方法与访问器方法

调用更改器方法时,调用它的对象会发生改变

只访问对象而不修改对象的方法就是访问器方法

最好将实例字段标记为 private

多个源文件的使用

如果将每一个类存放在一个单独的源文件中,则可以有两种编译源程序的方法,一是使用通配符调用 Java 编译器

javac Employee*.java ,这样一来,所有与通配符匹配的源文件都将被编译成类文件,二是使用以下方式

javac EmployeeTest.java ,当 Java 编译器发现这个类使用了其他类时,自动查找那个类的 .class 文件,如果没有找到,就会自动搜索那个类文件。

从构造器开始

现在只需记住构造器

  • 构造器与类同名
  • 每个类可以有一个以上的构造器
  • 构造器可以有 0,1 或多个参数
  • 构造器没有返回值
  • 构造器总是伴随着new操作符一起调用

不要在构造器定义与实例字段同名的局部变量,比如

> public Employee(String n, double s, ...) {
>   String name = n; // ERROR
>   double salary = s; // ERROR
> }
> ```

>
> 这个构造器声明了局部变量 name 和 salary,这些变量会遮蔽同名的实例字段

*var 声明局部变量*

如果可以从变量的初始值推导出它们的类型,那么可以使用 `var` 声明局部变量,而无需指定类型。比如

 `var harry = new Employee("Harry Hacker", 5000,  1989, 10, 1);`

注意 `var` 只能用于方法中的局部变量。参数和字段的类型必须声明

*使用 null 引用*

一个对象变量包含一个对象的引用,或包含一个特殊值 `null` ,后者没有引用任何对象。如果对 `null` 值应用一个方法,会产生一个 `NullPointerException` 异常

```java
LocalDate birthday = null;
String s = birthday.toString(); // NullPointerException

定义一个类时,最好清楚地知道哪些字段可能为 null 。有些字段可能因为传入的实参为 null 而变为 null

对此有两种解决方法。”宽容型"方法是吧 null 参数转换为一个适当的非 null 值:

if (n == null) name = "unknown"; else name = n

在 Java 9 中,Objects 类对此提供了一个便利方法:

public Employee(String n, double s, int year, int month, int day) {
  name = Objects.requireNonNullElse(n, "unknow");
  ...
}

“严格型”方法是直接拒绝 null 参数:

public Employee(String n, double s, int year, int month, int day) {
  Objects.requireNonNull(n, "The name cannot be null");
  name = n;
  ...
}

如果有人用一个 null 名字构造了一个 Employee 对象,就会产生 NullPointerException 异常。这个方法有两个好处

  1. 异常报告会提供这个问题的描述
  2. 异常报告会准确地指出问题所在位置,否则这个异常可能在其他地方出现,而很难追踪到真正导致问题的这个构造器参数

隐式参数和显式参数

方法用于操作对象以及存取它们的实例字段,比如:

public void raiseSalary(double byPercent) {
	double raise = salary * byPercent / 100;
}

调用这个方法

number007.raiseSalary(5)

raiseSalary 有两个参数,第一个参数称为隐式参数,是出现在方法名前的 Employee 类型的对象,第二个参数是位于方法名后面括号中的数值,这是一个显式参数。(有人把隐式参数曾为方法调用的目标或接收者)

每一个方法中,关键字 this 指示隐式参数。可以这样改写

// 使用 this 可以将实例字段和局部变量明显区分
public void raiseSalary(double byPercent) {
	double raise = this.salary * byPercent / 100;
}

在 C++ 中,通常在类的外面定义方法

封装的优点

有一些方法

public String getName() {
  retunr name;
}

它们只是简单地返回实例字段值,又称为字段访问器。如果只是把 name 字段简单设为 public ,则会导致破坏这个字段值的捣乱者可能出现在任何地方。所以有些时候,我们想要获得或设置实例字段的值,可以提供下面三项内容:

  • 私有的数据字段;
  • 公共的字段访问器方法;
  • 一个公共的字段更改器方法;

注意,不要编写返回可变对象引用的访问器方法。如果需要返回一个可变对象的引用,首先要对它进行克隆。

基于类的访问权限

一个方法可以访问所属类的所有对象的私有数据

class Employee {
  ...
  public boolean equals(Employee other) {
    return name.equals(other.name);
  }
}

私有方法

有时,我们希望将一个计算代码分成若干个独立的辅助方法。通常,这些方法不应该成为公共接口的一部分。

在 Java 中,实现私有方法,只需要将关键字 public 改为 private 即可

final 实例字段

可以将实例字段定义为 final 。这样的字段必须在构造对象时初始化。也就是说,必须确保在每个构造器执行之后,这个字段的值已经设置,并且以后不能再修改这个字段。比如

class Employee {
	private final String name;
}

final 修饰符对于类型为基本类型或不可变类字段尤其有用。对于可变的类,使用 final 修饰符可能会造成混乱。比如

private final StringBuilder evaluations

在构造器中初始化为

evaluations = new StringBuilder();

不过这个对象可以更改

public void giveGoldStar() {
	evaluations.append(LocalDate.now() + ": Gold star!\n");
}

静态字段与静态方法

静态字段

如果将一个字段定义为 static ,每个类只有一个这样的字段。而对于非静态的实例字段,每个对象都有自己的一个副本。

静态常量

静态变量用的少,但是静态常量却很常用。比如,在 Math 类中定义一个静态常量:

public class Math {
	...
  public static final double PI = 3.1415926
  ...
}

在程序中,可以使用 Math.PI 来访问这个常量。

由于每个类对象都可以修改公共字段,所以最好不要有公共字段。而公共常量(即 final 字段)却没问题

静态方法

静态方法不是在对象上执行的方法。可以认为静态方法是没有 this 的方法。在两种情况下可以使用静态方法:

  • 方法不需要访问对象状态,因为它所需要的所有参数都通过显式参数提供
  • 方法只需访问类的静态字段

工厂方法

静态方法还有一种常见的用途。类似 LocalDateNumberFormat 的类使用静态工厂方法来构造对象

main 方法

可以调用静态方法而不需要任何对象。不需要构造 Math 类的任何对象就可以调用 Mah.pow 。同理, main 方法也是一个静态方法

public class Application {
	public static void main(String [] args) {
    // construct objects here
    ...
  }
}

main 方法可以不对任何对象进行操作。静态的 main 方法将执行并构造程序所需的对象。

每一个类可以有一个 main 方法。这是常用于对类进行单元测试的一个技巧

方法参数

按值调用表示方法接收的是调用者提供的值,而按引用调用表示方法接受的是调用者提供的变量地址。方法可以修改按引用传递的变量的值,而不能修改按值传递的变量的值。

Java 程序设计语言总是采用按值调用。也就是说,方法得到的是所有参数值的一个副本。具体来讲,方法不能修改传递给它的任何参数变量的内容。

比如

double percent = 10;
harry.raiseSalary(percent); // percent 值依然为 10

但是如果传入引用对象会产生一个引用对象的副本,而更改这个引用对象的副本会直接更改引用对象的值,但是在方法结束时副本会被抛弃。这和 JavaScript 中的引用对象稍有不同,比如在 Java 中:

public static void swap(Employee x, Employee y) {
	Employee temp = x;
  x = y;
  y = temp;
}

var a = new Employee("alice", ...);
var b = new Employee("bob", ...);
swap(a, b) // 并不会交换值

因为 swap 方法的参数 x 和参数 y 被初始化为两个对象引用的副本,而这个方法交换的是这两个副本。

总结下 Java 中对方法参数能做什么和不能做什么

  • 方法不能修改基本数据类型的参数(即数值型和布尔型)。
  • 方法可以改变对象参数的状态。
  • 方法不能让一个对象参数引用一个新的对象。

对象构造

Java 提供了多种编写构造器的机制

重载

有些类有多个构造器,如下构造一个·空的 StringBuilder 对象

var messages = new StringBuilder()

或指定一个初始字符串

var message = new StringBuilder("To do:\n")

这种功能叫做重载,如果多个方法(比如构造器方法)有相同的名字,不同的参数,就出现了重载。它用各个方法首部中的参数类型与特定方法调用中所使用的值类型进行匹配,来选出正确的方法,查找匹配的过程被称为 重载解析

Java 允许重载任何方法,因此,要完整地描述一个方法,需要指定方法名以及参数类型。这叫做方法的签名

默认字段初始化

如果在构造器中没有显式地为字段设置初值,那么就会被自动地赋为默认值:数值为 0 、布尔值为 false 、对象引用为 null

无参数的构造器

很多类都包含一个无参数的构造器,由无参数构造器创建对象时,对象的状态会设置为适当的默认值。比如,以下是 Employee 类的无参数构造器

public Employee()
{
  name = "";
  salary = 0;
  hireDay = LocalDate.now()
}

如果写一个类时没有写构造器,就会为你提供一个无参数构造器。这个构造器将所有的实例字段设为默认值。

如果类中提供了至少一个构造器,但是没有提供无参数的构造器,那么构造对象时如果不提供参数就是不合法的

显式字段初始化

通过重载类的构造器方法,可以采用多种形式设置类的实例字段的初始状态,不管怎样调用构造器,每个实例字段都要设置为一个有意义的初值。

可以再类定义中直接为任何字段赋值,比如

class Employee {
	private String name = "";
}

在执行构造器之前完成这个赋值操作,如果一个类的所有构造器都希望把某个特定的实例字段设置为同一个值,这个语法就特别有用

调用另一个构造器

如果构造器的第一个语句形如 this(...) ,这个构造器将调用同一个类的另一个构造器

public Employee(double s) {
  this("Employee #" + nextId, s);
  nextId++;
}

在 C++ 中,一个构造器不能调用另一个构造器,必须将抽取出的初始化代码边写成一个独立的方法

初始化块 在一个类的声明中,可以包含任意多个代码块。只要构造这个类的对象,这些块就会执行。

// 会首先运行初始化块,然后才运行构造器的主体部分
class Employee {
  private int id;
  {
    id = nextId;
    nextId++
  }
  public Employee() {
    name = "";
    salary = 0;
  }
}

这种机制不是必须的,也不常见

如果类的金泰字段需要很复杂的初始化代码,那么可以使用静态的初始化块

static {
  var generator = new Random();
  nextId = generator.nextInt(10000);
}

对象析构和 finalize 方法

在 C++ 中,有显式地析构器方法,其中放置一些当对象不再使用时需要执行的清理代码。而 Java 由于有自动的垃圾回收,所以我们可以在里面写对其他资源的回收。

Java 允许使用包将类组织在一个集合中,借助包可以方便地组织自己的代码,将自己的代码与别人提供的代码库分开管理

包名

使用包的主要原因是确保类名的唯一性。假如两个程序员不约而同地建立了 Employee 类,只要将这些类放在不同的包中,就不会产生冲突。事实上,为了保证包名的绝对唯一性,要用一个因特网域名以逆序的形式作为包名,然后对于不同的工程使用不同的子包

类的导入

一个类可以使用所属包中的所有类,以及其他包中的公共类

我们可以采用两种方式访问另一个包中的公共类。第一种方式就是使用完全限定名称 fully qualified name ;就是包名后跟着类名。例如:

java.time.LocalDate today = java.time.LocalDate.now();

这显然很繁琐。更简单且常用的方式是使用 import 语句。 import 语句是一种引用包中各个类的简洁方式。一旦使用了 import 语句,在使用类时,就不必写出类的全名了

import 语句应该位于源文件的顶部(但位于 package 语句的后面)。比如,使用下面语句导入 java.util 包中的所有类

import java.time.*

然后就可以使用

LocalDate today = LocalDate.now();

需要注意的是,只能使用 * 导入一个包,不嫩使用 import java.*import java.*.*

但在发生命名冲突时,就需要注意包了。此时需要在每个类名前加上完整的包名。

在包中定位类是编译器的工作。类文件中的字节码总是完整的包名引用其他类

在 C++ 中,与包机制类似的时命名空间 namespace 特性,可以认为 Java 中的 packageimport 语句类似于 C++ 中的 namespaceusing 指令

静态导入

有一种 import 语句允许导入静态方法和静态字段,而不只是类。

比如

import static java.lang.System.*;

可以这样使用

out.println("Goodbye, World!");

还可以导入特定的方法和字段

import static java.lang.System.out;

在包中增加类

要想将类放入包中,就需要将包的名字放在源文件的开头,即放在定义这个包中各个类的代码之前。比如

package com.horstmann.corejava;

public class Employee {
  ...
}

如果没有在源文件中放置 package 语句,这个源文件中的类就属于无名包 unnamed package

将源文件放在与完整包名匹配的子目录中。比如, com.horstmann.corejava 包中的所有源文件应该放在 com/horstmann/corejava 中。编译器将类文件也放在相同的目录结构中。

编译器在编译源文件的时候不检查目录结构。所以包路径不对也可能不会报错,但是最后的程序将无法运行。如果包与目录不匹配,虚拟机就找不到类

包访问

前面已经接触访问修饰符 publicprivate 。而如果没有指定 publicprivate ,则这个部分(类,方法和变量)可以被同一个包中的所有方法访问。

类路径

类文件也可以存储在 JAR(Java 归档) 文件中。在一个 JAR 文件中,可以包含多个压缩形式的类文件和子目录,这样既可以节省空间又可以改善性能。在程序中用到第三方的库文件时,你通常需要得到一个或多个需要包含的 JAR 文件

为了使类能够被多个程序共享,需要做到以下几点

  1. 把类文件放到一个目录中,例如/home/user/classdir。需要注意,这个目录是包树状结构的基目录。如果希望增加com.horstmann.corejava.Employee类,那么Employee.class类文件就必须位于子目录/home/user/classdir/com/horstmann/corejava
  2. JAR文件放在一个目录中,例如:/nome/user/archives
  3. 设置类路径class path。类路径是所有包含类文件的路径的集合

在 UNIX 环境中,类路径中的各项之间用 : 分隔

在 Windows 环境中,以 ; 分隔

不论是 UNIX 还是 Windows,都用 . 表示当前目录

可以在 JAR 文件目录中指定通配符,如下

/home/user/classdir:.:/home/user/archives/'*' 在 UNIX 中, * 必须转义以防止 shell 扩展

javac 编译器总是在当前目录中查找文件,但 java 虚拟机仅在类路径中包含 . 目录的时候才查看当前目录。所以如果设置了类路径就必须包含 . ,不然可以通过编译,但不能运行

设置类路径

最好使用 -classpath(或 -cp), 或者 java 9 中的 --class-path 选项指定类路径:

java -classpath /home/user/classdir:,:/home/user/archives/arhive.jar MyProg

或者

java -classpath c:\classdir;.;\archives\archives\archive.jar MyProg

整个指令必须放在一行中。这样的命令最好放在一个 shell 脚本或是一个批处理文件中

JAR 文件

在将应用程序打包时,我们希望只像用户提供一个单独的文件,而不是一个包含大量类文件的目录结构,Java 归档 JAR 文件就是为此目的而设计的。

创建 JAR 文件

可以使用 jar 工具制作 JAR 文件。创建一个新的 JAR 文件最常用的命令是使用以下语法:

jar cvf jarFileName file1 file2 ...

通常,jar 命令的格式如下

jar options file1 file2 ...

具体选项见 P143

清单文件

除了类文件、图像和其他资源外,每个 JAR 文件还包含一个清单文件,用于描述归档文件的特殊特性。

清单文件被命名为 MANIFEST.MF ,它位于 JAR 文件的一个特殊的 META-INF 子目录中。

复杂的清单文件可能包含更多条目,这些清单条目被分成多个节。第一节被称为主节。它作用于整个 JAR 文件。随后的条目用来指定命名实体的属性,例如单个文件、包或 URL。它们都必须以一个 Name 条目开始。节与节之间用空行分开。

要想编辑清单文件,需要将希望添加到清单文件中的行放到文本文件中,然后运行:

jar cfm jarFileName manifestFileName ...

比如想要创建一个包含清单文件的 JAR 文件,应该运行:

jar cfm MyArchive.jar manifest.mf com/mycompany/mypkg/*.class

想要更新一个已有的 JAR 文件的清单,则需要将增加的部分放置到一个文本文件中,然后执行以下命令:

jar ufm MyArchive.jar manifest-additions.mf

具体请参看 https://docs.oracle.com/javase/10/docs/specs/jar/jar.html

可执行 JAR 文件

可以使用 jar 命令中的 e 选项指定程序段入口点,即通常需要在调用 java 程序启动器时指定的类: jar cvfe MyProgram.jar com.muycompany.mypkg.MainAppClass files to add

或者可以再清单文件中指定程序的主类,包括以下形式的语句:

Main-Class: com.mycompany.mypkg.MainAppClass

不要为主类名增加扩展名 .class

警告:清单文件的最后一行必须以换行符结束。否则,清单文件将无法被正确读取。

不论使用哪一种方法,用户可以简单地通过下面的命令来启动程序:

java -jar MyJProgram.jar

取决于操作系统的配置,用户甚至可以通过双击 JAR 文件图标来启动应用。

多版本 JAR 文件

有时我们需要向 Java 8 和 Java 9 用户发布不同的应用程序,这是可以使用多版本 JAR multi-release JAR ,其中可以包含面向不同 Java 版本的类文件

为了保证向后兼容,而外的类文件放在 META-INF/versions 目录中:

Application.class
BuildingBlocks.class
Util.class
META-INF
	MANIFEST.MF(with line Multi-Release: true)
	versions
	 9
	 	Application.class
	 	BuildingBlocks.class
	 10
	 	BuildingBlocks.class

Java 9 版本的代码将使用 9 后面的 Application.class

想要增加不同版本的类文件,可以使用 --release 标志:

jar uf MyProgram.jar --release 9 Application.class

关于命令行选项的说明

Java 开发包的命令行选项一直以来都是用单个短横线加多字母选项名的形式,比如

java -jar ...

java -Xlint:unchecked -classpath...

但 jar 命令是个例外,这个命令遵循经典的 tar 命令选项格式,而没有短横线:

jar cvf ...

但在 Java 9 之后,需要在多字母选项名前加两个短横线。

-- 和多字母的选项的参数用空格或一个等号 = 分隔:

javac --class-path /home/user/classdir ...javac --class-path=/home/user/classdir ...

不过因为这些规范还没有统一,可以安全地使用 jar 命令的长选项

文档注释

JDK 包含一个很有用的工具,叫做 javadoc ,它可以由源文件生成一个 HTML 文档。如果在源代码中添加以特殊定界符 /** 开始的注释,就可以很容易地生成一个看上去具有专业水准的文档,此时修改源代码并重新运行 javadoc 就可以保持两者的一致性

注释的插入

Javadoc 实用工具从下面几项中抽取信息:

  • 模块
  • 公共类与接口
  • 公共的和受保护的字段
  • 公共的和受保护的构造器及方法

可以为以上各个特性编写注释。注释放置在所描述的特性的前面。每个 /**...*/ 文档注释包含标记以及之后紧跟着的自由格式文本。标记以 @ 开始,如 @since@param

自由格式文本的第一句应该是一个概要性的句子。Javadoc 工具自动地将这些句子抽取出来生成概要页面。

在自由格式文本中,可以使用 HTML 修饰符,例如,用于强调的 <emp>...</emp> ,用于着重强调的 <strong>...</strong> 、用于项目符号列表的 <ul>/<li> 以及用于包含图像的 <img ...> 等。要键入等宽带吗,需要使用 {@code ...} 而不是 <code>...</code> ——这样一来,就不用操心对代码中的 < 字符转移了。

如果文档中有到其他文件的链接,如图像文件,就应该将这些文件放到包含源文件的目录下的一个子目录 doc-files 中。

类注释

类注释必须放在 import 语句之后,类定义之前。

下面是一个类注释的例子:

/**
 * A {@code Card} object represents a playing card, such
 * as "Queen of Heart".
 */
 public class Card
 { ... }

方法注释

每一个方法注释必须放在所在所描述的方法之前。除了通用标记之外,还可以使用下面的标记:

  • @param variable description

    这个标记将给当前方法 parameters 部分添加一个条目。这个描述占据多行,并且可以使用 HTML 标记。一个方法的所有 @param 标记必须放在一起

  • @return description

    这个标记将给当前方法调价 returns 部分。这个描述可以跨多行,并且可以使用 HTML 标记。

  • @throws class description

    这个标记将添加一个注释,表示这个方法有可能抛出异常。有关异常的详细内容将在第 7 章中讨论。

下面是一个方法注释的实例:

/**
	* 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
	*/

字段注释

只需要对公共字段(通常指的是静态常量)建立文档。例如

/**
 * The "Hearts" card suit
 */
public static final int HEARTS = 1;

通用注释

标记 @since text 会建立一个 since 条目。 text 可以是引入这个特性版本的任何描述,比如 @since 1.7.1

下面的标机可以用在类文档注释中

  • @author name

    这个标记将产生一个 author 条目。可以使用多个 @author 标记,每一个 @author 标记对应一个作者,这样版本控制系统能够更好地跟踪作者

  • @version text

    这个标记将产生一个 version 条目。这里的文本可以是对当前版本的任何描述

通过 @see@link 标记,可以使用超链接,链接到 javadoc 文档的相关部分或外部文档。

标记 @see reference 将在 see also 部分增加一个超链接,可以用于类中,也可用于方法中

@see com.horstmann.corejava.Employee#raiseSalary(double)

这会建立一个链接到 com.horstmann.corejava.Employee 类的 raiseSalary(double) 方法的超链接。可以省略包名,甚至把包名和类名都省去,这样一来,这会位于当前包或当前类中

需要注意,一定要使用 # 而不是 . 分隔类名与方法名,或类名与方法名

如果 @see 标记后面有一个 < 字符,就需要指定一个超链接。可以超链接到任何 URL,比如:

@see <a href="www.horstmann.com/corejava.html">The Core Java home page</a>

在上述各种情况下,都可以指定一个可选的标签 label 作为链接锚 link anchor ,如果省略了标签,用户看到的就是链接

如果 @see 标记后面有一个双引号 " 字符,文本就会显示在 see also 部分。例如:

@see "Core Java 2 volume 2"

包注释

可以直接将类、方法和变量的注释放置在 Java 源文件中,只要用 /**...*/ 文档注释界定就可以了。但是,想要产生包注释,就需要在每一个包目录中添加一个单独的文件,有以下两个选择:

  1. 提供一个名为package.info.java的 java 文件。这个文件必须包含一个初始的以/***/界定的 Javadoc 注释,后面是一个 package 语句。
  2. 提供一个名为package.html的 HTML 文件。会抽取标记·<body>...</body>之间的所有文本

注释抽取

如果希望 HTML 文件将放在名为 docDirectory 的目录下。执行以下步骤:

  1. 切换到包含想要生成文档的源文件的目录。如果有嵌套的包要生成文档,比如com.horstmann.corejava,就必须切换到包含子目录com的目录

  2. 如果是一个包,应该运行命令

javadoc -d docDirectory nameOfPackage1 nameOfPackage2 ...

如果文件在无名的包中,就应该运行

javadoc -d docDirectory *.java

不能省略 -d docDirectory 选项,否则 HTML 文件就会被提取到当前目录下。另一个很有用的选项是 -link ,用来为标准类添加超链接。比如

javadoc -link http://docs.oracle.com/javase/9/docs/api *.java

那么,所有标准类库都会自动链接到 Oracle 网站的文档

类设计技巧

应用这些技巧可以使你设计的类更能得到专业 OOP 圈子的认可

  1. 一定要保证数据私有。绝对不要破坏封装性,最好保持实例字段的私有性。
  2. 一定要对数据进行初始化。最好不要依赖系统的默认值,应该显式地初始化所有数据。
  3. 不要再类中使用过多的基本类型。这个想法是使用其他的类替换使用多个相关的基本类型。
  4. 不是所有字段都需要单独的字段访问器和字段更改器。
  5. 分解有过多职责的类。如果明显地可以将一个类分解成两个更为简单的类,就应该将其分解。
  6. 类名和方法名能够体现它们的职责。与变量应该有一个能够反映其含义的名字一样,类也应该如此。
  7. 优先使用不可变的类。LocalDate类以及java.time包中的其他类是不可变的——没有方法能够修改对象的状态