20230519 4. 对象与类
对 象 与 类
面向对象程序设计概述
面向对象 OOP
面向对象的程序是由对象组成的, 每个对象包含对用户公开的特定功能部分和隐藏的实现部分。
传统的结构化程序设计通过设计一系列的过程(即算法)来求解问题。一旦确定了这些过程, 就要开始考虑存储数据的方式。这就是 Pascal 语言的设计者 Niklaus Wirth 将其著作命名为《算法 + 数据结构 = 程序》 ( Algorithms + Data Structures = Programs, Prentice Hall, 1975 ) 的原因。
需要注意的是,在 Wirth 命名的书名中, 算法是第一位的, 数据结构是第二位的,这就明确地表述了程序员的工作方式。首先要确定如何操作数据, 然后再决定如何组织数据, 以便于数据操作。 而 OOP 却调换了这个次序, 将数据放在第一位,然后再考虑操作数据的算法。
对于一些规模较小的问题, 将其分解为过程的开发方式比较理想。而面向对象更加适用于解决规模较大的问题。
例如一个简单的Web浏览器,实现需要2000个过程,采用面向对象大约需要100个类,每个类平均包含20个方法。这种结构更易于掌握和寻找bug
类
类( class )是构造对象的模板或蓝图
由类构造 (construct)对象的过程称为创建类的实例 ( instance )
封装 ( encapsulation , 有时称为 数据隐藏 )是与对象有关的一个重要概念。 从形式上看,封装不过是将数据和行为组合在一个包中, 并对对象的使用者隐藏了数据的实现方式。对象中的数据称为 实例字段( instance field ), 操纵数据的过程称为 方法( method )。 对于每个特定的类实例(对象)都有一组特定的实例字段值。这些值的集合就是这个对象的当前 状态 ( state )。无论何时, 只要向对象发送一个消息,它的状态就有可能发生改变。
实现封装的关键在于绝对不能让类中的方法直接地访问其他类的实例字段。 程序仅通过对象的方法与对象数据进行交互。
封装给对象赋予了 “ 黑盒 ” 特征, 这是提高重用性和可靠性的关键。 这意味着一个类可以全面地改变存储数据的方式,只要仍旧使用同样的方法操作数据, 其他对象就不会知道或介意所发生的变化。
OOP 的另一个原则会让用户自定义 Java 类变得轻而易举, 这就是: 可以通过扩展一个类来建立另外一个新的类。
事实上, 在 Java 中, 所有的类都源自于一个“神通广大的超类”,它就是 Object
在扩展一个已有的类时, 这个扩展后的新类具有所扩展的类的全部属性和方法。在新类中, 只需提供适用于这个新类的新方法和数据字段就可以了。通过扩展一个类来建立另外一个类的过程称为 继承(inheritance) 。
对 象
对象的三个主要特性:
- 对象的行为(behavior) :可以对对象施加哪些操作,或可以对对象施加哪些方法?
- 对象的状态 (state) :当施加那些方法时,对象如何响应?
- 对象标识(identity ) :如何辨别具有相同行为与状态的不同对象?
同一个类的所有对象实例, 由于支持相同的 行为 而具有家族式的相似性。对象的行为是用可调用的方法定义的。
此外,每个对象都保存着描述当前特征的信息。这就是对象的 状态 。对象的状态可能会随着时间而发生改变,但这种改变不会是自发的。 对象状态的改变必须通过调用方法实现(如果不经过方法调用就可以改变对象状态, 只能说明封装性遭到了破坏)。
对象的状态并不能完全描述一个对象。每个对象都有一个唯一的 标识( identity ) 。例如,在一个订单处理系统中, 任何两个订单都存在着不同之处,即使所订购的货物完全相同也是如此。需要注意,作为一个类的实例, 每个对象的标识永远是不同的, 状态常常也存在着差异 。
对象的这些关键特性在彼此之间相互影响着。 例如, 对象的状态影响它的行为(如果一个订单“已送货” 或“已付款”, 就应该拒绝调用具有增删订单中条目的方法。反过来, 如果订单是“空的”,即还没有订购任何商品,就不该允许“发货”) 。
识 别 类
传统的过程化程序设计, 必须从顶部的 main 函数开始编写程序。在面向对象程序设计时没有所谓的“ 顶部”。对于学习OOP 的初学者来说常常会感觉无从下手。答案是:首先从设计类开始,然后再往每个类中添加方法。
识别类的简单规则是在分析问题的过程中寻找名词,而方法对应着动词。
当然, 所谓“ 找名词与动词” 原则只是一种经验, 在创建类的时候, 哪些名词和动词是重要的完全取决于个人的开发经验。
类之间的关系
在类之间, 最常见的关系有
- 依赖 ( uses-a )
- 聚合( has-a )
- 继承( is-a )
依赖(dependence ) ,即 “ uses-a ” 关系, 是一种最明显的、 最常见的关系。 如果一个类的方法操纵另一个
类的对象,我们就说一个类依赖于另一个类。
应该尽可能地将相互依赖的类减至最少。用软件工程的术语来说, 就是让类之间的耦合度最小。
聚合(aggregation ) ,即 “ has-a ” 关系, 是一种具体且易于理解的关系。聚合关系意味着类 A 的对象包含类 B 的对象。
继承(inheritance)表示一个更特殊的类与一个更一般的类之间的关系
表达类关系的 UML ( Unified Modeling Language, 统一建模语言 )符号 :
区分依赖和聚合:
-
依赖表示的是一个类对另一个类的使用,但并不依赖于它的存在。通俗来讲,一个类对象在运行期间需要另一个类对象的支持,但是它们之间并不是强依赖关系。如果依赖的类发生变化,不会对该类造成影响,而且该类可以在不依赖这个类对象的情况下独立存在。依赖关系通常通过构造函数、方法参数等方式来体现。
-
聚合则表示一个类是由另一个类的实例组成,或者说一个类包含多个其他的类对象。聚合关系是一种“has-a”的关系,比如一辆车“拥有”四个轮子和一个引擎。 这样的关系通常用于表示对象之间的整体部分关系。聚合关系的特点是组成部分可以拥有独立的生命周期,可以分别存在,彼此之间没有强依赖关系。
总的来说,依赖关系往往是比较临时和短暂的,而聚合关系通常是较为长久的“成员”的关系
使用预定义类
并不是所有的类都具有面向对象特征。 例如,Math
类。
对象与对象变量
要想使用对象,就必须首先构造对象, 并指定其初始状态。然后,对对象应用方法。
在 Java 程序设计语言中, 使用 构造器(constructor) 构造新实例。 构造器是一种特殊的方法, 用来构造并初始化对象。
Java 的日期类库有些混乱, 已经重新设计了两次
一定要认识到: 一个对象变量并没有实际包含一个对象,而仅仅引用一个对象。 所有的对象都存储在堆中。
在 Java 中,任何对象变量的值都是对存储在另外一个地方的一个对象的引用。new
操作符的返回值也是一个引用。
局部变量不会自动地初始化为 null
,而必须通过调用 new
或将它们设置为 null
进行初始化。
String s1, s2 = null;
// System.out.println(s1.toString()); // 编译时报错:Variable 's1' might not have been initialized
System.out.println(s2.toString()); // 运行时异常:Exception in thread "main" java.lang.NullPointerException
Java 类库中的 LocalDate
类
时间是用距离一个固定时间点的毫秒数(可正可负)表示的,这个时间点就是所谓的纪元(epoch),它是UTC时间1970年1月1日0时。UTC 就是 Coordinated Universal Time (国际协调时间),与 GMT(Greenwich Mean Time ,格林尼治时间)一样,是一种实用的科学标准时间。
UTC 更加准确且广泛使用,而 GMT 已经大多数被取代,成为一个地理术语,用来指代经过格林尼治子午线的时区。
类库设计者决定将保存时间与给时间点命名分开。所以标准 Java 类库分别包含了两个类:一个是用来表示时间点的 Date
类;另一个是用来表示大家熟悉的日历表示法的 LocalDate
类。Java SE 8 引入了另外一些类来处理日期和时间的不同方面
将时间度量与日历分开是一种很好的面向对象设计
不要使用构造器来构造 LocalDate
类的对象。实际上,应当使用静态工厂方法 (factory method) 代表你调用构造器。
类库设计者意识到应当单独提供类来处理日历, 不过在此之前这些方法已经是 Date
类的一部分了。Java 1.1 中引入较早的一组日历类时,Date
方法被标为废弃不用。 虽然仍然可以在程序中使用这些方法, 不过如果这样做, 编译时会出现警告。 最好还是不要使用这些废弃不用的方法, 因为将来的某个类库版本很有可能将它们完全删除。
LocalDate now = LocalDate.now();
System.out.println(LocalDate.of(2021, 10, 26)); // 2021-10-26
Date date = new Date(2021,10,26); // 注意:2021-11-26
System.out.println(date); // Sat Nov 26 00:00:00 CST 3921
更改器方法与访问器方法
Java 库的一个较早版本曾经有另一个类来处理日历, 名为 GregorianCalendar
。
与 LocalDate.plusDays
方法返回新的对象不同,GregorianCalendar.add
方法是一个 更改器方法 ( mutator method )
只访问对象而不修改对象的方法有时称为 访问器方法(accessor method),例如,LocalDate.getYear
和 GregorianCalendar.get
就是访问器方法
在 Java 中,访问器方法和更改器方法在语法上没有区别,有些注解可以提供约定的见文知义的效果,但是无法强制
显示当前月的日历:
LocalDate localDate = LocalDate.now();
int month = localDate.getMonthValue();
int today = localDate.getDayOfMonth();
localDate = localDate.minusDays(today - 1); // Set to start of month
DayOfWeek weekday = localDate.getDayOfWeek();
int value = weekday.getValue(); // 1 = Monday, ... 7 = Sunday
// 打印头信息
System.out.println("Mon Tue Wed Thu Fri Sat Sun");
// 打印第一周的空格
for (int i = 1; i < value; i++) {
System.out.print(" ");
}
while (localDate.getMonthValue() == month) {
System.out.printf("%3d", localDate.getDayOfMonth());
if (localDate.getDayOfMonth() == today) {
System.out.print("*");
} else {
System.out.print(" ");
}
localDate = localDate.plusDays(1);
if (localDate.getDayOfWeek() == DayOfWeek.MONDAY) {
System.out.println();
}
}
if (localDate.getDayOfWeek() != DayOfWeek.MONDAY) {
System.out.println();
}
用户自定义类
如果可以从变量的初始值推导出它们的类型,那么可以用 var 关键字声明局部变量
null 表示没有引用任何对象,对 null 应用方法,会产生 NullPointerException
异常,NPE
从 Java 17 开始,NullPointerException
异常错误消息会包含 null 值的变量或方法名
java.util.Objects
工具类中与 null 相关的方法:
- isNull
- nonNull
- requireNonNull
- requireNonNullElse
- requireNonNullElseGet
隐式参数与显式参数
public void raiseSalary(double byPercent) {
double raise = salary * byPercent / 100;
salary += raise;
}
raiseSalary 方法有两个参数。 第一个参数称为 隐式 (implicit ) 参数, 是出现在方法名前的 Employee 类对象。 第二个参数位于方法名后面括号中的数值, 这是一个 显式 (explicit ) 参数 (有些人把隐式参数称为方法调用的目标或接收者)
在每一个方法中, 关键字 this
表示隐式参数。
在 Java 中,所有的方法都在类的内部定义,但这并不表示它们是内联方法。是否将某个方法设置为内联方法是 Java 虚拟机的任务。即时编译器会关注哪些简短、经常调用而且没有被覆盖的方法调用,并进行优化。
封装的优点
不要引入与实例字段同名的局部变量,局部变量会遮蔽(shadow)实例字段
getXXX 方法只返回实例字段的值,又称为 字段访问器(field accessor)
想要获得或设置实例字段的值,需要提供下面三项内容:
- 私有的实例字段
- 公共的字段访问器方法
- 公共的字段更改器方法
警告: 注意不要编写返回引用可变对象的访问器方法。 例如,返回一个 Date
类对象。
如果需要返回一个可变对象的引用, 应该首先对它进行克隆(clone)。 对象 clone 是指存放在另一个位置上的对象副本。
基于类的访问权限
方法可以访问所调用对象的私有数据。
一个类的方法可以访问这个类的所有对象的私有数据,这有些奇怪
public boolean equals(Employee other) {
// 这里不止访问了自身的name,也访问了other对象的name
return name.equals(other.name);
}
调用方式:
if (harry.equals(boss)) {
// ...
}
final 实例字段
可以将实例字段定义为 final。 构建对象时必须初始化这样的字段。也就是说, 必须确保在每一个构造器执行之后, 这个字段的值被设置, 并且在后面的操作中,不能够再对它进行修改。
final 修饰符大都应用于基本 (primitive) 类型字段,或不可变(immutable) 类的字段(如果类中的所有方法都不会改变其对象, 这种类就是不可变的类。例如 String
对于可变的类, 使用 final 修饰符可能会对读者造成混乱。例如,
private final StringBuilder evaluations;
final 关键字只是表示存储在 evaluations 变量中的对象引用不会再指示其他 StringBuilder
对象。 不过这个对象的内容仍然可以更改。
静态字段与静态方法
静态字段
如果将字段定义为 static , 每个类中只有一个这样的字段。而每一个对象对于所有的实例字段却都有自己的一份拷贝。
静态字段属于类,不属于任何独立的对象。
静态字段又被称为类字段。 “静态” 只是沿用了 C++ 的叫法,并无实际意义
静态常量
在 Math
类中定义了一个静态常量:
public static final double PI = 3.14159265358979323846;
在程序中,可以采用 Math.PI
的形式获得这个常量
另一个多次使用的静态常量是 System.out
public final static PrintStream out = null;
这里赋值为 null 也是已赋值状态。
如果查看一下 System
类, 就会发现有一个 setOut
方法, 它可以将 System.out
设置为不同的流。 读者可能会感到奇怪, 为什么这个方法可以修改 final 变量的值。 原因在于, setOut
方法是一个本地方法, 而不是用 Java 语言实现的。 本地方法可以绕过 Java 语言的存取控制机制。 这是一种特殊的方法, 在自己编写程序时, 不应该这样处理。
静态方法
静态方法在运算时,没有隐式的参数(this)。
静态方法不能实例字段,可以访问静态字段
可以通过类名或对象调用静态方法。不过,通过对象调用静态方法很容易造成混淆, 其原因是静态方法计算的结果与对象毫无关系。 建议使用类名, 而不是对象来调用静态方法。
在下面两种下使用静态方法:
- 方法不需要访问对象状态,其所需参数都是通过显式参数提供(例如:
Math.pow
) - 方法只需要访问类的静态字段(例如:
Math.toRadians
)
工厂方法
静态方法还有另外一种常见的用途。类似 LocalDate
和 NumberFormat
的类使用静态工厂方法(factory method) 来构造对象。
为什么 NumberFormat
类不利用构造器完成这些操作呢? 这主要有两个原因:
- 无法命名构造器。构造器的名字必须与类名相同。但是, 这里希望将得到的货币实例和百分比实例采用不同的名字
- 当使用构造器时,无法改变所构造的对象类型。而 Factory 方法将返回一个
DecimalFormat
类对象, 这是NumberFormat
的子类
main 方法
main 方法也是一个静态方法
main 方法不对任何对象进行操作。事实上,在启动程序时还没有任何一个对象。静态的 main 方法将执行并创建程序所需要的对象。
每一个类可以有一个 main 方法。这是一个常用于对类进行单元测试的技巧。
方法参数
按值调用 (call by value) 表示方法接收的是调用者提供的值。按引用调用 ( call by reference) 表示方法接收的是调用者提供的变量位置(location)。一个方法可以修改传递引用所对应的变量值, 而不能修改传递值调用所对应的变量值。
Java 程序设计语言总是采用按值调用。 方法得到的是所有参数值的一个拷贝,特别是,方法不能修改传递给它的任何参数变量的内容。
方法参数共有两种类型:
- 基本数据类型(数字、布尔值)
- 对象引用
Java 对两种方法参数类型的处理都是相同的,都是传递参数值拷贝,基本数据类型的拷贝在方法中被改变不会影响到入参,对象引用的拷贝实际上是指向相同对象的引用,所以如果将引用的拷贝重新指向其他对象,不影响入参引用,如果是使用拷贝引用修改指向对象的状态,因为指向的对象是同一个,所以感觉是入参引用被改变了。
方法参数:
- 方法不能修改基本数据类型的参数
- 方法可以改变对象参数的状态
- 方法不能让一个对象参数引用一个新对象
对 象 构 造
重载
如果多个方法(比如, StringBuilder 构造器方法)有相同的名字、 不同的参数,便出现了 重载(overloading)
编译器必须挑选出具体执行哪个方法,它通过用各个方法给出的参数类型与特定方法调用所使用的值类型进行匹配来挑选出相应的方法。 如果编译器找不到匹配的参数, 就会产生编译时错误, 因为根本不存在匹配, 或者没有一个比其他的更好。(这个过程被称为 重载解析(overloading resolution)。)
Java 允许重载任何方法, 而不只是构造器方法。因此,要完整地描述一个方法,需要指出 方法名 以及 参数类型 。这叫做 方法的签名(signature)。 例如, String 类有 4 个称为 indexOf
的公有方法。
indexOf(int)
indexOf(int, int)
indexOf(String)
indexOf(String, int)
返回类型不是方法签名的一部分。 也就是说, 不能有两个名字相同、 参数类型也相同却返回不同类型值的方法。
默认字段初始化
如果在构造器中没有显式地给字段赋予初始值,那么就会被自动地赋为默认值: 数值为 0、布尔值为 false、 对象引用为 null。但是,这并不是一种良好的编程习惯。
这是字段与局部变量的主要不同点。 必须明确地初始化方法中的局部变量。 但是,如果没有初始化类中的字段, 将会被自动初始化为默认值(0、 false 或 null )。
无参数的构造器
仅当类没有提供任何构造器的时候, 系统会提供一个默认的构造器。这个构造器将所有的实例字段设置为默认值
使用 this(...)
在构造器中调用另一个构造器
显式字段初始化
可以在类定义中, 直接将一个值赋给任何字段。
public class Parent {
private int i = 1;
// ...
}
在执行构造器之前,先执行赋值操作。 当一个类的所有构造器都希望把相同的值赋予某个特定的实例字段时,这种方式特别有用。
初始值不一定是常量值, 可以调用方法对字段进行初始化。
初始化块
三种初始化数据字段的方法:
- 在构造器中设置值
- 在声明中赋值
- 初始化块(initialization block)
初始化块分为非静态和静态两种,都在构造器之前执行。静态块只能访问静态字段,且在非静态块之前执行。
public class Parent {
private int i1 = 11;
private static int i2 = 21;
{
i1 = 12;
}
static {
i2 = 22;
}
}
初始化块可以定义在字段声明前,初始化结果取决于定义的顺序,例如上面的结果为:i1 = 12、i2 = 22,下面的结果为:i1 = 11、i2 = 21
public class Parent {
{
i1 = 12;
}
static {
i2 = 22;
}
private int i1 = 11;
private static int i2 = 21;
}
初始化块定义在字段声明前,只能赋值,不能使用,否则会报编译错误。只是语法允许,没有实际意义
{
// System.out.println(i1); // 编译错误:Illegal forward reference
i1 = 12;
}
private int i1 = 11;
对象析构与 finalize 方法
由于 Java 有自动的垃圾回收器,不需要人工回收内存, 所以 Java 不支持析构器。
某些对象使用了内存外的资源,例如文件,应当提供 close
方法完成必要的清理工作
不要使用 finalize
方法清理资源, 这是因为很难知道这个方法什么时候才能够调用。而且该方法已废弃。
如果可以等到虚拟机退出,那么可以使用方法 Runtime.addShutdownHook
添加“关闭钩” (shutdown hook)
在 Java 9 中,可以使用 Cleaner
类注册一个动作,当对象不可达时,就会完成这个动作。
记录 Record
记录(record)是一种特殊形式的类,其状态不可变,而且公共可读。
记录的实例字段成为组件(component),自动为 final 字段
示例:
public record Point(double x, double y) {
}
编译后的 class 文件:
public record Point(double x, double y) {
public Point(double x, double y) {
this.x = x;
this.y = y;
}
public double x() {
return this.x;
}
public double y() {
return this.y;
}
}
除了构造器和访问器方法,还有3个自动定义的方法:toString, equals, hashCode
对于自动定义的构造器和方法,可以使用相同方法签名的自定义实现覆盖
可以为记录增加自定义方法
可以有静态字段和方法
不能为记录增加实例字段
对于完全由一组变量表示的不可变数据,要使用记录而不是类。如果数据是可变的,或者数据表示随时间改变,则使用类。记录更易读、更高效,而且在并发程序中更安全。
构造器
自动定义地设置所有实例字段的构造器成为标准构造器(canonical constructor)
还可以自定义构造器,第一个语句必须调用另一个构造器
实现标准构造器时,建议使用一种简洁(compact)形式,不用指定参数列表
public Point {}
// 等效于
public Point(double x, double y) {
this.x = x;
this.y = y;
}
public Point {
x = 0.5;
y = 1.6;
}
// 等效于
public Point(double x, double y) {
x = 0.5;
y = 1.6;
this.x = x;
this.y = y;
}
简介模式是为实例字段赋值前修改参数变量,不能在简洁构造器的主体中读取或修改实例字段
包
Java 允许使用包( package ) 将类组织起来。借助于包可以方便地组织自己的代码,并将自己的代码与别人提供的代码库分开管理。
使用包的主要原因是确保类名的唯一性。为了保证包名的绝对唯一性, Sun 公司建议将公司的因特网域名(这显然是独一无二的)以逆序的形式作为包名,并且对于不同的项目使用不同的子包。
从编译器的角度来看, 嵌套的包之间没有任何关系。 例如,java.util
包与 java.util.jar
包毫无关系。每一个都拥有独立的类集合。
类的导入
一个类可以使用所属包中的所有类,以及其他包中的公共类
可以使用完全限定名
可以使用 import
语句导入一个特定的类或者整个包。
需要注意的是, 只能使用星号(*
) 导入一个包, 而不能使用 import java.*
或 import java.*.*
导入以 java 为前缀的所有包。
如果一个类中需要使用两个 Date 类, 又该怎么办呢? 答案是,在每个类名的前面加上完整的包名。
在包中定位类是编译器 ( compiler ) 的工作。类文件中的字节码肯定使用完整的包名来引用其他类。
静态导入
import
语句不仅可以导入类,还增加了导入静态方法和静态字段的功能。
import static java.lang.System.out;
out.println();
在包中增加类
如果没有在源文件中放置 package
语句, 这个源文件中的类属于无名包(unnamed package)
编译器处理文件(带有文件分隔符和扩展名.java的文件),Java 解释器加载类(带有.分隔符)
包访问
标记为 public 的部分可以被任意的类使用;标记为 private 的部分只能被定义它们的类使用。 如果没有指定 public 或 private, 这个部分(类、方法或变量)可以被同一个包中的所有方法访问。
类 路 径
类存储在文件系统的子目录中。类的路径必须与包名匹配。
JAR 文件使用 ZIP 格式组织文件和子目录。可以使用所有 ZIP 工具查看内部的 rt.jar 以及其他的 JAR 文件。
由于运行时库文件( rt.jar 和在 jre/lib 与 jre/lib/ext 目录下的一些其他的 JAR 文件)会被自动地搜索, 所以不必将它们显式地列在类路径中。
设置类路径
采用 -classpath
(或 -cp
或 Java 9 中的 --class-path
) 选项指定类路径:
Linux:java -classpath /home/user/dassdir:.:/home/user/archives/archive.jar HyProg
Windows:java -classpath c:\classdir;.;c:\archives\archive.jar MyProg
利用 -classpath 设置类路径是首选的方法,另一个种方法是通过设置 CLASSPATH 环境变量来指定类路径
在 Java 9 种,可以从模块路径加载类
Jar 文件
Java 归档文件 - JAR
使用 ZIP 压缩格式
jdk/bin 目录下 jar 命令
每个jar文件包含一个清单文件(manifest),用于描述归档文件的特殊特性,被命名为 MANIFEST.MF,位于JAR文件的一个特殊的 META-INF 子目录中
清单文件可能包含更多条目,清单条目被分组为多个节
清单文件中可以指定主类 Main-Class
java -jar MyProgram.jar
在Windows平台,可以使用第三方的包装器将jar转换为exe可执行文件
Java 9 引入了多版本JAR(multi-release JAR)
特定于版本的类文件放在 /META-INF/versions 目录中
多版本JAR并不适用于不同版本的程序或库。对于不同的版本,所有类的公共API都应该是一样的。多版本JAR的唯一作用是使你的某个特定版本的程序或库能够使用多个不同的JDK版本。如果增加了功能或者改变了一个API,就应当提供一个新版本的JAR
文档注释
JDK 包含一个很有用的工具,叫做 javadoc
, 它可以由源文件生成一个 HTML 文档。
注释的插入
javadoc 实用工具(utility) 从下面几项中抽取信息:
- 模块
- 包
- 公有类与接口
- 公有的和受保护的字段
- 公有的和受保护的构造器及方法
应该为上面几项编写注释 、 注释应该放置在所描述特性的前面。 注释以 /**
开始, 并以 */
结束。
每个 /** . . . */ 文档注释包含标记以及之后紧跟着的 自由格式文本( free-form text )。标记由 @
开始, 如 @author
或 @param
。
自由格式文本的第一句应该是一个概要陈述。javadoc 工具自动地将这些句子抽取出来形成概要页。
在自由格式文本中,可以使用 HTML 修饰符, 例如,用于强调的 <em>
、 用于着重强调的 <strong>
以及包含图像的 <img>
等。不过, 一定不要使用 <hl>
或 <hr>
,因为它们会与文档的格式产生冲突。若要键入等宽代码, 需使用 {@code ... }
而不是 <code>
如果文档中有到其他文件的链接, 例如, 图像文件(用户界面的组件的图表或图像等), 就应该将这些文件放到子目录 doc-files 中。javadoc 工具将从源目录拷贝这些目录及其中的文件到文档目录中。 在链接中需要使用 doc-files 目录, 例如:<img src="doc-files/uml_png" alt="UML diagram" >
。
类注释
类注释必须放在 import 语句之后,类定义之前。
没有必要在每一行的开始用星号 *
,然而, 大部分 IDE 提供了自动添加星号 *
, 并且当注释行改变时, 自动重新排列这些星号的功能。
方法注释
每一个方法注释必须放在所描述的方法之前。除了通用标记之外, 还可以使用下面的标记:
@param
变量描述@return
描述@throws
类描述
字段注释
只需要对公有字段(通常指的是静态常量)增加文档注释。
/**
* The "Hearts" card suit
*/
public static final int HEARTS = 1;
通用注释
下面的标记可以用在类文档的注释中:
标记 | 描述 |
---|---|
@author |
作者 |
@version |
版本 |
下面的标记可以用于所有的文档注释中:
标记 | 描述 | 示例 |
---|---|---|
@since |
始于 | @since version 1.7.1 |
@deprecated |
不再使用 | @deprecated Use <code> setVisible(true) </code> instead |
@see |
引用,用于类和方法上,这个标记将在 “ see also ” 部分增加一个超级链接。 | @see com.horstraann.corejava.Employee#raiseSalary(double) @see <a href="http://www.horstmann.com/corejava.html">The Core Java home page</a> @see "Core Java 2 volume 2" |
@link |
引用,可用于注释中的任何位置 | {@link com.horstraann.corejava.Employee#raiseSalary(double)} |
在 Java 9 中,可以使用 {@index entry} 标记为搜索框增加一个条目
包注释
可以直接将类、 方法和变量的注释放置在 Java 源文件中, 只要用 /** . . . */ 文档注释界定就可以了。但是, 要想产生包注释,就需要在每一个包目录中添加一个单独的文件。可以有如下两个选择:
- 提供一个以
package-info.java
命名的 Java 文件。这个文件必须包含一个初始的以 /** 和 */ 界定的 Javadoc 注释, 后面是一个 package 。它不应该包含更多的代码或注释 - 提供一个以 package.html 命名的 HTML 文件。在标记
<body>
中的所有
文本都会被抽取出来
还可以为所有的源文件提供一个概要注释。这个注释将被放置在一个名为 overview.html 的文件中,这个文件位于包含所有源文件的父目录中。标记 <body>
之间的所有文本将被抽取出来。当用户从导航栏中选择 “Overview” 时,就会显示出这些注释内容。
注释的抽取
javadoc -d docDirectory nameOfPackage1 nameOfPackage2 ...
类设计技巧
-
一定要保证数据私有
-
一定要初始化数据
-
不要在类中使用过多的基本类型
用其他的类代替多个相关的基本类型的使用。这样会使类更加易于理解且易于修改。例如, 用一个
Address
的新类替换Customer
类中以下的实例字段:private String street; private String city; private String state; private int zip;
-
不是所有的字段都需要单独的字段访问器和更改器
-
将职责过多的类进行分解
-
类名和方法名要能够体现它们的职责
命名类的好习惯是采用一个名词(Order)、 前面有形容词修饰的名词 (RushOrder) 或动名词(有
-ing
后缀)修饰名词(例如,BillingAddress) -
优先使用不可变的类
LocalDate
类以及java.time
包中的其他类是 不可变的——没有方法能修改对象的状态 。更改对象的问题在于, 如果多个线程试图同时更新一个对象, 就会发生并发更改。其结果是不可预料的。 如果类是不可变的,就可以安全地在多个线程间共享其对象
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· .NET10 - 预览版1新功能体验(一)
2020-01-13 20200113 SpringBoot整合MyBatis