第五章 初始化与清理
目录
Java中采用了构造器,并额外提供了垃圾回收器
5.1 用构造器确保初始化
Java中,通过提供构造器,类的设计者可以确保每个对象都会得到初始化。创建对象时,如果其类具有构造器,Java就会在用户有能力操作对象之前自动调用相应的构造器,从而保证了初始化的进行
创建对象时,将会为对象分配存储空间,并调用相应的构造器
不接受任何参数的构造器饺子默认构造器(Java文档中通常使用术语:无参构造器)
在Java中,初始化和创建捆绑在一起,两者不能分离
构造器是一种特殊类型的方法,因为它没有返回值(与返回值为空(void)的方法不同)
5.2 方法重载
5.2.1 区分重载方法
每个重载方法必须有一个独一无二的参数类型列表
5.2.2 涉及基本类型的重载
基本类型能从一个“较小”的类型自动提升到一个“较大”的类型,此过程一旦牵涉到重载,可能会造成一些混淆
5.2.3 以返回值区分重载方法
根据方法的返回值来区分重载方法是行不通的
5.3 默认构造器
如果已经定义了一个构造器(无论是否有参数),编译器就不会帮你自动构建默认构造器
5.4 this关键字
this关键字只能在方法内部使用,表示对“调用方法的那个对象”的引用
如果在方法内调用同一个类的另一个方法,就不必使用this,直接调用即可
当前方法中的this引用会自动应用于同一类中的其他方法
this关键字对于将当前对象传递给其他方法也很有用
5.4.1 在构造器中调用构造器
可用this关键字做到这点
在构造器中,如果为this添加了参数列表,将产生对符合此参数列表的某个构造器的明确调用;这样,调用其他构造器就有了直接的途径
尽管可以用this调用一个构造器,但却不能调用两个;此外,必须将构造器调用置于最起始处,否则编译器会报错
参数和数据成员名称相同,会产生歧义,用this.s表示数据成员
除构造器外,编译器禁止在其他任何方法中调用构造器
5.4.2 static的含义
static方法就是没有this的方法
在static方法的内部不能调用非静态方法
可以在没有创建任何对象的前提下,仅通过类本身来调用static方法
5.5 清理:终结处理和垃圾回收
特殊情况:假定你的对象(并非使用new)获得了一块“特殊”的内存区域,由于垃圾回收器只知道释放那些经由new分配的内存,所以它不知道该如何释放该对象的这块“特殊”内存。
Java运行在类中定义一个名为finalize()的方法。它的工作原理“假定”是这样的:一旦垃圾回收器准备好释放对象占用的存储空间,将首先调用finalize()方法,并在下一次垃圾回收动作发生时才会真正回收对象占用的内存
- 对象可能不被垃圾回收
- 垃圾回收并不等于“析构”
- 垃圾回收只与内存有关
5.5.1 finalize()的用途何在
使用垃圾回收器的唯一原因是为了回收程序不再使用的内存
所以,对于与垃圾回收有关的任何行为来说(尤其是finalize()方法),它们必须同内存及其回收有关
特殊情况:使用“本地方法”
本地方法是一种在Java中调用非Java方法的方式
5.5.2 你必须实施清理
垃圾回收器的存在并不能完全代替析构函数(而且绝对不能直接调用finalize())
无论是“垃圾回收”还是“终结”,都不保证一定会发生。如果Java虚拟机(JVM)并未面临内存耗尽的情形,它是不会浪费时间去执行垃圾垃圾回收以恢复内存的
5.5.3 终结条件
对象终结条件的验证:
当某个对象不再感兴趣——也就是它可以被清理了,这个对象应该处于某种状态,使它占用的内存可以被安全地释放
只有对象中存在没有被适当清理的部分,程序就存在很隐晦的缺陷
finalize()可以用来最终发现这种情况——尽管它并不总是会被调用
class Book{
boolean checkOut = false;
Book(boolean checkOut) {
this.checkOut = checkOut;
}
void checkIn() {
checkOut = false;
}
protected void finalize() {
if(checkOut)
System.out.println("Error: checked out");
// Normally, you'll also do this:
// super.finalize(); // Call the base-class version
}
}
public class TerminationCondition {
public static void main(String[] args) {
Book novel = new Book(true);
// Proper.cleanup:
novel.checkIn();
// Drop the reference, forget to clean up:
new Book(true);
// Force garbage collection & finalization:
System.gc();
}
}
/* Output:
* Error: checked out
*/
System.gc()用于强制进行终极操作
5.5.4 垃圾回收器如何工作
垃圾回收器对应提高对象的创建速度具有明显的效果
垃圾回收器的介入:工作时,将一面回收空间,一面使堆中的对象紧凑排列,这样“堆指针”就可以很容易移动到更靠近传送带的开始处,也就尽量避免了页面错误
计数:简单但速度很慢
每个对象有一个引用计数器,当有引用连接至对象时+1;当引用离开作用域或被置为null时-1;当引用计数为0是,释放占用空间
问题:对象间的循环引用 =》对象应该被回收但计数不为0
对任何“活”的对象,一定能最终追溯到其存活在堆栈或静态存储区之中的引用
Java虚拟机采用一种自适应的垃圾回收技术
如何处理找到的存活对象
停止—复制:先暂停程序的运行(因为它不属于后台回收模式),然后将所有存活的对象从当前堆复制到另一个堆,没有被复制的全部都是垃圾;当对象被复制到新堆,它们是一个挨着一个的,所以新堆保持紧凑排列,然后就可以按前述方法简单、直接地分配新空间
当把对象从一处搬到另一处时,所有指向它的引用都必须修正
效率会降低(1. 有两个堆;2. 复制)
标记—清扫:要是没有新垃圾产生,就会转换到另一种工作模式(即“自适应”)
从堆栈和静态存储区出发,遍历所有的引用,进而找出所有存活的对象。当它找到一个存活对象,就会给对象设一个标记,这个过程中不会回收任何对象。只有全部标记工作完成,清理动作才会开始。在清理过程中,没有标记的对象将被释放,不会发生任何复制动作
剩下的堆空间是不连续的,如果希望得到连续空间,就得重新整理剩下的对象
对于一般应用而言,速度相当慢;只会产生少量垃圾甚至不产生垃圾时,速度就很快
也必须在程序暂停的情况下进行
“自适应的、分代的、停止—复制、标记—清扫”式垃圾回收器:
在Java虚拟机中,内存分配以较大的“块”为单位。如果对象较大,它就会占用单独的块。有了块以后,垃圾回收器在回收的时候就可以往废弃的块里拷贝对象。每个块都用相应的代数来记录它是否还存活(如果块在某处被引用,代数会增加)。垃圾回收器对上次回收动作后新分配的块进行整理。这对处理大量短命的临时对象很有帮助。垃圾回收器会定期进行完整的清理动作——大型对象仍不会被复制(只是代数增加),内含小型对象的那些块则被复制、整理。Java虚拟机会进行监视,如果所有对象都很稳定,垃圾回收器的效率降低的话,就切换到“标记—清扫”方式;同样,Java虚拟机会跟踪“标记—清扫”的效果,要是堆空间出现很多碎片,就会切换回“停止—复制”方式。这就是“自适应”技术。
Java虚拟机中用于提升速度的附加技术:
“即时”编译器:把程序全部或部分翻译成本地机器码(本来是Java虚拟机的工作)
当需要装载某个类(通常是为该类创建第一个对象)时,编译器会先找的其.class文件,然后将该类的字节码装入内存
两种方案:
1. 让即时编译器编译所以代码
缺陷:加载动作散落在整个程序生命周期内,累加起来要花更多时间;会增加可执行码的长度(字节码要比编译器展开后的本地机器码小很多),将导致页面调度,从而降低程序速度
2. 惰性评估:即时编译器只在必要的时候才编译代码
e.g. Java HotSpot技术
代码每次执行的时候都会做一些优化,所以执行的次数越多,速度越快
5.6 成员初始化
Java尽力保证:所有变量在使用前都能得到恰当的初始化。对于方法的局部变量,Java以编译时错误的形式来贯彻这种保证。
类的每个基本类型数据成员保证都会有一个初始值
在类里定义一个对象引用时,如果不将其初始化,此引用就会得到一个特殊值null
5.6.1 指定初始化
在定义类成员变量的地方为其赋值
5.7 构造器初始化
无法阻止自动初始化的进行,它将在构造器被调用之前发生
5.7.1 初始化顺序
在类的内部,变量定义的先后顺序决定了初始化的顺序。即使变量定义散布于方法定义间,它们仍会在任何方法(包括构造器)被调用之前得到初始化
5.7.2 静态数据的初始化
无论创建多少对象,静态数据都只占用一份存储区域
static关键字不能应用于局部变量,只能作用于域
如果一个域是静态的基本类型域,且没有对它进行初始化,那么它就会活动基本类型的标准初值;如果它是一个对象引用,那么它的默认初始化值就是null
静态初始化只在必要时刻才会进行:只有在第一个对象被创建或者第一次访问静态数据的时候才会初始化;此后,静态对象不会再次被初始化
初始化的顺序是先静态对象,再非静态对象
对象的创建过程:假设有一个Dog对象
- 即使没有显式地使用static关键字,构造器实际上也是静态方法。因此,当首次创建类型为Dog的对象时(构造器可以看成静态方法),或者Dog类的静态方法/静态域首次被访问时,Java解释器必须查找类路径,以定位Dog.class文件
- 载入Dog.class(这将创建一个Class对象),有关静态初始化的所有动作都会执行。因此,静态初始化只在Class对象首次加载的时候进行一次
- 当用new Dog()创建对象的时候,首先在堆上为Dog对象分配足够的存储空间
- 这块存储空间会被清零,这就自动将Dog对象的所有基本数据类型设为默认值,而引用则被设置成了null
- 执行所有出现于字段定义处的初始化动作
- 执行构造器
5.7.3 显式的静态初始化
Java允许将多个静态初始化动作组织成一个特殊的”静态子句“(也称静态块)
这段代码仅执行一次:当首次生成这个类的一个对象,或者首次访问属于那个类的静态数据成员时(即便从未生成过那个类的对象)
5.7.4 非静态实例初始化
class Mug{
Mug(int marker) {
System.out.println("Mug("+marker+")");
}
void f(int marker) {
System.out.println("f("+marker+")");
}
}
public class Mugs {
Mug mug1;
Mug mug2;
{
mug1 = new Mug(1);
mug2 = new Mug(2);
System.out.println("mug1 & mug2 initialized");
}
Mugs() {
System.out.println("Mugs()");
}
Mugs(int i) {
System.out.println("Mugs(int)");
}
public static void main(String[] args) {
System.out.println("inside main()");
new Mugs();
System.out.println("new Mugs() completed");
new Mugs(1);
System.out.println("new Mugs(1) completed");
}
}
/* Output:
* inside main()
* Mug(1)
* Mug(2)
* mug1 & mug2 initialized
* Mugs()
* new Mugs() completed
* Mug(1)
* Mug(2)
* mug1 & mug2 initialized
* Mugs(int)
* new Mugs(1) completed
*/
实例初始化子句:
{
mug1 = new Mug(1);
mug2 = new Mug(2);
System.out.println("mug1 & mug2 initialized");
}
对于支持“匿名内部类”的初始化是必须的
可以保证无论调用了哪个显式构造器,某些操作都会发生
实例初始化子句在构造器之前执行
5.8 数组初始化
数组只是相同类型的、用一个标识符名称封装到一起的一个对象序列或基本类型数据序列
将一个数组赋值给另一个数组:a2 = a1; (只是复制了一个引用)
所以数组都有一个固定成员length,可以通过它获知数组内包含了多少元素,但不能修改
最大下标:length-1
一旦访问下标越界,就会出现运行时错误(即异常)
Arrays.toString()方法属于java.util标准库,将产生一维数组的可打印版本
对于存有对象的数字,如果忘记创建对象,并试图使用数组中的空引用,就会在运行时产生异常
5.8.1 可变参数列表
Java SE5特性
应用于参数个数或类型未知的场合
public class NewVarArgs {
static void printArray(Object... args) {
for(Object obj : args) {
System.out.print(obj+" ");
}
System.out.println();
}
public static void main(String[] args) {
printArray(new Integer(47),
new Float(3.14),
new Double(11.11));
printArray(47, 3.14, 11.11);
printArray("one", "two", "three");
printArray((Object[])new Integer[]{1, 2, 3, 4});
printArray();
}
}
/* Output:
* 47 3.14 11.11
* 47 3.14 11.11
* one two three
* 1 2 3 4
*/
当你指定参数是,编译器会为你填充参数。你获取的仍是一个数组
public class OptionalTrailingArguments {
static void f(int required, String ... trailing) {
System.out.print("required: " + required + " ");
for(String s : trailing) {
System.out.print(s+" ");
}
System.out.println();
}
public static void main(String[] args) {
f(1, "one");
f(2, "two", "three");
f(0);
}
}
/* Output:
* required: 1 one
* required: 2 two three
* required: 0
*/
在可变参数列表中可以使用任何类型的参数,包括基本类型
可变参数列表使得重载过程变得复杂
public class OverloadingVarargs {
static void f(Character ... args) {
System.out.print("first ");
for(Character c : args) {
System.out.print(c+" ");
}
System.out.println();
}
static void f(Integer ... args) {
System.out.print("second ");
for(Integer i : args) {
System.out.print(i+" ");
}
System.out.println();
}
static void f(Long ... args) {
System.out.print("third ");
for(Long l : args) {
System.out.print(l+" ");
}
System.out.println();
}
public static void main(String[] args) {
f('a', 'b', 'c');
f(1);
f(2, 1);
f(0);
f(0L);
//! f(); // won't compile -- ambiguous
}
}
/* Output:
* first a b c
* second 1
* second 2 1
* second 0
* third 0
*/
在每一种情况中,编译器会使用自动包装机制来匹配重载的方法,然后调用最明确匹配的方法
但在不使用参数调用f()时,编译器就无法知道该调用哪个方法了
你应该只是在重载方法的一个版本上使用可变参数列表,或者压根不使用它
5.9 枚举类型
Java SE5
由于枚举类型的实例是常量,通常用大写字母表示(用下划线分割单词)
当你创建enum时,编译器会自动添加一些有用的特性,如:
toString()方法:显示某个enum的名字
ordinal()方法:表示声明次序
static values()方法:安照enum常量的声明次序产生由这些常量值构成的数组