5.1 用构造器确保初始化
构造器采用与类相同的名称:
class Rock {
Rock() { //This is the constructor
System.out.print("Rock ");
}
}
请注意,由于构造器的名称必须与类名完全相同,所以“每个方法首字母小写”的编码风格并不适用于构造器。
不接受任何参数的构造器叫做默认构造器,Java文档中通常使用术语无参构造器,但是和其他方法一样,构造器也能带有形式参数,以便指定如何创建对象。
构造器是一种特殊类型的方法,因为它没有返回值,这与返回值为空(void)明显不同。
5.2 方法重载
在Java(和C++)里,构造器是强制重载方法名的另一个原因。既然构造器的名字已经由类名所决定,就只能有一个构造器名。那么要想用多种方式创建一个对象该怎么办呢?这就需要多个构造器。由于都是构造器,所以它们必须有相同的名字,即类名。为了让方法名相同而形式参数不同的构造器同时存在,必须用到
方法重载。
5.2.1 区分重载方法
每个重载的方法都必须有一个独一无二的参数类型列表。甚至参数顺序的不同也足以区分两个方法。不过,一般情况下别这么做,因为这会使代码难以维护。
5.2.2 涉及基本类型的重载
如果传入的数据类型(实际参数类型)小于方法中声明的形式参数类型,实际数据类型就会被提升。如果传入的实际参数大于重载方法声明的形式参数,就得通过类型转换来执行窄化转换。
5.2.3 以返回值区分重载方法
在“为了副作用而调用”的情况下,根据方法的返回值来区分重载方法是行不通的。
5.3 默认构造器
默认构造器(无参构造器)是没有形式参数的,它的作用是创建一个“默认对象”。如果你写的类中没有构造器,则编译器会自动帮你创建一个默认构造器。
5.4 this关键字
this关键字只能在方法内部使用,表示对“调用方法的那个对象”的引用。this的用法和其他对象引用并无不同。但要注意,如果在方法内部调用同一个类的另一个方法,就不必使用this,直接调用即可。当前方法中的this引用会自动应用于同一类中的其他方法。
5.4.1 在构造器中调用构造器
可能为一个类写了多个构造器,有时可能想在一个构造器中调用另一个构造器,以避免重复代码。可用this关键字做到这一点。
通常写this的时候,都是指“这个对象”或者“当前对象”,而且它本身表示对当前对象的引用。在构造器中,如果为this添加了参数列表,那么就有了不同的含义。这将产生对符合此参数列表的某个构造器的明确调用;这样,调用其他构造器就有了直接的途径。
5.4.2 static的含义
static方法就是没有this的方法。在static方法的内部不能调用非静态方法,反过来倒是可以的。而且可以在没有创建任何对象的前提下,仅仅通过类本身来调用static方法。这实际上正是static方法的主要用途。它很像全局方法。Java中禁止使用全局方法,但你在类中置入static方法就可以访问其他static方法和static域。
5.5 清理:终结处理和垃圾回收
假定你的对象(并非使用new)获得了一块“特殊”的内存区域,由于垃圾回收器只知道释放那些经由new分配的内存,所以它不知道该如何释放该对象的这块“特殊”内存。为了应对这种情况,Java允许在类中定义一个名为finalize()的方法。它的工作原理“假定”是这样的:一旦垃圾回收器准备好释放对象占用的存储空间,将首先调用其finalize()方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。所以要是你打算用finalize(),就能在垃圾回收时刻做一些重要的清理工作。
这里有一个潜在的编程陷阱,因为有些程序员(特别是C++程序员)刚开始可能会误把finalize()当作C++中的析构函数(C++中销毁对象必须用到这个函数)。所以有必要明确区分一下:在C++中,对象一定会被销毁(如果程序中没有缺陷的话),而Java里的对象却并非总是被垃圾回收。或者换句话说:
1.对象可能不被垃圾回收。
2.垃圾回收并不等于“析构”。
也许你会发现,只要程序没有濒临存储空间用完的那一刻,对象占用的空间就总也得不到释放。如果程序执行结束,并且垃圾回收器一直都没有释放你创建的任何对象的存储空间,则随着程序的退出,那些资源也会全部交还给操作系统。这个策略是恰当的,因为垃圾回收本身也有开销,要是不使用它,那就不用支付这部分开销了。
5.5.1 finalize()的用途何在
3.垃圾回收只与内存有关。
也就是说,使用垃圾回收器的唯一原因是为了回收程序不再使用的内存。所以对于与垃圾回收有关的任何行为来说(尤其是finalize()方法),它们也必须同内存及其回收有关。
5.5.2 你必须实施清理
如果希望进行除释放存储空间之外的清理工作,还是得明确调用某个恰当的Java方法。这就等同于使用析构函数了,只是没有它方便。记住,无论是“垃圾回收”还是“终结”,都不保障一定会发生。如果Java虚拟机(JVM)并未面临内存耗尽的情形,它是不会浪费时间去执行垃圾回收以恢复内存的。
5.5.3 终结条件
通常,不能指望finalize(),必须创建其他的“清理”方法,并且明确地调用它们。看来,finalize()只能存在与程序员很难用到的一些晦涩用法里了。不过,finalize()还有一个有趣的用法,它并不依赖于每次都要对finalize()进行调用,这就是对象
终结条件的验证。System.gc()用于强制进行终结动作。
5.5.4 垃圾回收器如何工作
Java中的垃圾回收器工作时,将一面回收空间,一面使堆中的对象紧凑排列,这样“堆指针”就可以很容易移动到更靠近传送带的开始处,也就尽量避免了页面错误。通过垃圾回收器对对象重新排列,实现了一种高效的、有无限空间可供分配的堆模型。
引用记数是一种简单但速度很慢的垃圾回收技术,每个对象都含有一个引用记数器,当有引用连接至对象时,引用记数加1.当引用离开作用域或被置为null时,引用记数减1.虽然管理引用记数的开销不大,但这项开销在整个程序生命周期中将持续发生。垃圾回收器会在含有全部对象的列表上遍历,当发现某个对象的引用记数为0时,就释放其占用的空间(但是,引用记数模式经常会在记数值变为0时立即释放对象)。这种方法有个缺陷,如果对象之间存在循环引用,可能会出现“对象应该被回收,但引用记数却不为零”的情况。对垃圾回收器而言,定位这样的交互自引用的对象组所需的工作量极大。引用记数常用来说明垃圾收集的工作方式,但似乎从未被应用于任何一种Java虚拟机实现中。
在一些更快的模式中,垃圾回收器并非基于引用记数技术。它们依据的思想是:对任何“活”的对象,一定能最终追溯到其存活在堆栈或静态存储区之中的引用。在这种方式下,Java虚拟机将采用一种
自适应的垃圾回收技术。如何处理找到的存活对象,取决于不同的Java虚拟机实现,有一种做法名为
停止—复制(stop-and-copy)。对于这种所谓的“复制式回收器”而言,效率会降低。一些Java虚拟机会进行检查:要是没有新垃圾产生,就会转换到另一种工作模式(即“自适应”)。这种模式称为
标记—清扫(mark-and-swep)。“标记—清扫”所依据的思路同样是从堆栈和静态存储区出发,遍历所有的引用,进而找出所有存活的对象。
Java虚拟机中有许多附加技术用以提升速度。尤其是与加载器操作有关的,被称为“即时”
(Just-In-Time,JIT)编译器的技术。这种技术可以把程序全部或部分翻译成本地机器码(这本来是Java虚拟机的工作),程序运行速度因此得以提升。当需要装载某个类(通常是在为该类创建第一个对象)时,编译器会先找到其.class文件,然后将该类的字节码装入内存。此时有两种方案可供选择。一种是让即时编译器编译所有代码,另一种做法称为
惰性评估,意思是即时编译器只在必要的时候才编译代码。
5.6 成员初始化
对于方法的局部变量,Java编译时提示错误,要是类的数据成员(即字段)是基本类型,类的每个基本类型数据成员保证都会有一个初始值。在类里定义一个对象引用时,如果不将其初始化,此引用就会获得一个特殊值null。
5.6.1 指定初始化
在定义类成员变量的地方为其赋值(注意在C++里不能这样做)。
5.7 构造器初始化
keyi可以用构造器来进行初始化,但是无法阻止自动初始化的进行,它将在构造器被调用之前发生。
5.7.1 初始化顺序
在类的内部,变量定义的先后顺序决定了初始化的顺序。即使变量定义散步于方法定义之间,它们仍旧会在任何方法(包括构造器)被调用之前得到初始化。
5.7.2 静态数据的初始化
初始化的顺序是先静态对象(如果它们尚未因前面的对象创建过程而被初始化),而后是“非静态”对象。
总结一下对象的创建过程,假设有个名为Dog的类:
1.即使没有显式地使用static关键字,构造器实际上也是静态方法。因此,当首次创建类型为Dog的对象时(构造器可以看成静态方法),或者Dog类的静态方法/静态域首次被访问时,Java解释器必须查找类路径,以定位Dog.class文件。
2.然后载入Dog.class,有关静态初始化的所有动作都会执行。因此,静态初始化只在Class对象首次加载时进行一次。
3.当用new Dog()创建对象的时候,首先将在堆上为Dog对象分配足够的存储空间。
4.这块存储空间会被清零,这就自动地将Dog对象中的所有基本类型数据都设置成了默认值(对数字来说就是0,对布尔型和字符型也相同),而引用则被设置成了null。
5.执行所有出现在字段定义出的初始化动作。
6.执行构造器。
5.7.3 显式的静态初始化
Java允许将多个静态初始化动作组织成一个特殊的“静态子句”(有时也叫做“静态块”)。
public class Spoon {
static int i;
static {
i = 47;
}
}
5.7.4 非静态实例初始化
Java中也有被称为实例初始化的类似语法,用来初始化每一个对象的非静态变量。看起来它与静态初始化子句一模一样,只不过少了static关键字。这种语法对于支持“匿名内部类”的初始化是必须的,但是它也使得你可以保证无论调用了哪个显式构造器,某些操作都会发生。才输出中可以看到实例初始化子句是在两个构造器之前执行的。
5.8 数组初始化
要定义一个数组,只需在类型名后加上一对空方括号即可:int [ ] a1; 方括号也可以置于标识符后面:int a1[ ] ;
编译器不允许指定数组的大小,数组的初始化是由一对花括号括起来的值组成的。在这种情况下,存储空间的分配(等价于使用new)将由编译器负责。如:
int [ ] a1 = { 1, 2, 3, 4, 5};
数组的赋值其实是引用的复制。数据成员length从第0个元素开始计数。
如果在编写程序时,并不能确定在数组里需要多少个元素,可以直接用new在数组里创建元素。
int [ ] a = new int [ rand.nextInt(20) ];
Arrays.toString()方法属于java.util标准类库,它将产生一维数组的可打印版本。
如果你创建了一个非基本类型的数组,那么你就创建了一个引用数组。如果忘记了创建对象,并且试图使用数组中的空引用,就会在运行时产生异常。
也可以用花括号括起来的列表来初始化对象数组。有两种形式:
Integer [ ] a = {
new Integer(1),
new Integer(2),
3,
};
Integer [ ] b = new Integer[ ] {
new Integer(1),
new Integer(2),
3,
};
初始和列表中的最后一个逗号都是可选的。
5.8.1 可变参数列表
可变参数列表与自动包装机制可以和谐共处:
public class AutoboxingVarargs {
public static void f(Integer ... args) {
for (Integer i : args)
System.out.print(i + " ");
System.out.println();
}
public static void main(String[] args) {
f (new Integer(1), new Integer(2));
f (4,5,6,7,8,9);
f (10, new Integer(11), 12);
}
可以在单一的参数列表中将类型混合在一起,而自动包装机制将有选择地将int参数提升为Integer。
可变参数列表使得重载过程变得复杂了。你应该总是只在重载方法的一个版本上使用可变参数列表,或者压根就不使用它。
5.9 枚举类型
public enum Spiciniee {
NOT, MILD, MEDIUM, HOT, FLAMING
}
Spiciness howHot = Spiciness.MEDIUM;
创建enum时,编译器会自动添加一些有用的特性。如toString()方法,以便显示某个enum实例的名字,ordinal()方法用来表示某个特定enum常量的声明顺序,以及static values()方法,用来按照enum常量的声明顺序,产生由这些常量值构成的数组。