Java核心技术1-对象与类

  |  

摘要: 本文是《Java核心技术 10th》中对象与类的要点总结

【对算法,数学,计算机感兴趣的同学,欢迎关注我哈,阅读更多原创文章】
我的网站:潮汐朝夕的生活实验室
我的公众号:算法题刷刷
我的知乎:潮汐朝夕
我的github:FennelDumplings
我的leetcode:FennelDumplings


本文是《Java核心技术1》第10版 【Chap4 类和对象】 的要点总结。由于此前在 C++ 中已经接触过面向对象编程,这里主要关注 Java 与 C++ 有区别的地方。


面向对象程序设计概述

封装: 形式上就是数据和方法包装套一起。
对象中的数据称为实例域(instance field),操纵数据的过程称为方法(method)。
实现封装的关键在于绝对不能让类中的方法直接地访问其他类的实例域。程序仅通过对象的方法与对象数据进行交互

对象的状态: 对于特定实例,都有一组特定的实例域的值,这些值的集合构成对象当前的状态。

Java 中,所有的类都源自 Object 类。

对象的 3 个主要特性

  1. 对象的行为: 可以对对象施加哪些方法。
  2. 对象的状态: 当施加方法时,对象如何响应。
  3. 对象标识: 如何辨别具有相同行为与状态的不同对象。

类之间的关系

  1. 依赖(“uses”): 如果一个类 A 的方法操纵另一个类 B 的对象,则 A 类依赖于 B 类。
  2. 聚合(“has”): 类 A 的对象包含类 B 的对象
  3. 继承(“is”)

程序员一般采用 UML 画类图来描述类之间的关系。


使用预定义类

不是所有的类都具有面向对象特征,例如 Math 类。我们可以在程序中用 Math 类的方法,但是 Math 类并没有数据,因此也没有生成对象以及初始化实例域的事情。

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

构造器: 一种特殊的方法,名称与类名相同,用来构造并初始化对象,初始化对象后,就可以调用对象的方法了。

例如

1
String s = new Date().toString();

对象变量: 定义对象变量后,可以引用对应类型的对象。但是对象变量本身并不是对象。

1
Date deadline;

定义一个对象变量后,需要初始化才可以调用方法。可以用新构造的对象:

1
deadline = new Date();

也可以引用一个已经存在的对象,此时两个变量引用同一个对象。

1
2
Date birthday;
birthday = deadline;

可以显式地将对象变量设置为 null,表面该对象便来你能够目前没有引用任何变量。但局部变量不会自动地初始化为 null。

1
2
3
deadline = null;
if(deadline != null)
...

通过未初始化的对象变量调用方法,产生编译时错误。
通过值为 null 的对象变量调用方法,产生运行时错误。

Java 对象变量与 C++ 的指针、引用的区别

C++ 没有空引用,且引用不能被赋值。

可以将 Java 对象变量看做 C++ 的对象指针。也就是 Java 的 Date birthday 相当于 C++ 的 Date *birthday。Java 中的 null 引用相当于 C++ 中的 nullptr 指针。

所有 Java 对象都在堆中,当一个对象包含另一个对象变量时,该对象变量依然包含着指向另一个堆对象的指针。

C++ 通过拷贝构造器和赋值操作来实现对象的自动拷贝,在 Java 中,必须使用 clone 方法获得对象的完整拷贝。

OOP设计, 将时间与日历分离: LocalDate 与 Date 类

类设计者将保存时间点给时间点命名分开,所以 Java 标准库中有两个类。

  • Date: 实例有一个状态即特定的时间点,使用距离固定时间点(UTC)的毫秒数表示
  • LocalDate: 处理日历表示法

LocalDate 类的对象一般不用构造器来构造,而是使用静态工厂方法来构造新对象。如下

  • 构造新对象,表示构造此对象时的日期。
1
LocalDate.now();
  • 提供年、月、日构造一个特定日期的对象
1
LocalDate newYearEve = LocalDate.of(1999, 12, 31)

此后,就可以调用 LocalDate 对象的方法了

1
2
3
4
LocalDate newDay = newYearEve.plusDays(1000);
int year = newDay.getYear();
int month = newDay.getMonthValue();
int day = newDay.getDayOfMonth();

访问器与更改器

1
LocalDate newDay = newYearEve.plusDays(1000);

plusDays 方法不改变调用这个方法的对象,而是会生成一个新的对象,然后把该对象赋值给 newDay。这种不改变对象的状态的方法称为访问器方法。例如 toUpperCase 也是保持原字符串不变,返回一个将字符大写的新字符串。

调用后,对象的状态会改变的方法称为更改器方法

C++ 中的 const 与 Java 的访问器

在 C++ 中,带有 const 后缀的方法为访问器方法,默认为更改器方法。但在 Java 汇总,访问器和更改器在语法上没有明显区别。


用户自定义类

通常的类定义形式如下。

1
2
3
4
5
6
7
8
9
10
11
class ClassName {
field1
field2
...
constructor1
constructor2
...
method1
method2
...
}

下面我们设计并实现一个薪金管理系统,并以这个例子看用户自定义类中会遇到的各种问题和概念。

首先是 Employee 类的开发,代码如下,内容框架跟上面的通常的类定义形式一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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);

// 涨薪 5%
for(Employee e: staff)
e.raiseSalary(5);

// 打印所有员工的数据
for(Employee e: staff)
System.out.println(String.format("name=%s, salary=%f, hireDay=%tF",
e.getName(), e.getSalary(), e.getHireDay()));
}
}

class Employee {
// instance fields 实例域
private String name;
private double salary;
private LocalDate hireDay;

// constructor 构造器
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;
}
}

自定义类的使用 (编译和执行)

假设 Employee 这个类已经开发完了,需要实际使用的时候,需要一个带有 public 访问修饰符的 EmployeeTest 类,其中包含 main 方法,且文件名必须与 public 类的名字匹配,也就是 EmployeeTest.java。一个源文件中只能有一个 public 类,但可以有任意数目的非公有类。

1
javac EmployeeTest.java

编译的时候,会在目录下创建两个文件: Employee.class、EmployeeTest.class。将含有 main 方法的类名提供给字节码解释器,就可以启动程序了。

1
java EmployeeTest

也可以将 Employee 和 EmployeeTest 写到两个文件里,分别为 Employee.java 和 EmployeeTest.java,编译的时候依然可以像下面这样。

1
javac EmployeeTest.java

这是因为当 Java 编译器发现 EmployeeTest.java 使用了 Employee 类时,会自动搜索 Employee.class,如果没找到,则搜索 Employee.java 然后对其进行编译。如果 Employee.java 比 Employee.class 版本新,也会自动编译该文件。相当于 java 编译器内置了 make 功能。

自定义类的要点

  • 访问级别

方法和实例域都有访问级别,访问级别共有 4 种。

  • 构造器

总是伴随着 new 的执行被调用。将实例域初始化为所希望的状态。

构造器 C++ 和 Java 的区别

Java 构造器的工作方式与 C++ 一样,但是有个关键区别,所有 Java 对象都是伴随 new 在堆中构造的,而 C++ 不一定要用 new。

  • 方法的参数

方法的参数有两类。

一个是隐式参数(implicit),调用时出现在方法名前的 Employee 类对象,在方法中,this 表示隐式参数。

第二个是显式参数(explicit),调用时位于方法名后面括号中,方法声明时需要明确写出。

关于类外定义方法与内联方法 C++ 和 Java 的区别

C++ 在类内定义方法的话,会自动称为内联方法(inline)。

Java 汇总所有方法必须在类内定义,但不表示它们都是内联方法。是否将某个方法设置为内联方法,是 Java 虚拟机的任务。

  • 封装: 访问器和更改器

实例域的值私有,构造后就没有办法修改了,很多时候需要获得或设置实例域的值。此时就需要公有的访问器和更改器方法。这样做比简单的公有数据域复杂但是好处也有很多。

  1. 可以改变内部实现,而除了该类的方法之外,不影响其他代码
  2. 为了进行新旧数据表示之间的转换,访问器和更改器需要做许多工作,但同时可以将执行错误检查集中在更改器中,方便管理。

注意访问器方法不要返回可变对象,这会破坏封装性。如果返回实例域的可变对象的引用,后续通过该引用调用更改器会影响原对象内的实例域。如果一个方法想返回实例域的可变对象,应该首先进行克隆(clone)。

  • 私有方法

数据域一般设计成私有。方法大多数情况是公有的。

但是有的时候希望将计算代码划分为若干独立的辅助方法,这些辅助方法不应该成为公有接口的一部分,此时就应该设为 private。

方法设计为私有后,不会被外部调用,因此当数据发生变化,或者其它原因导致该方法不再需要,可以直接删掉,而如果是公有的,就不敢删了,因为外面可能调用它。

  • final 实例域

实例域定义为 final,构件对象时必须初始化,且此后这个域的值不能再进行修改。

final 一般应用在基本类型域 (primitive, 8 种),或不可变类的域 (类中每个方法都不会改变其状态),例如 String 类就是一个不可变的类。

而如果将 final 应用在可变的类,虽然也可以,但容易造成混淆。例如 final Employee e = new Employee(),表示对象引用 e 不会再指向其它 Employee 对象,而当前指向的 Employee 对象是可以改变的。


静态域与静态方法

静态域

在大多数面向对象语言中,静态域称为类域,这里的 static 是沿用了 C++ 的叫法,并无实际意义。

1
2
3
4
5
6
7
8
9
class Employee {
private static int nextId = 1;
private int id;

public void setId() {
id = nextId;
nextId++;
}
}

例如上面的代码,Employee 有一个实例域 id 和一个静态域 nextId。每个雇员对象都有一个自己的 id 域,但所有实例共享 nextId 域,它属于类,而不属于任何对象。

下面是静态常量的例子:

1
2
3
public class Math {
public static final double PI = 3.1415926;
}

System.out 也是静态常量,它在 System 类中的声明如下:

1
2
3
public class System {
public static final PrintStream out = ...;
}

由于每个类对象都可以对公有域进行修改,所以最好不要将域设计为 public。然而,公有常量(即 final 域)却没问题。因为 out 被声明为 final,所以不允许再将其他打印流赋给它。

静态方法

静态方法是一种不能向对象实施操作的方法,也就是没有隐式参数 this 的方法。例如 Math.pow

静态方法不能访问实例域,但是可以访问自身类中的静态域,例如

1
2
3
public static int getNextId() {
return nextId;
}

静态方法可以通过类名调用。虽然也可以通过对象调用,但容易出现混淆,还是建议通过类名调用静态方法。

1
Employee.getNextId();

当一个方法满足以下条件时,建议写成静态方法

  • 一个方法不需要访问对象状态,其所需参数都是通过显式参数提供
  • 一个方法只需要访问类的静态域

C++ 和 Java 关于静态域和静态方法的不同

功能上相同,但是写法上不同。

C++ 中,通过 :: 操作符访问自身作用域之外的静态域和静态方法。

关于 static 这个术语的发展历程

起初,C 引入关键字 static 是为了表示退出一个块后依然存在的局部变量。

随后,static 在 C 中有了第二种含义,表示不能被其他文件访问的全局变量和函数。为了避免引入一个新的关键字,关键字 static 被重用了。

最后,C++ 第三次重用了这个关键字,与前面赋予的含义完全不一样,这里将其解释为:属于类且不属于类对象的变量和函数。这个含义与 Java 相同。

工厂方法

静态方法还有一种用途就是静态工厂方法。前面已经见过 LocalDate.nowLocalDate.of 这两个工厂方法。

这里看一个新的:NumberFormat 类也有工厂方法生成不同风格的格式化对象。

1
2
3
4
5
6
7
8
9
10
11
import java.text.NumberFormat;

class x {
public static void main(String[] args) {
NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance();
NumberFormat percentFormatter = NumberFormat.getPercentInstance();
double x = 0.1;
System.out.println(currencyFormatter.format(x));
System.out.println(percentFormatter.format(x));
}
}

打印结果为

1
2
$0.10
10%

使用工厂方法而不用构造器有两个原因

  1. 无法命名构造器,构造器的名字必须与类名相同。
  2. 当使用构造器时,无法改变所构造的对象类型。

main 方法

main 也是一个静态方法。启动程序时,还没有任何一个对象,静态的 main 方法将执行并创建程序所需要的对象。

每一个类可以有一个 main 方法。这是一个常用于对类进行单元测试的技巧。例如,可以在 Employee 类中添加一个 main 方法,此时如果要独立测试 Employee 类,只需要执行 java Employee 即可。

1
2
3
4
5
6
7
class Employee {
public static void main(String[] args) {
Employee e = new Employee("Romeo", 50000, 2003, 3, 31);
e.raiseSalary(10);
System.out.println(e.getName() + " " + e.getSalary());
}
}

方法参数

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

但是注意,方法参数有两种类型:

  • 基本数据类型:数字、布尔值
  • 对象引用

一个方法不可能修改一个基本数据类型的参数,例如

1
2
double x = 10;
func(x);

无论 func 是怎么实现的,调用 func 后,x 还是 10。

但是对象引用作为参数就有可能了,例如

1
2
3
4
5
6
public static void func(Employee x) {
x.raiseSalary(100);
}

Employee harry = new Employee(...);
func(harry);

首先 x 被初始化为 harry 值的拷贝,也就是对象引用的拷贝。

raiseSalary 方法应用于这个对象引用,x 和 harry 同时引用的 Employee 对象的薪资提高了 100%。

方法结束后,参数 x 不再使用。Harry 继续引用那个薪金增加 100% 的对象。

关键点:方法得到的是对象引用的拷贝,对象引用及其拷贝同时引用同一个对象。

C++ 中的参数传递方式:

C++ 提供两种参数传递方式,值传递和引用传递。

Java 中使用对象引用作为参数时,类似于 C++ 中用指针作为参数。但是容易被误解为引用传递。下面以 swap 函数的例子来看看

1
2
3
Employee x = Employee();
Employee y = Employee();
swap(x, y);

我们希望调用 swap 后,x 指向了原来 y 指向的对象;y 指向了原来 x 指向的对象。

在 C++ 中通过引用传递,我们可以轻松实现 swap:

1
2
3
4
5
6
void swap(Employee& e1, Employee& e2)
{
Employee tmp = e1;
e1 = e2;
e2 = tmp;
}

在 Java 中,如果做类似实现

1
2
3
4
5
public static void swap(Employee e1, Employee e2) {
Employee tmp = e1;
e1 = e2;
e2 = tmp;
}

则该方法并没有改变 x 和 y 中的对象引用,也就是它们还是指向原来的对象。这个方法交换的是两个对象引用的拷贝,而不是交换两个对象引用本身。

在 Java 中如果想实现 swap 功能,需要额外的设计和实现。


对象构造

除了简单构造器之外,Java 还提供了很多编写构造器的机制。

重载

很多类有多个构造器,例如 StringBuilder。有多个相同名字、不同参数的方法,则出现了重载。

编译器必须挑选出具体执行哪个方法,它通过用各个方法给出的参数类型与特定方法调用所使用的值类型进行匹配来挑选出相应的方法。如果编译器找不到匹配的参数,就会产生编译时错误。

要完整描述一个方法,需要方法名以及参数类型,称为方法的签名。注意返回值不是方法签名的一部分,也就是不能有两个名字相同,参数类型也相同但是返回不同类型的方法。

默认域初始化

在构造器中没有显式地给域赋予初值,那么就会被自动地赋为默认值:数值为 0、布尔值为 false、对象引用为 null。

注意:方法中的局部变量不会默认初始化,必须明确初始化。

无参数的构造器

如果一个类没有写构造器,则系统会提供一个无参数构造器,会将所有的实例域设置为默认值。

如果类中提供了构造器,但是没有提供无参数的构造器,则构造对象时不提供参数是不合法的。

可以在类定义中,直接将一个值赋给任何域,例如:

1
2
3
class Employee {
private String name = "";
}

这样会在执行构造器之前先执行赋值操作。当一个类所有构造器都希望把相同的值赋予某个实例域时,这种写法很有用。

初始值不一定是常量值,也可以调用方法进行初始化,例如:

1
2
3
4
5
6
7
8
9
10
class Employee {
private static int nextId;
private int id = assignId();

private static int assignId() {
int r = nextId;
nextId++;
return r;
}
}

C++ 和 Java 中关于初始化实例域的区别

在C++中,不能直接初始化类的实例域。所有的域必须在构造器中设置。

但是,有一个特殊的初始化器列表语法,如下

1
2
3
4
5
6
7
Employee::Employee(string n, double s, int y, int m, int d)
:name(n)
,salary(x)
,hireDay(LocalDate.of(y, m, d))
{
...
}

C++使用这种特殊的语法来调用域构造器。在 Java 中没有这种必要,因为对象没有子对象,只有指向其他对象的指针。

调用同一个类的另一个构造器

1
2
3
4
public Employee(double s) {
this("Employee #" + nextId, s);
nextId++;
}

这种写法对于公共的构造器代码只写一次即可。

注意:在 C++ 中一个构造器不能调用另一个构造器,而必须将公共初始化代码写成一个独立的方法。

初始化块

初始化数据域,除了在构造器中设置值和在声明中赋值外,还有第三种机制:初始化块,建议放在域定义之后。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Employee {
private static int nextId;

private int id;
private String name;
private double salary;

// 初始化块
{
id = nextId;
nextId++;
}

public Employee(String n, double s) {
name = n;
salary = s;
}

public Employee() {
name = "";
salary = 0;
}
}

静态初始化块

1
2
3
4
5
class Employee {
// 静态初始化块
static generator = new Random();
nextId = generator.nextInt(10000);
}

调用构造器的具体处理步骤

1) 所有数据域被初始化为默认值(0、false 或 null)。
2) 按照在类声明中出现的次序,依次执行所有域初始化语句和初始化块。
3) 如果构造器第一行调用了第二个构造器,则执行第二个构造器主体。
4) 执行这个构造器的主体。

对象析构与 finalize 方法

由于 Java 有自动的垃圾回收器,不需要人工回收内存,所以 Java 不支持析构器。

某些对象使用了内存之外的其他资源,例如,文件使用了系统资源的另一个对象的句柄。在这种情况下,当资源不再需要时,将其回收和再利用就十分重要。

可以为任何一个类添加 finalize 方法。finalize 方法将在垃圾回收器清除对象之前调用。

注意:在实际应用中,不要依赖于使用 finalize 方法回收任何短缺的资源,这是因为很难知道这个方法什么时候才能够调用。


包的作用是将类组织起来,将自己的代码与别人提供的代码库分开管理。

为了保证包名的绝对唯一性,Sun 公司建议将公司的因特网域名(这显然是独一无二的)以逆序的形式作为包名,并且对于不同的项目使用不同的子包。

java 标注库分布在多个包中,例如

  • java.lang
  • java.util
  • java.net

从编译器的角度来看,嵌套的包之间没有任何关系。例如,java.util 包与 java.util.jar 包毫无关系。每一个都拥有独立的类集合。

类的导入

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

如果要访问另一个包中的公有类,有两种方式,第一个是写出包全名,例如

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

另一种是使用 import 进行简化,import 应该位于 package 语句的后面,可以用 .*,但是写明所导入的类,可读性更好。

1
2
import java.time.*;
LocalDate today = LocalDate.now();

类名冲突

发生类名冲突的时候,需要注意包的名字。例如:

1
2
3
4
import java.util.*
import java.sql.*

Date today;

由于 java.util 和 java.sql 中都有 Date 类,因此在使用 Date 类的时候,会编译错误,那么需要加一个特定 import 解决问题。

1
2
3
4
5
import java.util.*;
import java.sql.*;
import java.util.Date;

Date today;

如果两个 Date 都要使用,则需要在类名前加上完整包名。

1
2
java.util.Date deadline = new java.util.Date();
java.sql.Date today = new java.sql.Date(...);

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

C++ 的 #include 与 Java 的 import 的区别

在 C++ 中,必须使用 #include 将外部特性的声明加载进来,这是因为 C++ 编译器无法查看任何文件的内部,除了正在编译的文件以及在头文件中明确包含的文件。

Java 编译器可以查看其他文件的内部,只要告诉它到哪里去查看就可以了。

在 C++ 中,与包机制类似的是命名空间(namespace)。在 Java 中,packageimport 语句类似于 C++ 中的 namespaceusing 指令。

静态导入

import 除了导入类,还可以导入静态方法和静态域。例如:

1
import static java.lang.System.*;

此后就可以直接使用 System 类的静态方法和静态域,而不用类名前缀:

1
2
out.println("Goodbye, World!"); // System.out
exit(0); // System.exit

将类放入包中

要将一个类放入包中,要将包的名字放在源文件的开头,包中定义类的代码之前。例如:

1
2
3
4
5
package xyz.chengzhaoxi.corejava;

public class Employee {
...
}

如果没有在源文件中放置 package 语句,这个源文件中的类就被放置在一个默认包(defaulf package)中。默认包是一个没有名字的包。

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

从当前目录看,目录结构大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- PackageTest.java
- PackageTest.class
- xyz
- chengzhaoxi
- corejava
- Employee.java
- Employee.class
- ml
- nlp
- Ngram.java
- Ngram.class
- cv
- Cnn.java
- Cnn.class
- Svm.java
- Svm.class

编译当前目录下的 PackageTest.java 命令如下,PackageTest.java 中使用 Employee 类,则编译器会自动查找文件 xyx/chengzhaoxi/corejava/Employee.java 并进行编译。

1
javac PackageTest.java

编译不同包下的类的过程如下,注意编译器对文件进行操作(xyz/ml/nlp/Ngram.java),而 Java 解释器加载类(xyz.ml.nlp.Ngram)。

1
2
javac xyz/ml/nlp/Ngram.java
java xyz.ml.nlp.Ngram

编译器在编译源文件时,是不检查目录结构的,如果目录结果写错了,会在运行时才会发现无法运行。

包作用域

访问修饰符的几种情况:

  • 标记为 public 的部分可以被任意的类使用;
  • 标记为 private 的部分只能被定义它们的类使用。
  • 如果没有指定 public 或 private,这个部分(类、方法或变量)可以被同一个包中的所有方法访问。对于类来说,这种默认是正常的,但是对于变量,这种默认不太好,所以变量最好还是显式第指定 public/private。

类路径

类存储在文件系统的子目录中。类的路径必须与包名匹配。类文件也可以存储在JAR(Java归档)文件中。

在一个 JAR 文件中,可以包含多个压缩形式的类文件和子目录,这样既可以节省又可以改善性能。在程序中用到第三方(third-party)的库文件时,通常会给出一个或多个需要包含的 JAR 文件。

JAR 文件使用 ZIP 格式组织文件和子目录。可以使用所有 ZIP 实用程序查看内部 JAR 文件。

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

1) 确定包树状结构的基目录,然后把类放到一个基目录中。
2) 将 JAR 文件放在一个目录中,例如:/home/user/archives。
3) 设置类路径(class path)。类路径是所有包含类文件的路径的集合。

-classpath 选项设置类路径

1
java -classpath /home/user/classdir:.:/home/user/archives/archive.jar MyProg

也可以设置环境变量

1
export CLASSPARH=/home/user/classdir:.:/home/user/archives/archive.jar

类路径包括以下

  • 基目录: /home/user/classdir
  • 当前目录: .
  • Jar 文件: /home/user/archives/archive.jar

注意:javac 编译器总是在当前的目录中查找文件,但 Java 虚拟机仅在类路径中有 “.” 目录的时候才查看当前目录。如果没有设置类路径,那也并不会产生什么问题,默认的类路径包含“.”目录。然而如果设置了类路径却忘记了包含“.”目录,则程序仍然可以通过编译,但不能运行。


文档注释

JDK 有一个 javadoc 的工具,可以由源文件生成一个 HTML 文档。

在源代码中添加 /** 开始,*/ 结束的注释即可。

javadoc 主要由以下几个特性中抽取信息

  • 公有类与接口
  • 公有的和受保护的构造器及方法
  • 公有的和受保护的域

应该为上面几部分写注释。注释文本中的一些要点如下

1
2
3
4
5
6
标记有 @ 开始,例如 @author, @param
HTML 修饰符,例如 <em></em>,<strong></strong>,<img ...> 等。
不要用 <h1> 或 <hr>,因为会与文档的格式产生冲突。
等宽代码用 {@code ...},而不是 <code></code>,这样就不用考虑对代码中的 < 转义了。
链接,例如图像文件,应该放到子目录 doc-files 中。
<img srd="doc-files/uml.png" alt="UML diagram">

几个通常的注释位置

类注释

1
2
3
4
5
6
7
8
9
10
/**
* 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 {
...
}

方法注释

常用标记

1
2
3
@param 
@return
@throws

域注释

只需要对公有域注释。

1
2
3
4
5
/**
* The "Hearts" Card suit
*/

public static final int HEARTS = 1;

通用注释

常用标记

1
2
3
4
5
6
@author
@version
@since 始于某版本,可以是对引入特性的版本描述
@deprecated 对类,方法或变量添加一个不再使用的注释并给出取代建议
@see 在 see also 部分增加一个超级链接
@link 在任意位置添加超链接

包注释

要想产生包注释,就需要在每一个包目录中添加一个单独的文件。有两种方法:

1) 提供一个以 package.html 命名的 HTML 文件。在标记 ... 之间的所有文本都会被抽取出来。
2) 提供一个以 package-info.java 命名的 Java 文件。这个文件必须包含一个初始的以 /***/ 界定的 Javadoc 注释,跟随在一个包语句之后。它不应该包含更多的代码或注释。

注释的抽取

如果是一个包,则命令如下

1
javadoc -d docDirectory nameOfPackage

如果是默认包,则命令如下

1
javadoc -d docDirectory *.java

可以加 -author 和 -version 选项。

可以加 -link 选项,用来为标准类添加超链接。例如

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

类设计技巧

1. 保证数据私有

数据的表示形式很可能会改变,但它们的使用方式却不会经常发生变化。当数据保持私有时,它们的表示形式的变化不会对类的使用者产生影响,即使出现bug也易于检测。

2. 一定要对数据初始化

Java 不对局部变量进行初始化,但是会对对象的实例域进行初始化。最好不要依赖于系统的默认值。

3. 不要在类中使用过多的基本类型

用其他的类代替多个相关的基本类型的使用。这样会使类更加易于理解且易于修改。

例如,Customer 类有如下实例域

1
2
3
4
private String street;
private String city;
private String state;
private int zip;

用一个 Address 类代替以上实例域,更容易处理地址的变化。

4. 不是所有的域都需要独立的域访问器和域更改器

不希望别人获取或设置的实例域,应该不提供域访问器和域修改器。

5. 将职责过多的类进行分解

什么叫过多依赖个人的理解。下面是一个指责过多的类的例子。

1
2
3
4
5
6
7
8
9
10
public class CardCheck {
private int[] value;
private int[] suit;

public CardCheck() {...}
public void shuffle() {...}
public int getTopValue() {...}
public int getTopSuit() {...}
public void draw() {...}
}

这个类实际上实现了两个独立的概念:一副牌(shuffle 和 draw 方法);一张牌(查看面值,查看花色),可以额外引入表示单张牌的 Card 类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class CardDeck {
private Card[] cards;

public CardDeck() {...}
public void shuffle() {...}
public Card getTop() {...}
public void draw() {...}
}

public class Card {
private int value;
private int suit;

public Card(int value, int aSuit) {...}
public int getValue() {...}
public int getSuit() {...}
}

6. 类名和方法名能体现职责

较好的方法是采用一个名词、前面有形容词或动名词修饰。

7. 优先使用不可变的类

不可变的类,没有方法能修改对象的状态,但是可以有方法返回状态已修改的新对象。非常适合表示值的类,例如 String,时间。


Share