初始化与清理

初始化和清理正是涉及安全的两个问题,Java 对此采用了构造器与垃圾回收器。

构造器

构造器的特点:

  • 构造器的名称必须与类名完全相同
  • 不接受任何参数的构造器称为无参构造器(也称为默认构造器),当类中没有构造器时,编译器自动创建
  • 如果类中有且仅有一个构造器,那么编译器将不会允许以其他任何方式创建类的对象

方法重载

需要注意的方面:

  • 每个重载的方法都必须有一个独一无二的参数类型列表
  • 传入的参数找不到匹配的方法,但是存在较大的类型的重载方法时,会自动转化成较大的类型,反之则会报错
1
2
3
4
5
6
7
8
9
10
11
12
13
class P {

static void f(int c) {
System.out.println("int");
}

}

char c = 5;
P.f(c); // int

double d = 5;
P.f(d); // 不兼容的类型: 从double转换到int可能会有损失

this 关键字

编译器对方法调用所做的幕后工作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class P {

void f(int k) {
System.out.println(k);
}

}

P a = new P();
P b = new P();
a.f(1);
b.f(2);

// 为了确定 f() 方法是被 a 还是被 b 调用,编译器把所操作对象的引用作为第一个参数传递给 f(),变成:
// P.f(a,1)
// P.f(b,2)
// 这是内部表示形式,并不能这样书写代码

this 关键字只能在方法内部使用,表示对调用方法的那个对象的引用。如果在方法内部调用同一个类的另一个方法,就不必使用 this,直接调用即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class P {

void f(int k) {
System.out.println(k);
}

void fk() {
f(0);
}

P getP() {
return this;
}

}

P a = new P();
a.fk(); // 0
System.out.println(a.toString()); // P@1b6d3586
a.getP().fk(); // 0
System.out.println(a.getP().toString()); // P@1b6d3586

通过 this 关键字,可以在一个构造器中调用另一个构造器,但不能调用两个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class P {

int n;
String s;

P(int n) {
this.n = n;
}

P(String s) {
this(10);
this.s = s;
}

}

P a = new P("s");
System.out.println(a.n); // 10
System.out.println(a.s); // s

清理

垃圾回收不保证一定会发生,与内存使用情况等因素有关。

Java 里的对象并非总是被垃圾回收:

  • 对象可能不被垃圾回收
  • 垃圾回收并不等于 C++ 中的析构
  • 垃圾回收只与内存有关

当某些对象并非使用 new 创建时(可能使用本地方法,即调用非 Java 代码),垃圾回收器将无法合适的释放这些对象的内存。为了应对这种情况,Java 允许在类中定义一个名为 finalize() 的方法。它的工作原理:一旦垃圾回收器准备好释放对象占用的存储空间,将首先调用 finalize() 方法,并且在下次垃圾回收时,才会真正回收对象占用的空间。例如:用本地方法调用 C 的 malloc() 函数分配空间,结束时,需要在 finalize() 中用本地方法调用 C 的 free() 函数。

另外,finalize() 还可以被用来验证对象的终结条件,即在 finalize() 方法里判断对象是否处于终结状态。

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
class Book {

boolean check;

Book(boolean check) {
this.check = check;
}

void checked() {
check = true;
}

protected void finalize() { // 在垃圾回收之前,检测书是否被检查
if (!check) {
System.out.println("Error: check is " + check);
}
}
}

Book book = new Book(false); // 新书未被检查
book.checked(); // 新书被检查
new Book(false); // 丢失引用,新书未被检查
System.gc(); // 强制进行终结,进行垃圾回收

// 输出:Error: check is false

垃圾回收

一种简单但速度很慢的垃圾回收技术,引用记数。每个对象都含有一个引用记数器,当有引用连接至对象时,引用记数加 1。当引用离开作用域或被置为 null 时,引用记数减 1。垃圾回收器会在含有全部对象的列表上遍历,当发现某个对象的引用记数为 0 时,释放空间(但是,引用记数模式经常会在记数值变为 0 时,立即释放对象)。缺陷:较难处理循环引用,对象应该被回收,但是引用记数却不为 0。

一种更快的模式,从堆栈和静态存储区开始,遍历所有引用,这些引用对应的便是活的对象。这种模式下衍生出来的技术便是,自适应 垃圾回收技术。包含有两种做法,停止-复制(stop-and-copy)标记-清理(mark-and-sweep):

  • 停止-复制,先暂停程序运行,然后把活的对象从当前堆复制到另一个堆,并且在新堆中保持紧凑排列
  • 标记-清理,当垃圾较少时采用,也是先暂停程序,将活的对象进行标记,全部标记完后,将没有标记的对象清理

即时(Just-In-Time,JIT)编译器,可以把程序全部或者部分翻译成本地机器码。当需要装载某个类(通常是在为该类创建第一个对象)时,编译器会找到其 .class 文件,然后将该类的字节码装入内存。一般采用 惰性评估(lazy evaluation),意思是 JIT 只在必要的时候才编译代码,这样,从不会执行的代码也许压根不会被 JIT 所编译。

初始化

初始化顺序:

  • 在类的内部,变量定义的先后顺序决定了初始化的顺序,即使变量定义散布于方法定义之间,它们仍旧会在任何方法(包括构造器)被调用之前得到初始化
  • 静态初始化只有在必要时刻才会进行(例如第一次访问时)
  • 同时涉及到静态变量和非静态变量,先初始化静态变量

对象的创建过程,假设有一个名为 Dog 的类:

  1. 即使没有显式地使用 static 关键字,构造器实际上也是静态方法。因此,当首次创建类型为 Dog 的对象时(或者 Dog 类的静态方法首次被访问时),Java 解释器必须查找类路径,以定位 Dog.class 文件,然后载入 Dog.class ,有关静态初始化的动作都会执行,且只在 Class 对象首次加载时进行
  2. 当用 new Dog() 创建对象时,首先将在堆上为 Dog 对象分配足够的存储空间,这块存储空间会被清零,这就自动地将 Dog 对象中的所有基本类型数据都设置成了默认值,而引用被设置成 null
  3. 执行所有出现于字段定义处的初始化动作,然后紧接着执行构造器

数组的初始化

初始化列表的最后一个逗号是可选的(便于维护长列表):

1
2
int[] n = new int[]{1, 2, 3,};
System.out.println(Arrays.toString(n)); // [1, 2, 3]

可变参数列表,编译器会使用参数来自动填充数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class P {

static void f(int... ints) {
for (int i : ints) {
System.out.print(i + " ");
}
System.out.println();
}

static void f(Object... objects) {
System.out.println(Arrays.toString(objects));
}
}

P.f(10, 10.5, "aa"); // [10, 10.5, aa]
P.f(new int[]{1, 2, 3}); // 1 2 3
P.f(new Integer[]{1, 2, 3}); // [1, 2, 3],Integer 会自动转型为 Object
P.f((Object[]) new Integer[]{1, 2, 3}); // 显式声明可以移除编译器警告
// P.f(); 某些情况下,编译器无法知道调用哪一个方法

枚举类型

enum 可以很好的与 switch 搭配使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
enum F {
A, B, C
}

F f = F.B;
switch (f) {
case A:
System.out.println(1);
break;
case B:
System.out.println(2);
break;
case C:
System.out.println(3);
}