Thinking in java 第五章 - 初始化与清理
随着计算机革命的发展,“不安全”的编程方式已逐渐成为编程代价高昂的主因之一。
1 用构造器确保初始化
构造器的作用就是确保了在你能操作对象之前,它已经被其当地初始化了。
请注意,由于构造器的名称必须与类名完全相同,所以“每个方法首字母小写”的编码风格并不适用构造器。
从概念上来讲,初始化与创建是彼此独立的,在代码中却找不到对initialize() 方法的明确调用。在Java中,初始化和创建捆绑在一起,两者不能分离。
构造器是一种特殊类型的方法,因为它没有返回值。这与返回值为空void明显不同。对于空返回值,尽管方法本身不会自动返回什么,但仍可选择让它返回别的东西。构造器则不会返回任何东西,你别无选择(new表达式确实返回了对新建对象的引用,但构造器本身并没有任何返回值)。假如构造器具有返回值,并且允许人们自行选择返回类型,那么势在必得让编译器知道该如何处理此返回值。
2 方法重载
构造器是强制重载方法名的另一个原因。
要是对明显相同的概念使用了不同的名字,那一定会让人很纳闷。好在有了方法重载,可以为两者使用相同的名字。
2.1 区分重载方法
每个重载的方法都必须有一个独一无二的参数类型列表。
2.2 涉及基本类型的重载
如果传入的数据类型(实际参数类型)小于方法中声明的形式参数类型,实际数据类型就会被提升。char型略有不同,如果无法找到恰好接受char参数的方法,就会把char直接提升至int型。
在这里,方法接受比较小的基本类型作为参数。如果过传入的实际参数过大,就得通过类型转换来执行窄化转换。如果不这样做,编译器就会报错。
2.3 以返回值区分重载方法
根绝方法的返回值来区分重载方法是行不通的。
3 默认构造器
如果你写的类中没有构造器,则编译器会自动帮你创建一个默认构造器。
如果已经定义了一个构造器(无论是否有参数),编译器就不会帮你自动创建默认构造器。
4 this关键字
a.peel(1);
//内部的表现形式
Banana.peel(a,1);
如果在方法内部调用一个类的另一个方法,就不必使用this,直接调用即可。当前方法中的this引用会自动应用于同一类中的去其他方法。
当需要返回对当前对象的引用时,就常常在return后面写上this。
4.1 在构造器中调用构造器
尽管可以用this调用一个构造器,但却不能调用两个。此外,必须将构造器调置于最起始处,否则编译器就会报错。
由于参数s的名称和数据成员s的名字相同,所以会产生歧义。使用this.s来代表数据成员就能解决这个问题。
出构造器外,编译器禁止在其他任何方法中调用构造器。
4.2 static的含义
static方法就是没有this的方法。
5 清理:终结处理和垃圾回收
假定你的对象(并非使用new)获得了一块“特殊”的内存资源,由于垃圾回收器只知道释放那些经由new分配的内存,所以他不知道该如何释放该对象的这块“特殊”内存。
解决方法:再类中定义一个finalize()方法)
工作原理(假定):一旦垃圾回收器准备好释放对象占用的内存,将首先调用其finalize()方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。所以要是你打算用finalize(),就能在垃圾回收时刻做一些重要的清理工作。
- 对象可能不被垃圾回收。
- 垃圾回收并不等于“析构”
5.1 finalize()的用途何在
不该将finalize()作为通用的清理方法,那么fnalize()的真正用途是什么呢?
3. 垃圾回收只与内存有关
使用垃圾回收器的唯一原因是为了回收程序不再使用的内存。所以对于与垃圾回收有关的任何行为来说(尤其是finalize() 方法),它们也必须同内存及其回收有关。
注意:
但是这是否意味着要是对象中含有其他对象,finalize() 就应该明确释放那些对象呢?
不,无论对象是如何创建的,垃圾回收器都会负责释放对象占据的所有内存。这就将对finalize() 的需求限制到一种特殊情况,及通过某种创建对象方式以外的方式为对象分配了存储空间。不过,Java中一切皆为对象,那这种情况是怎么回事呢?
看来之所以要使用finalize() ,是由于再分配内存时可能采用了类似C语言中的做法,而非Java中的通常做法。这种情况主要发生在使用“本地方法”的情况下,本地方法是一种在Java中调用非Java代码的方式。(目前本地方法只支持C和C++,但他们可以调用其他任何语言写的代码,所以实际上可以调用任何代码。)
在非Java代码中,也许会调用C的malloc() 函数系列来分配存储空间,而且除非调用了free() 函数,否则存储空间将得不到释放,从而造成内存泄漏。当然,free() 是C和C++中的函数,所以需要在finalize() 中用本地方法调用它。
它确实不是进行普通的清理工作的合适场所。那么,普通的清理工作应该在哪里执行呢?
5.2 你必须实施清理
Java不允许创建局部对象,必须使用new 创建对象。
可以肤浅的认为由于垃圾回收器的存在,使得Java没有析构函数。但是,随着学习的深入,就会发现垃圾回收器并不能完全代替析构函数。(而且绝对不能直接使用finalize() ,所以这也不是一种解决方案。)如果希望进行除释放存储空间之外的清理工作,还是得明确调用某个恰当的Java方法。这就等同于使用析构函数了,只是没有它方便。
记住,无论是“垃圾回收”还是“终结”,都不保证一定会发生。如果Java虚拟机(JVM)并未面临内存耗尽的情形,它是不会浪费时间去执行垃圾回收以恢复内存的。
5.3 终结条件
finalize() 一般只能用于程序员很难用到的一些晦涩用法里了。不过,finalize() 还有一个有趣的用法,它并不依赖于每次都要对finalize() 进行调用,这就是对象终结条件的验证。
5.4 垃圾回收器如何工作
其他语言在堆上分配对象的代价十分高昂,因此读者自然认为Java所有对象在堆上分配的方式也非常高昂。然而,垃圾回收器对于提高对象的创建速度,却具有明显的效果。听起来很奇怪——存储空间的释放竟然会影响存储空间的分配,但这确实是某些Java虚拟机的工作方式。这也意味着,Java从堆分配空间的速度,可以和其他语言从堆栈上分配空间的速度相媲美。
在某些虚拟机中,堆的实现方式类似于栈分配内存的方式,只是简单地移动Java的堆指针。当然,实际过程中在簿记工作方面还有少量额外开销,但比上查找可用空间开销大。
垃圾回收器详细内容请移步深入理解Java虚拟机。
6 成员初始化
Java尽力保证:所有变量在使用前都能够得到恰当的初始化。对于方法的局部变量,Java以编译时错误的形式来贯穿这种保证。
局部变量必须进行初始化,而类的数据成员都会有一个初始值。
6.1 指定初始化
有一种很直接的办法,就是在定义类成员变量的地方为其赋值(注意在C++里不能这样做,尽管C++的新手们总想这样做。)
程序的正确性取决于初始化的顺序,而与其编译方式无关。所以,编译器恰当地对“向前引用”发出了警告。
这种初始化方法既简单又直观。但是有个限制:每个对象都会具有相同的初值。有时,这正是所希望的,但有时却需要更大的灵活性。
7 构造器初始化
初始化早已得到保证。
无法阻止自动初始化的进行,它将在构造器被调用之前发生。
7.1 初始化顺序
**在类的内部,变量定义的先后顺序决定了初始化的顺序。**即使变量定义散布于方法定义之间,它们仍旧会在任何方法(包括构造器)被调用之前得到初始化。
7.2 静态数据的初始化
无论创建多少个对象,静态数据都只占用一份存储区域。static关键字不能应用于局部变量,因此它只能作用于域。
静态对象只有在第一个对象创建(或者第一次访问静态数据)的时候,它们才会被初始化。伺候,静态对象就不会再次被初始化。
总结一下对象的创建:
- 即使没有显示地使用static关键字,构造器实际上也是静态方法。因此,当首次创建对象时,或者访问其静态方法/静态域首次被访问时Java解释器必须查找类路径,以定位Dog.class文件
- 然后载入Dog.class(后面会学到,这将创建一个Class对象),有关静态初始化的所有动作都会执行。因此,静态初始化只会在Class对象首次加载的时候进行一次
- 当用new Dog()创建对象的时候,首先将在堆上为Dog对象分配足够的存储空间
- 这块存储空间被清零,这就自动地将Dog对象中的所有基本类型数据都设置成了默认值(对数字来说就是0,对布尔型和字符型也相同),而引用则被设置成了null
- 执行所有出现于字段定义出的初始化动作
- 执行构造器
7.3 显示的静态初始化
Java允许将多个静态初始化动作组织成一个特殊的“静态子句”(有时也叫做“静态块”) 与其他静态初始化动作一样,这段代码仅执行一次:当首次生成这个类的一个对象时,或者首次访问属于那个类的静态数据成员时(即便从未生成过那个类的对象)
7.4 非静态实例初始化
与静态初始化子句一模一样,只不过少了static关键字。实例初始化子句是在两个构造器之前执行的。
8 数组初始化
编译器不允许指定数组的大小。现在拥有的只是对数组的一个引用(你已经为该引用分配了足够的存储空间),而且也没给数组对象本身分配任何空间。 为了给数组创建相应的存储空间,必须写初始化表达式。
创建对象数组的时候,刚开始它只是一个引用数组。如果忘记了创建对象,并且视图使用数组中的空引用,就会在运行时产生异常。
8.1 可变参数列表
可变参数列表的参数个数可以为0到n
在Java SE5 之前,可变参数列表采用的都是传递Object[ ] 数组。
Java SE5 新加特性,使用Object… 或者其他类型可变参数String…;有了可变参数,就再也不用显示地编写数组语法了,当你指定参数时,编译器实际上会为你去填充数组。 使用可变参数列表不依赖于自动包装机制,而实际上使用的是基本类型。请注意,你可以在单一的参数列表中将类型混合在一起,而自动包装机制将有选择地将int参数提升为Integer。
在每一种情况中,编译器都会使用自动包装机制来匹配重载的方法,然后调用最明确匹配的方法。你应该总是只在重载方法的一个版本上使用可变参数列表,或者压根就不是用它。
9 枚举类型
在你创建enum时,编译器会自动添加一些有用的特性。tostring()方法显示输出;ordinal()方法表示某个特定的enum常量的而生声明顺序;static values()方法按照enum常量的声明顺序,产生由这些常量值构成的数组。
很大程度上可以将enum当做任何其他的类来处理;事实上,enum确实是类,并且有自己的方法。
由于switch是要在有限的可能值集合中进行选择,因此它与enum正是绝佳的组合。
10 总结
初始化在Java中占有至关重要的地位。 构造器能保证正确的初始化和清理(没有正确的构造器调用,编译器就不允许创建对象),所以有了完全的控制,也很安全。
有些垃圾回收器甚至能清理其他资源,比如图形化和文件句柄。 然而,垃圾回收器确实也增加了运行时的开销。