初始化和清理正是涉及安全的两个问题,Java 对此采用了构造器与垃圾回收器。
构造器
构造器的特点:
- 构造器的名称必须与类名完全相同
- 不接受任何参数的构造器称为无参构造器(也称为默认构造器),当类中没有构造器时,编译器自动创建
- 如果类中有且仅有一个构造器,那么编译器将不会允许以其他任何方式创建类的对象
方法重载
需要注意的方面:
- 每个重载的方法都必须有一个独一无二的参数类型列表
- 传入的参数找不到匹配的方法,但是存在较大的类型的重载方法时,会自动转化成较大的类型,反之则会报错
1 | class P { |
this 关键字
编译器对方法调用所做的幕后工作:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17class 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
21class 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
19class 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
25class 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
的类:
- 即使没有显式地使用
static
关键字,构造器实际上也是静态方法。因此,当首次创建类型为Dog
的对象时(或者Dog
类的静态方法首次被访问时),Java 解释器必须查找类路径,以定位Dog.class
文件,然后载入Dog.class
,有关静态初始化的动作都会执行,且只在Class
对象首次加载时进行 - 当用
new Dog()
创建对象时,首先将在堆上为Dog
对象分配足够的存储空间,这块存储空间会被清零,这就自动地将Dog
对象中的所有基本类型数据都设置成了默认值,而引用被设置成null
- 执行所有出现于字段定义处的初始化动作,然后紧接着执行构造器
数组的初始化
初始化列表的最后一个逗号是可选的(便于维护长列表):1
2int[] 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
19class 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
15enum 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);
}