Chapter 5 Initialize and Cleanup
不安全的编程方式已逐渐成为编程代价高昂的主因之一,初始化(initialize)和清理(cleanup)正是
涉及编程安全的两个关键问题。未初始化的元素会引起各种问题,用完元素一直不清理则会引起资源用尽
的问题。
5.1 Use Constructors to ensure intialization
每个类都应该有一个初始化方法initilize()
,提醒调用者在使用之前先调用该方法进行初始化,但这依赖于调用者的主动调用。
在Java中,通过构造器,确保用户有能力操作对象之前自动调用完毕构造器,保证了初始化。
为了确保构造方法不重名,且能够被编译器识别调用,采用c++语言的解决方法:构造器与类具有有相同的名称。
new
操作在创建对象,为对象分配空间时,将调用相应的构造器,
“初始化”和“创建”捆绑在一起,两者不能分离。
Java通过定义与类同名的构造函数,在new对象时自动调用,以确保对象的初始化。
5.2 Method Overloading
人类语言具有很强的"冗余"性,同样的动词会因为动词对象的不同而表达不同的动作。
Java中也一样,参数和方法名构成了方法标签,同样的方法名不同的参数可以视为不同的方法。
基本类型作为参数列表时,传入的参数首先选择完全符合的方法,若找不到则进行类型提升,只有主动进行类型转换才能降低。
5.3 Default Constructor
若类没有显示定义构造器,则编译器会自动创建一个无参默认构造器;若定义了构造器,编译器就不会创建了。
5.4 This
编译器把所操作对象的引用作为第一个参数this
传给类的方法,所以在方法内部能够通过this
调用其他属性或方法,
并且this
可以省略,编译器会首先认为变量或方法是类的。
this
也能用来表示构造器,但只能处于构造器的第一行。
总结:
- 在对象方法内部使用代表对象的引用。
- 在构造器中表示对象的另一个构造方法(不能循环引用)。
static方法就是没有this
的方法,可以在没有创建对象的前提下调用方法,类似于全局函数。
5.5 finalize
finalize()
会在对象被GC回收的时候执行,不能作为清理对象的方法。
因为:
- Java中对象可能不被垃圾回收(内存充足的话)
- 垃圾回收不等于"折构"
- 垃圾回收应该只与内存有关
finalize
主要用来提供给本地方法(Java调用其他语言)使用来回收内存。
Java中对象的清理一般是不可预料的,由GC来
5.6 GC
Java使用GC(垃圾回收器)来进行内存(主要是堆)的释放。
Java的GC一边回收内存,一边使堆中的对象紧凑排列,已获得更高的分配内存的速度。
GC的任务:
- 快速地回收内存
- 重排对象,提高分配内存的速度
- 减少GC操作的消耗
引用计数法
每个对象都含有一个引用计数器,当被引用时,计数+1;当离开作用域或被置为null时,计数减1。
垃圾回收器会在整个程序生命周期中,对含有全部对象的列表进行遍历,当发现对象引用计数为0就释放空间。
- 简单,但速度慢。
- 定位循环引用的情况消耗大。
- 用来说明垃圾回收,没有被应用于任何一种虚拟机中。
引用网络
思想:还处于工作中的对象的引用构成了"活"的对象引用链,遍历全部引用构成了"活"的对象的网络。
没有处于在网络中的对象就应该被回收,这就解决了循环引用的问题。
停止-复制
将堆分成两块,GC时先暂停程序的运行,从一个活的对象开始,遍历整个堆栈或静态存储区的引用作为应该存活的对象网络。然后将这些对象复制到另一个堆上,重新排列保持紧凑,修正所有对象的引用(映射为新地址)。余下的对象就是垃圾。
- 基于引用网络,解决了循环引用问题。
- 复制时会重新排列对象,使得分配内存的效率提高。
- 维护两个堆的消耗增加,且就算没有垃圾,也会进行复制的操作。
标记-清扫
GC时先暂停程序的运行,从一个活的对象开始,遍历整个堆栈或静态存储区的引用,并标记每个对象。
遍历标记完成后,清理没有被标记的对象。
- 基于引用网络,解决了循环引用问题。
- 没有复制操作。
- 标记清扫后剩下的堆空间是不连续的,需要重新整理。
分代回收
停止-复制方法导致了大量内存复制,特别是大对象的复制降低了程序的效率。
在Java中,对象生命周期处于不均匀的状态,一部分对象存活时间很短,一部分对象将一直存活,这就引入了分代的思想。
分代回收将内存分为不同的代,不同的代GC的频率不同。每次停止-复制时,存活下来的对象的代数将+1,代数越大的对象越被认为是持久对象,进行复制的频率将会降低。而代数低的对象将会被更快的清理,释放空间。
5.7 成员初始化
Java 尽力保证所有变量在使用前都能得到其恰当的初始化。
- 对于局部变量,编译器要求变量使用前必须进行初始化。
- 对于基本数据类型的类成员变量,会被自动赋于一个初始值。
- 对于对象引用的类成员变量,会获得特殊值null
类型 | 值 |
---|---|
char | '\u0000' |
byte | 0 |
short | 0 |
int | 0 |
long | 0L |
float | 0.0f |
double | 0.0 |
boolean | false |
Object | null |
自动初始化:
类型 | 值 |
---|---|
char | '\u0000' |
byte | 0 |
short | 0 |
int | 0 |
long | 0L |
float | 0.0f |
double | 0.0 |
boolean | false |
Object | null |
初始化顺序
类的初始化顺序(同级按从上到下的顺序):
- 父类静态成员自动初始化
- 父类静态初始块、父类静态成员指定初始化
- 子类静态成员自动初始化
- 子类静态初始块、类静态成员指定初始化
- 父类初始块、父类成员变量指定初始化
- 父类构造函数(若没有在构造器里super指定,调用无参构造函数)
- 子类初始块、子类成员变量指定初始化
- 子类构造函数
静态初始化
静态成员的指定初始化和静态初始块会在类第一次使用时执行一次,包括new和调用静态成员或方法。
对象创建过程
创建过程:
- 创建类的对象或访问类的静态方法/静态域时,解释器查找类路径,定位class文件。
- 载入class文件,创建对应的Class对象,执行静态初始化动作。
- 在堆上为对象分配空间,并将空间数值置为0,即自动初始化。、
- 执行指定初始化动作。
- 执行构造函数
5.8 数组初始化
数组只是一个相同类型、用一个标识符名称封装到一起的一个对象序列或基本类型数据序列。
// 初始化表达式
int[] a = {1,2,3};
// 进行自动初始化
int[] b = new int[3];
// 另一种初始化写法
Integer[] c = new Integer[]{
new Integer(1),
new Integer(2),
3
}
可变参数列表
通过数组能够传递可变数量的方法参数
void printArray(Object[] args){
for(Object obj:args){
System.out.println(obj+"");
}
}
// 需要传递数组
printArray(new Object[]{"one",1});
在JAVA5之后,可变参数列表特性添加了进来。
- 调用时会先寻找固定参数的方法,然后才是可变参数方法。
- 可变参数只能位于一般参数后
- 方法寻找顺序:恰好>自动提升>包装>可变参数
void printArray(Object... args){
for(Object obj:args){
System.out.println(obj+"");
}
}
// 编译器会创建数组传递
printArray("one",1);
5.9 枚举
- enum关键字在Java5中添加。
- enmu实际是实现了Enum的类,包含了name和ordinal两个实例域和一些方法。
- 枚举类型具有有限的实例(具名值),即实例都为常量。
- 枚举可以在switch语句中使用。
public abstract class Enum>
implements Comparable, Serializable {
private final String name;
private final int ordinal;
}
5.10 总结
- 再总结了C++由于初始化问题中产生的居多问题后,Java使用一种精巧的初始化机制————构造器来确保正确的初始化。
- 在Java中,由垃圾回收器来自动为对象释放内存,极大地简化了编程工作。
- 垃圾回收器增加了运行时的开销,使得垃圾回收器的实现一直在优化,以提升它的效率和速度。