曾经有人说,作为Java程序员如果没有卷过这本书,就算不上是真正的Java程序员,那么我就也来卷卷它吧。下面是我的读书摘录笔记。
《Java 核心技术卷1 基础知识》第一章 Java程序设计概述 笔记_码农UP2U的博客-CSDN博客
《Java 核心技术卷1 基础知识》第二章 Java 程序设计环境 笔记_码农UP2U的博客-CSDN博客
《Java 核心技术卷1 基础知识》第三章 Java 的基本程序设计结构 笔记_码农UP2U的博客-CSDN博客
目录
4.1 面向对象程序设计概述
面向对象程序设计(object-oriented programming, OOP)
面向对象的程序是由对象组成的,每个对象包含对用户公开的特定功能部分和隐藏的实现部分。
传统的结构化程序设计通过设计一系列的过程(即算法)来求解问题。一旦确定了这些过程,就要开始考虑存储数据的适当方式。这就是 Pascal 语言的设计者 Niklaus Wirth 将其著作命名为《算法 + 数据结构 = 程序》的原因。
在 Wirth 的这个书名中,算法是第一位的,数据结构是第二位的,这就明确地表述了程序员的工作方式。首先要确定如何操作数据,然后再决定如何组织数据的结构,以便于操作数据。而 OOP 却调换了这个次序,将数据放在第一位,然后再考虑操作数据的算法。
4.1.1 类
类(class)是构造对象的模板或蓝图
由类构造(construct)对象的过程称为创建类的实例(instance)
用 Java 编写的所有代码都位于某个类里面
封装(encapsulation)是处理对象的一个重要概念。从形式上看,封装就是将数据和行为组合在一个包中,并对对象的使用者隐藏具体的实现方式。对象中的数据称为实例字段(instance field),操作数据的过程称为方法(method)。作为一个类的实例,特定对象都有一组特定的实例字段值。这些值的集合就是这个对象的当前状态(state)。只要在对象上调用一个方法,它的状态就有可能发生改变。
封装的关键在于,绝对不能让类中的方法直接访问其他类的实例字段。程序只能通过对象的方法与对象数据进行交互。
可以通过扩展其他类来构建新类。
所有其他类都扩展自这个 Object 类
在扩展一个已有的类时,这个扩展后的新类具有被扩展类的全部属性和方法。你只需要在新类中提供适用于这个新类的新方法和数据字段就可以了。通过扩展一个类来建立另外一个类的过程称为继承(inheritance)
4.1.2 对象
对象的三个主要特性:
- 对象的行为(behavior)
- 对象的状态(state)
- 对象的标识(identity)
4.1.3 识别类
首先从识别类开始,然后再为各个类添加方法
识别类的一个经验是在分析问题的过程中寻找名词,而方法对应着动词
对于每一个动词,都要识别出负责完成相应动作的对象
4.1.4 类之间的关系
在类之间,最常见的关系有
- 依赖(uses-a)
- 聚合(has-a)
- 继承(is-a)
依赖(dependence),即“uses-a”关系,是一种 最明显的、最常见的关系。
如果一个类的方法使用或操纵另一个类的对象,我们就说一个类依赖于另一个类。
应该尽可能地将相互依赖的类减至最少。关键是,如果类 A 不知道 B 的存在,它就不会关心 B 的任何改变。用软件工程的术语来说,就是尽可能减少类之间的耦合。
聚合(aggregation),即“has-a”关系。包容关系意味着类 A 的对象包含类 B 的对象。
继承(inheritance),即“is-a”关系,表示一个更特殊的类与一个更一般的类之间的关系。如果类 A 扩展类 B,类 A 不但包含从类 B 继承的方法,还会有一些额外的功能。
UML Unified Modeling Language 统一建模语言
4.2 使用预定义类
Math 类只封装了功能,它不需要也不必隐藏数据。由于没有数据,因此也不必考虑创建对象和初始化它们的实例字段,因为根本没有实例字段。
4.2.1 对象与对象变量
要想使用对象,首先必须构造对象,并指定其初始状态。
使用构造器(constructor)构造新实例。构造器是一种特殊的方法,用来构造并初始化对象。
内置的(built-in)类型
适应性如何
设置任务就交给了类库的设计者。如果类设计得不完善,其他的程序员可以很容易地编写自己的类,以便增强或替代(replace)系统提供的类
要想构造一个 Date 对象,需要在构造器前面加上 new 操作符
new Date();
这个表达式构造了一个新对象。这个对象被初始化为当前的日期和时间。
可以对刚刚创建的对象应用一个方法。对新构造的 Date 对象应用 toString 方法
Stirng s = new Date().toString;
希望构造器可以多次使用,因此,需要将对象存在在一个变量中:
Date birthday = new Date();
对象变量 birthday ,它引用了新构造的对象
对象与对象变量之间存在着一个重要的区别
Date deadline;
定义了一个对象变量 deadline,它可以引用 Date 类型的对象。变量 deadline 不是一个对象,而且实际上它也没有引用任何对象。此时还不能在这个变量上使用任何 Date 方法。下面的语句
s = deadline.toString();
将产生编译错误。
必须首先初始化变量 deadline,这有两个选择,可以初始化这个变量,让它引用一个新构造的对象:
deadline = new Date();
也可以设置这个变量,让它引用一个已有的对象。
deadline = birthday;
现在,这两个变量都引用同一个对象。
要认识到重要的一点:对象变量并没有实际包含一个对象,它只是引用一个对象。
在 Java 中,任何对象变量的值都是对存储在另外一个地方的某个对象的引用。
Date deadline = new Date();
有两部分。表达式 new Date() 构造了一个 Date 类型的对象,它的值是对新创建对象的一个引用。这个引用存储在变量 deadline 中。
可以显式地将对象变量设置为 null,指示这个对象变量目前没有引用任何对象。
所有的 Java 对象都存储在堆中。当一个对象包含另一个对象变量时,它只是包含着另一个堆对象的指针。
在 Java 中,必须使用 clone 方法获得对象的完整副本。
4.2.2 Java 类库中的 LocalDate 类
Date 类的实例有一个状态,即特定的时间点
时间是用距离一个固定时间点的毫秒数表示的,这个时间点就是所谓的纪元(epoch),它是 UTC 时间 1970 年 1 月 1 日 00:00:00 。UTC 就是 Coordinated Universal Time (国际协调时间),与大家熟悉的 GMT (即 Greenwich Mean Time 格林尼治时间)一样,是一种实用的科学标准时间
类库设计者决定将保存时间与给时间点命名分开。所以标准 Java 类库分别包含类两个类:一个是用来表示时间点的 Date 类;另一个是用大家熟悉的日历表示法表示日期的 LocalDate 类。
不要使用构造器来构造 LocalDate 类的对象。实际上,应当使用静态工厂方法(factory method),它会代表你调用构造器。
LocalDate.now()
会构造一个新对象,表示构造这个对象时的日期
可以提供年、月、日来构造对应一个特定日期的对象:
LocalDate.of(1999,12,31)
通常我们都希望将构造的对象保存在一个对象变量中:
LocalDate newYearsEve = LocalDate.of(1999, 12, 31);
一旦有来一个 LocalDate 对象,可以用方法 getYear、getMonthValue 和 getDayOfMonth 得到年、月、日。
Date 类也有得到日、月、年的方法,分别是 getDay、getMonth 以及 getYear,不过这些方法已经废弃。
JDK 提供了 jdeprscan 工具来检查你的代码中是否使用了 Java API 已经废弃的特性
4.2.3 更改器方法与访问器方法
plusDays 方法会生成一个新的 LocalDate 对象
更改器方法(mutator method)
访问器方法(accessor method)
date = date.minusDays(today - 1);
DayOfWeek weekday = date.getDayOfWeek();
int value = weekday.getValue();
这里遵循国际管理,即周末是一周的末尾,星期一就返回 1,星期二返回 2,依此类推。星期日则返回 7。
import java.time.DayOfWeek;
import java.time.LocalDate;
public class CalendarTest
{
public static void main(String[] args) {
LocalDate date = LocalDate.now();
int month = date.getMonthValue();
int today = date.getDayOfMonth();
date = date.minusDays(today - 1);
DayOfWeek dayOfWeek = date.getDayOfWeek();
int value = dayOfWeek.getValue();
System.out.println("Mon Tue Wed Thu Fri Sat Sun");
for (int i = 0; i < value; i ++) {
System.out.print(" ");
}
while (date.getMonthValue() == month) {
System.out.printf("%3d", date.getDayOfMonth());
if (date.getDayOfMonth() == today) {
System.out.print("*");
} else {
System.out.print(" ");
}
date = date.plusDays(1);
if (date.getDayOfWeek().getValue() == 1) {
System.out.println("");
}
}
}
}
4.3 用户自定义类
主力类(workhorse class)
4.3.1 Employee类
最简单的类定义形式为:
class ClassName
{
field1;
field2;
...
constructor1;
constructor2;
...
method1;
method2;
...
}
程序清单 4-2 EmployeeTest/EmployeeTest.java
import java.time.LocalDate;
public class EmployeeTest
{
public static void main(String[] args) {
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);
for (Employee e : staff) {
e.raiseSalary(5);
}
for (Employee e : staff) {
System.out.println("name = " + e.getName() + ", salary = " + e.getSalary() + ", hireDay = " + e.getHireDay());
}
}
}
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;
hireDay = LocalDate.of(year, month, day);
}
public String getName()
{
return name;
}
public double getSalary()
{
return salary;
}
public LocalDate getHireDay()
{
return hireDay;
}
public void raiseSalary(double byPercent)
{
double raise = salary * byPercent / 100;
salary += raise;
}
}
源文件名是 EmployeeTest.java,这是因为文件名必须与 public 类的名字相匹配。在一个源文件中,只能有一个公共类,但可以有任意数目的非公共类。
当编译这段代码的时候,编译器将在目录下创建两个类文件:EmployeeTest.class 和 Employee.class。
将程序中包含 main 方法的类名提供给字节码解释器,以启动这个程序:
java EmployeeTest
字节码解释器开始运行 EmployeeTest 类的 main 方法中的代码。
4.3.2 多个源文件的使用
将 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 编译器就会自动地重新编译这个文件。
4.3.3 剖析 Employee 类
关键字 public 意味着任何类的任何方法都可以调用这些方法
关键字 private 确保只有类自身都方法能够访问这些实例字段,而其他类的方法不能够读写这些字段
可以用 public 标记实例字段,但这是一种很不好的做法。public 数据字段允许程序中的任何方法对其进行读取和修改,这就完全破坏了封装。任何类的任何方法都可以修改 public 字段
类包含都实例字段通常属于某个类类型
4.3.4 从构造器开始
构造器与类同名
在构造类的对象时,构造器会运行,从而将实例字段初始化为所希望的初始状态
构造器总是结合 new 运算符来调用的。不能对一个已经存在的对象调用构造器来达到重新设置实例字段的目的。
构造器
- 构造器与类同名
- 每个类可以有一个以上的构造器
- 构造器可以有 0 个、1 个或多个参数
- 构造器没有返回值
- 构造器总是伴随着 new 操作符一起调用
所有的 Java 对象都是在堆中构造的,构造器总是结合 new 操作符一起使用
4.3.5 用 var 声明局部变量
在 Java 10 中,如果可以从变量的初始值推导出它们的类型,那么可以用 var 关键字声明局部变量,而无须指定类型
不会对数值类型使用 var
var 关键字只能用于方法中的局部变量。参数和字段的类型必须声明。
4.3.6 使用 null 引用
一个对象变量包含一个对象的引用,或者包含一个特殊值 null,后者表示没有引用任何对象。
如果对 null 值应用一个方法,会产生一个 NullPointerException 异常
程序并不捕获这些异常,而是依赖程序员从一开始就不要带来异常
对此有两种解决方法。“宽容型”方法是把 null 参数转换为一个适当的非 null 值:
if (n == null) name = "unknow"; else name = n;
在 Java 9 中, Object 类对此提供类一个便利方法:
public Employee(String n, double s, int year, int month, int day)
{
name = Object.requireNonNullElse(n, "unknown");
...
}
“严格型”方法则是干脆拒绝 null 参数
public Employee(String n, double s, int year, int month, int day)
{
Object.requireNonNull(n, "The name cannot be null");
name = n;
...
}
这种方法有两种好处:
- 异常报告会提供这个问题的描述
- 异常报告会准确地指出问题所在的位置,否则 NullPointerException 异常可能在其他地方出现,而很难追踪到真正导致问题的这个构造器参数。
如果要接受一个对象引用作为构造参数,就要问问自己:是不是真的希望接受可有可无的值。如果不是,那么“严格型”方法更合适。
4.3.7 隐式参数与显式参数
方法用于操作对象以及存取它们的实例字段
public void raiseSalary(double byPercent)
{
double raise = salary * byPercent / 100;
salary += raise;
}
number007.raiseSalary(5);
这个调用执行以下指令:
double raise = number007.salary * 5 / 100;
number007.salary += raise;
raiseSalary 方法有两个参数。第一个参数称为隐式(implicit)参数,是出现在方法名前的类型对象。第二个参数是位于方法名后面括号中的数值,这是一个显式(explicit)参数。(有人把隐式参数称为方法调用的目标或接收者)。
显式参数显式地列在方法声明中,隐式参数没有出现在方法声明中。
在每一个方法中,关键字 this 指示隐式参数。
public void raiseSalary(double byPercent)
{
double raise = this.salary * byPercent / 100;
this.salary += raise;
}
有些程序员更偏爱这样的风格,因为这样可以将实例字段与局部变量明显地区分开来。
在 Java 中,所有的方法都必须在类的内部定义,但并不表示它们是内联方法。是否将某个方法设置为内联方法是 Java 虚拟机的任务。即时编译器会监视那些简短、经常调用而且没有被覆盖的方法调用,并进行优化。
4.3.8 封装的优点
访问器方法
只返回实例字段值,因此又称为字段访问器
想要获得或设置实例字段的值,需要提供下面三项内容
- 一个私有的数据字段
- 一个公共的字段访问器方法
- 一个公共的字段更改器方法
这样做有着下列明显的好处:
首先,可以改变内部实现,而除了该类的方法之外,这不会影响其他代码
第二点好处:更改器方法可以完成错误检查,而只对字段赋值的代码可能没有这个麻烦
不要编写返回可变对象引用的访问器方法
Date 对象是可变的,这一点就破坏了封装性
Employee harry = ...;
Date d = harry.getHireDay();
double tenYearsInMilliSeconds = 10 * 365.25 * 24 * 60 * 60 * 1000;
d.setTime(d.getTime() - (long)tenYearsInMilliSeconds);
出错原因很很微妙。d 和 harry.hireDay 引用同一个对象。对 d 调用更改器方法就可以自动地改变这个 Employee 对象的私有状态
如果需要返回一个可变对象的引用,首先应该对它进行克隆(clone)。对象克隆是指存放在另一个新位置上的对象的副本。
class Employee
{
public Date getHireDay()
{
return (Date)hireDay.clone()
}
}
4.3.9 基于类的访问权限
方法可以访问调用这个方法的对象的私有数据
4.3.10 私有方法
在 Java 中,要实现私有方法,只需将关键字 public 改为 private 即可。
4.3.11 final 实例字段
可以将实例字段定义为 final。这样的字段必须在构造对象时初始化。
必须确保在每一个构造器执行之后,这个字段的值已经设置,并且以后不能再修改这个字段。
final 修饰符对于类型为基本类型或者不可变类的字段尤其有用。(如果类中的所有方法都不会改变其对象,这样的类就是不可变的类。例如,String 类)
对于可变的类,使用 final 修饰符可能会造成混乱
4.4 静态字段与静态方法
4.4.1 静态字段
如果将一个字段定义为 static,每个类只有一个这样的字段。而对于非静态的实例字段,每个对象都有自己的一个副本。
class Employee
{
private static int nextId = 1;
private int id;
}
即使没有 Employee 对象,静态字段 nextId 也存在。它属于类,而不属于任何单个的对象。
静态字段被称为类字段
4.4.2 静态常量
静态变量使用得比较少,但静态常量却很常用。例如,在 Math 类中定义一个静态常量。
可以用 Math.PI 来访问这个变量
省略关键字 static,PI 就变成了 Math 类但一个实例字段。需要通过 Math 类的一个对象来访问 PI,并且每一个 Math 对象都有它自己的一个 PI 副本
4.4.3 静态方法
静态方法是不在对象上执行的方法。例如,Math 类的 pow 方法就是一个静态方法。
Math.pow(x, a);
会计算幂 x ^ a。在万层运算时,它并不使用 Math 对象。换句话说,它没有隐式参数。
可以认为静态方法是没有 this 参数的方法
Employee 类的静态方法不能访问 id 实例字段,因为它不能在对象上执行操作。但是,静态方法可以访问静态字段。
public static int getNextId()
{
return nextId;
}
方法可以省略关键字 static 吗?答案是肯定的。这样一来,需要通过 Employee 类对象的引用来调用这个方法。
可以使用对象调用静态方法,这是合法的。
两种情况下可以使用静态方法:
- 方法不需要访问对象状态,因为它需要的所有参数都通过显式参数提供
- 方法只需要访问类的静态字段
4.4.4 工厂方法
静态方法还有另外一种常见的用途。类似 LocalDate 和 NumberFormat 的类使用静态工厂方法(factory method)来构造对象。
NumberFormat currencyInstance = NumberFormat.getCurrencyInstance();
NumberFormat percentInstance = NumberFormat.getPercentInstance();
double x = 0.1;
System.out.println(currencyInstance.format(x));
System.out.println(percentInstance.format(x));
为什么 NumberFormat 类不利用构造器完成这些操作呢?这主要有两个原因。
- 无法命名构造器。构造器的名字必须与类名相同。但是,这里希望有两个不同的名字,分别得到货币实例和百分比实例。
- 使用构造器时,无法改变所构造对象的类型。而工厂方法实际上将返回 DecimalFormat 类的对象,这是 NumberFormat 的一个子类。
4.4.5 main 方法
main 方法也是一个静态方法
main 方法不对任何对象进行操作。事实上,在启动程序时还没有任何对象。静态的 main 方法将执行并构造程序所需对象。
4.5 方法参数
按值调用(call by value)表示方法接收的是调用者提供的值。按引用调用(call by reference)表示方法接收的是调用者提供的变量地址。方法可以修改按引用传递的变量的值,而不能修改按值传递的变量的值。
以前还有按名调用(call by name),Algol 程序设计语言是最古老的高级程序设计语言之一,它使用的就是这种参数传递方式
Java 程序设计语言总是采用按值调用。方法得到的是所有参数值的一个副本。
有两种类型的方法参数:
- 基本数据类型(数字、布尔值)
- 对象引用
总结以下在 Java 中对方法参数能做什么和不能做什么
- 方法不能修改基本数据类型的参数(即数值型或布尔型)
- 方法可以改变对象参数的状态
- 方法不能让一个对象参数引用一个新的对象
方法可以通过对象引用的副本修改所引用对象的状态
4.6 对象构造
4.6.1 重载
重载(overloading),如果多个方法有相同的名字、不同的参数,便出现了重载。编译器必须挑选出具体调用哪个方法。它用各个方法首部中的参数类型与特定方法调用中所使用的值类型进行匹配,来选出正确的方法。如果编译器找不到匹配的参数,就会产生编译时错误,因为根本不存在匹配,或者没有一个比其他的更好(这个查找匹配的过程被称为重载解析(overloading resolution))
Java 允许重载任何方法,而不只是构造器方法。要完整地描述一个方法,需要指定方法名以及参数类型。这叫作方法的签名(signature)
返回类型不是方法签名的一部分。不能有两个名字相同、参数类型也相同却有不同返回类型的方法。
4.6.2 默认字段初始化
如果在构造器中没有显式地为字段设置初值,那么就会被自动地赋为默认值:数值为 0、布尔值为 false、对象引用为 null。
方法中的局部变量必须明确地初始化。在类中,如果没有初始化类中的字段,将会自动初始化为默认值。
4.6.3 无参数的构造器
很多类都包含一个无参数的构造器,由无参构造器创建对象时,对象的状态会设置为适当的默认值
如果写一个类时没有编写构造器,就会为你提供一个无参构造器。这个构造器将所有的实例字段设置为默认值。
如果类中提供了至少一个构造器,但是没有提供无参数的构造器,那么构造对象时如果不提供参数就是不合法的
仅当类没有任何其他构造器的时候,你才会得到一个默认的无参数构造器
4.6.4 显式字段初始化
可以在类定义中直接为任何字段赋值
class Employee
{
private String name = "";
}
在执行构造器之前先完成这个赋值操作
利用方法调用初始化一个字段
class Employee
{
private static int nextId;
private in id = assignId;
private static int assignId()
{
int r = nextId;
nextId ++;
return r;
}
}
4.6.5 参数名
用单个字母作为参数名
在每个参数前面加上一个前缀“a”
参数变量会遮蔽同名的实例字段
4.6.6 调用另一个构造器
关键字 this 指示一个方法的隐式参数。不过,这个关键字还有另外一个含义。
如果构造器的第一个语句形如 this(...),这个构造器将调用同一个类的另一个构造器。
public Employee(double s)
{
this("Employee #" + nextId, s);
nextId ++;
}
当调用 new Employee(60000) 时,Employee(double) 构造器将调用 Employee(String, double) 构造器
采用这种方式使用 this 关键字非常有用,这样对公共的构造器代码只需要编写一次即可
4.6.7 初始化快
前面已经讲过两种初始化数据字段的方法:
- 在构造器中设置值
- 在声明中赋值
Java 还有第三种机制,称为初始化快(initialization block)。在一个类的声明中,可以包含任意多个代码块。只要构造这个类的对象,这些块就会被执行。
首先运行初始化快,然后才运行构造器的主体部分。
这种机制不是必需的,也不常见。通常会直接将初始化代码放在构造器中。
建议总是将初始化块放在字段定义之后。
调用构造器的具体处理步骤:
- 如果构造器的第一行调用另一个构造器,则基于所提供的参数执行第二个构造器
- 否则
- 所有数据字段初始化为其默认值
- 按照在类声明中出现的顺序,执行所有字段初始化方法和初始化块
- 执行构造器主体代码
可以通过提供一个初始值,或者使用一个静态的初始化块来初始化静态字段
如果类的静态字段需要很复杂的初始化代码,那么可以使用静态的初始化块
将代码放在一个块中,并标记关键字 static
在类第一次加载的时候,将会进行静态字段的初始化
所有静态字段初始化方法以及静态初始化块都将依照类声明中出现的顺序执行
package chapter3.ConstructorTest;
import java.util.Random;
public class ConstructorTest
{
public static void main(String[] args) {
Employee[] staff = new Employee[3];
staff[0] = new Employee("Harry", 40000);
staff[1] = new Employee(60000);
staff[2] = new Employee();
for (Employee employee : staff) {
System.out.println("name = " + employee.getName() + ", id = " + employee.getId() + ", salary = " + employee.getSalary());
}
}
}
class Employee
{
private static int nextId;
private int id;
private String name = "";
private double salary;
static
{
Random generator = new Random();
nextId = generator.nextInt(10000);
}
{
id = nextId;
nextId ++;
}
public Employee(String n, double s)
{
name = n;
salary = s;
}
public Employee(double s)
{
this("Employee #" + nextId, s);
}
public Employee()
{
}
public String getName()
{
return name;
}
public double getSalary()
{
return salary;
}
public int getId()
{
return id;
}
}
4.6.8 对象析构与 finalize 方法
由于 Java 会完成自动的垃圾回收,不需要人工回收内存,所以 Java 不支持析构器
如果一个资源一旦使用完就需要立即关闭,那么应当提供一个 close 方法来完成必要的清理工作。可以在对象使用完时调用这个 close 方法
如果可以等到虚拟机退出,那么可以用方法 Runtime.addShutdownHook 增加一个“关闭钩”(shutdown hook)。在 Java 9 中,可以使用 Cleaner 类注册一个动作,当对象不再可达时(除了清洁器还能访问,其他对象都无法访问这个对象),就会完成这个动作。在实际中这些情况很少见。
不要使用 finalize 方法来完成清理。这个方法原本要在垃圾回收器清理对象之前调用。不过,你并不能知道这个方法到底什么时候调用,而且该方法已经被废弃。
4.7 包
使用包(package)将类组织在一个集合中
4.7.1 包名
使用包的主要原因是确保类名的唯一性
为了确保包名的绝对唯一性,要用一个因特网域名以逆序的形式作为包名,然后对于不同的工程使用不同的子包
类的“完全限定”名
从编译器的角度来看,嵌套的包之间没有任何关系。例如,java.util 包与 java.util.jar 包毫无关系。每一个包都是独立的类集合
4.7.2 类的导入
采用两种方式访问另一个包中的公共类。第一种方式就是使用完全限定名(fully qualified name);就是包名后面跟着类名。
简单且更常用的方式是使用 import 语句。import 语句是一种引用包中各个类的简捷方式。一旦使用了 import 语句,在使用类时,就不必写出类的全名了。
可以使用 import 语句导入一个特定的类或者整个包。import 语句应该位于源文件的顶部。
import java.time.*;
还可以导入一个包中的特定类:
import java.time.LocalDate;
在包中定位类是编译器(compiler)的工作。类文件中的字节码总是使用完整的包名引用其他类。
4.7.3 静态导入
import 语句允许导入静态方法和静态字段,而不只是类
源文件顶部,添加一条指令:
import static java.lang.System.*;
就可以使用 System 类的静态方法和静态字段,而不必加类名前缀:
out.println("Goodbye, World!");
exit(0);
还可以导入特定的方法或字段:
import static java.lang.System.out;
4.7.4 在包中增加类
将类放入包中,就必须将包的名字放在源文件的开头
没有在源文件中放置 package 语句,这个源文件中的类就属于无包名(unnamed package)
将源文件放到与完整包名匹配的子目录中。编译器将类文件也放在相同的目录结构中。
PackageTest 类属于无名包;Employee 类属于 com.horstmann.corejava 包。
想要编译这个程序,只需切换到基目录,并运行命令
javac PackageTest.java
编译器就会自动地查找文件 com/horstmann/corejava/Employee.java 并进行编译
4.7.5 包访问
没有指定 public 或 private,这个部分(类、方法或变量)可以被同一个包中的所有方法访问
对于变量来说就有些不适宜类,变量必须显式地标记为 private,不然的话将默认为包可访问。显然,这样做会破坏封装性。
在默认情况下,包不是封闭的实体。也就是说,任何人都可以向包中添加更多的类。
从 1.2 版开始,JDK 的实现者修改了加载器,明确地禁止加载包名以"java."开头的用户自定义类。
4.7.6 类路径
类文件也可以存储在 JAR 文件中。在一个 JAR 文件中,可以包含多个压缩形式的类文件和子目录,这样既可以节省空间又可以改善性能。
JAR 文件使用 ZIP 格式组织文件和子目录。可以使用任何 ZIP 工具查看 JAR 文件
类路径所列出的目录和归档文件是搜索类的起始点
一个源文件只能包含一个公共类,并且文件名与公共类名必须匹配
4.7.7 设置类路径
最好使用 -classpath(或 -cp,或者 Java 9 中的 --class-path)选项指定类路径:
java -classpath /home/user/classdir
或者
java -classpath c:\classdir
整个指令必须写在一行中
利用 -classpath 选项设置类路径是首选的方法,也可以通过设置 CLASSPATH 环境变量来指定。具体细节依赖于所使用的 shell
4.8 JAR 文件
在将应用程序打包时,你一定希望只向用户提供一个单独的文件,而不是一个包含大量类文件的目录结构,Java 归档(JAR)文件就是为此目的设计的。一个 JAR 文件既可以包含类文件,也可以包含诸如图像和声音等其他类型的文件。JAR 文件是压缩的,它使用了我们熟悉的 ZIP 压缩格式。
4.8.1 创建 JAR 文件
可以使用 jar 工具制作 JAR 文件。创建一个新 JAR 文件最常用的命令使用以下语法:
jar cvf jarFileName file1 file2 ...
例如:
jar cvf CalculatorClasses.jar *.class icon.gif
通常,jar 命令的格式如下:
jar option file1 file2 ...
4.8.2 清单文件
除了类文件、图像和其他资源外,每个 JAR 文件还包含一个清单文件(manifest),用于描述归档文件的特殊特性
清单文件被命名为 MANIFEST.MF,它位于 JAR 文件的一个特殊的 META-INF 子目录中。符合标准的最小清单文件及其内容:
Manifest-Version: 1.0
清单条目被分成多个节。第一节被称为主节(main section)。它作用于整个 JAR 文件。随后的条目用来指定命名实体的属性,如单个文件、包或者 URL。它们都必须以一个 Name 条目开始。节与节之间用空行分开。
4.8.3 可执行 JAR 文件
可以使用 jar 命令中的 e 选项指定程序的入口点,既通常需要在调用 java 程序启动器时指定的类:
jar cvfe MyProgram.jar com.mycompany.mypkg.MainAppClass files to add
可以在清单文件中指定程序的主类,包括以下形式的语句:
Main-Class: com.company.mypkg.MainAppClass
不要为主类名增加扩展名.class
清单文件的最后一行必须以换行符结束。否则,清单文件将无法被正常地读取。
用户可以简单地通过下面的命令来启动程序:
java -jar MyProgram.jar
4.8.4 多版本 JAR 文件
Java 9 引入了多版本的 JAR(multi-release JAR),其中可以包含面向不同 Java 版本的类文件
额外的类文件放在 META-INF/versions 目录中
4.8.5 关于命令行选项说明
Java 开发包(JDK)的命令行选项一直以来都使用单个短横线加多字母选项名的形式
从 Java 9 开始,Java 工具开始转向一种更常用的选项格式,多字母选项名前面加两个短横线,另外对于常用的选项可以使用单字母快捷方式
4.9 文档注释
javadoc 它可以由源文件生成一个 HTML 文档。
如果在源代码中添加以特殊定界符 /** 开始的注释,你也可以很容易地生成一个看上去具有专业水准的文档。
4.9.1 注释的插入
javadoc 实用工具从下面几项中抽取信息:
- 模块
- 包
- 公共类与接口
- 公共的和受保护的字段
- 公共的和受保护的构造器及方法
注释放置在所描述特性的前面。注释以 /** 开始,并以 */ 结束
每个 /** ... */ 文档注释包含标记以及之后紧跟着的自由格式文本(free-form text)。标记以 @ 开始,如 @since 或 @param
自由格式文本的第一句应该是一个概要性的句子。javadoc 工具自动地将这些句子抽取出来生成概要页。
在自由格式文本中,可以使用 HTML 修饰符
要键入等宽代码,需要使用 {@code ...}
4.9.2 类注释
类注释必须放在 import 语句之后,类定义之前
4.9.3 方法注释
每一个方法注释必须放在所描述的方法之前。除了通用标记之外,还可以使用下面的标记:
- @param variable description 这个标记将给当前方法的 parameters 部分添加一个条目
- @return description 这个标记将给当前方法添加 returns 部分
- @throws class description 这个标记将添加一个注释,表示这个方法有可能抛出异常
4.9.4 字段注释
只需要对公共字段(通常指的是静态常量)建立文档
4.9.5 通用注释
标记 @since text 会建立一个 since 条目。text 可以是引入这个特性的版本的任何描述
下面的标记可以用在类文档注释中
- @author name 这个标记将产生一个 author 条目
- @version text 这个标记将产生一个 version 条目
通过 @see 和 @link 标记,可以使用超链接,链接到 javadoc 文档的相关部分或外部文档
标记 @see reference 将在 see also 部分增加一个超链接。它可以用于类中,也可以用于方法中
4.9.6 包注释
包注释,就是需要在每一个包目录中添加一个单独的文件。可以有如下两个选择:
1.提供一个名为 package-info.java 的 Java 文件。这个文件必须包含一个初始化的以 /** 和 */ 界定的 Javadoc 注释,后面是一个 package 语句。它不能包含更多的代码或注释。
2.提供一个名为 package.html 的 HTML 文件。会抽取标记 <body>...</body> 之间的所有文本
4.9.7 注释抽取
如果是一个包,应该运行命令
javadoc -d docDirectory nameOfPackage
如果要为多个包生成文档,运行:
javadoc -d docDirectory nameOfPackage1 nameOfPackage2
如果文件在无名的包中,就应该运行
javadoc -d docDirectory *.java
4.10 类设计技巧
1.一定要保证数据私有
绝对不要破坏封装性
2.一定要对数据进行初始化
Java 不会为你初始化局部变量,但是会对对象的实例字段进行初始化。最好不要依赖于系统的默认值,而是应该显式地初始化所有的数据,可以提供默认值,也可以在所有构造器中设置默认值
3.不要在类中使用过多的基本类型
要用其他的类替换使用多个相关的基本类型。这样会使类更易于理解,也更易于修改
4.不是所有的字段都需要单独的字段访问器和字段更改器
在对象中,常常包含一些不希望别人获得或设置的实例字段
5.分解有过多职责的类
如果明显地可以将一个复杂的类分解成两个更为简单的类,就应该将其分解
6.类名和方法名要能够体现它们的职责
类名应当是一个名词(Order),或者前面有形容词修饰的名词(RushOrder),或者是有动名词(有 -ing 后缀)修饰的名词(BillingAddress)。
访问器方法用小写 get 开头,更改器方法用小写的 set 开头
7.优先使用不可变的类
更改对象的问题在于,如果多个线程试图同时更新一个对象,就会发生并发更改。其结果是不可预料的。如果类是不可变的,就可以安全地在多个线程间共享其对象。