五,初始化与清理
1. 用构造器确保初始化
在 Java 中,通过提供构造器,类的设计者可确保每个对象都会得到初始化.创建对象时,如果其类具有构造器,Java 就会在用户有能力操作对象之前自动调用相应的构造器,从而保证了初始化的进行.构造器的名称与类的名称相同.("每个方法首字母小写" 的编码风格并不适用于构造器)
构造器有助于减少错误,并使代码更易于阅读.从概念上讲,"初始化" 与 "创建" 是彼此和独立的.在 Java 中,"初始化" 和 "创建" 捆绑在一起,两者不能分离.
构造器是一种特殊类型的方法,因为它没有返回值.这与返回值为空(void)明显不同.构造器不会返回任何东西(new 表达式确实返回了对新建对象的引用,但构造器本身并没有任何返回值).
2. 方法重载
在 Java 里,构造器是强制重载方法名的一个原因.既然构造器的名字已经由类名所决定,就只能有一个构造器名.那么要想用多种方式创建一个对象该怎么办?假设你要创建一个类,即可以用标准方式进行初始化,也可以从文件里读取信息来初始化.这就需要两个构造器:一个默认构造器,另一个取字符串作为形式参数--该字符串表示初始化对象所需的文件名称.由于都是构造器,所它们必须有相同的名字,即类名.为了让方法名相同而形式参数不同的构造器同时存在,必须用到方法重载.
①区分重载方法
每个重载方法都必须有一个独一无二的参数类型列表.甚至参数顺序的不同也足以区分两个方法.不过,不建议这么做.
②涉及基本类型的重载
基本类型能从一个 "较小" 的类型自动提升至一个 "较大" 的类型,此过程一旦牵涉到重载,可能会造成了一些混淆.
常数值会被当作 int 值处理.
如果传入的数据类型(实际数据类型)小于方法中声明的形式参数类型,数据类型就会被提升.char 类型略有不同,如果无法找到恰好接受 char 参数的方法,就会把 char 直接提升至 int 型.
如果传入的实际参数大于重载方法声明的形式参数,就得通过类型转换来执行窄化转换.如果不这样做,编译器就会出错.
③以返回值区分重载方法
在区分重载方法时,为什么只能以类名和方法的形参列表作为标准呢?能否用方法的返回值来区分呢?
答案是不能.
因为有时,我们并不关心方法的返回值,我们想要的是方法调用的其他效果,这时我们会调用方法而忽略返回值.例如这样调用方法: f();
3. 默认构造器
默认构造器(又名 "无参" 构造器)是没有形式参数的--它的作用是创建一个 "默认对象".如果你写的类中没有构造器,则编译器会自动帮你创建一个默认构造器.但是,如果已经定义了一个构造器(无论是否有参数),编译器就不会自动创建默认构造器.
4.this 关键字
如果有同一类型的两个对象,分别是 a 和 b.你可能想知道,如何才能让这两个对象都能调用同一个方法呢:
如果只有一个 peel 方法,它如何知道是被 a 调用还是被 b 调用呢?
class Banana {void peel(int i) { /*.....*/ } }
public class BananaPeel {
public static void main(String[] args){
Banana a = new Banana(),
b = new Banana();
a.peel(1);
b.peel(2);
}
}
为了能用简便,面向对象的语法来编写代码--即 "发送消息给对象",编译器做了一些幕后工作.它暗自把 "所操作的对象的引用" 作为第一个参数传递给 peel().所以上述两个方法的调用就变成了这样:
这时内部的表示形式.我们并不能这样书写代码.
Banana.peel(a,1);
Banana.peel(b,2);
假设你希望在方法的内部获得对当前对象的引用.由于这个引用是由编译器 "偷偷" 传入的,所以没有标识符可用.但是,为此有个专门的关键字:this.this 关键字只能在方法内部使用,表示对 "调用方法的那个对象" 的引用.this 的用法和其他对象引用并无不同.但要注意,如果在方法内部调用用一个类的另一个方法,就不必使用 this,直接调用即可.
只在必要处使用 this.
①在构造器中使用构造器
可能为了一个类写了多个构造器,有时可能想在一个构造器中调用另一个构造器,以避免重复代码.可用 this 关键字做到这一点.
通常写 this 的时候,都是指 "这个对象" 或 "当前对象",而且它本身表示对当前对象的引用.在构造器中,如果为 this 添加了参数列表,那么就有了不同的含义.这将产生对符合此参数列表的某个构造器的明确调用.另外,必须把构造器调用置于最起始处,否则编译器会报错.
②static 的含义
static 方法就是没有 this 的方法.在 static 方法的内部不能调用非静态方法,反过来倒是可以的.(静态方法可以创建自身的引用,和 this 效果一样,通过这个引用可调用非静态方法,例如 main 方法.)而且可以在没有创建任何对象的前提下,仅仅通过类本身来调用 static 方法.
5. 清理:终结处理和垃圾回收
Java 有垃圾回收器负责回收无用对象占据的内存资源.但也有特殊情况:假定你的对象(并非使用 new)获得了一块 "特殊" 的内存区域,由于垃圾回收器只知道释放那些经由 new 分配的内存,所以它不知道该如何释放该对象的这块 "特殊" 内存.所以,Java 允许在类中定义一个名为 finalize() 的方法.它的工作原理 "假定" 是这样的:一旦垃圾回收器准备好释放对象占用的存储空间,将首先调用 finalize() 方法,并且在下一次垃圾回收动作发生时,才真正回收对象占用的内存.
这里有一个潜在的编程陷阱,因为有些程序员(特别是 C++ 程序员)刚开始可能会误把 finalize() 当作 C++ 中的折构函数(C++ 中销毁对象必须用到这个函数).所以有必要明确区分一下:在 C++ 中,对象一定会被销毁;而 Java 里的对象却并非总是被垃圾回收.或者换句话说:
1. 对象可能不被垃圾回收.
2. 垃圾回收不等于 "折构".
这意味着在你不再需要某个对象之前,如果必须执行某些动作,那么你得自己去做.Java 并未提供 "折构函数" 或相似的概念,要做类似的清理工作,必须自己动手创建一个执行清理工作的普通方法.例如,假设某个对象在创建过程中会将自己绘制到屏幕上,如果不是明确地从屏幕上擦除,它可能永远都得不到清理.如果在 finalize() 里加入某种擦除功能,当 "垃圾回收" 发生时(不能保证一定发生),finalize() 得到了调用,图像就会被擦除.要是 "垃圾回收" 没有发生,图像就会一直保留下来.
. 也许你会发现,只要程序没有濒临存储空间用完的那一刻,对象占用的空间就不会被释放.如果程序执行结束,并且垃圾回收器一直都没有释放你创建的任何对象的存储空间,则随着程序的退出,那些资源也会全部交还给操作系统.这个策略是恰当的,因为垃圾回收本身也有开销,要是不使用它,那就不用支付这部分的开销了.
①finalize() 的用途何在
finalize() 的真正用途是什么呢?
这就引出需要记住的第三点:
3. 垃圾回收只与内存有关.
也就是说,使用垃圾回收器的唯一原因是为了回收程序不用的内存.无论对象是怎样创建的,垃圾回收器都会负责释放对象占据的所以内存.这就将对 finalize() 的需求限制到一种特殊情况,即通过某种创建对象方式以外的方式为对象分配了存储空间.但是,Java 中一切皆为对象,这种特殊情况又是怎么回事呢?
之所以要有 finalize(),是由于在分配内存时可能采用了类似 C 语言中的做法.这种情况主要发生在使用 "本地方法" 的情况下,本地方法是一种在 Java 中调用非 Java 代码的方式.本地方法目前只支持 C 和 C++,但它们可以调用其他语言的代码,所以实际上可以调用任何代码.在非 Java 代码中,可能会调用 C 的 malloc() 函数系列来分配存储空间,而且除非调用了 free() 函数,否则存储空间将得不到释放,从而造成内存泄漏.当然,free() 是 C 和 C++ 中的函数,所以需要在 finalize() 中用本地方法调用它.
②你必须实施清理
要清理一个对象,用户必须在需要清理的时刻调用执行清理动作的方法.
Java 不允许创建局部对象,必须使用 new 创建对象.记住,无论是垃圾回收还是终结,都不保证一定发生.如果 Java 虚拟机并未面临内存耗尽的情形,它是不会浪费时间去执行垃圾回收的.
③终结条件
通常,不能指望 finalize(),必须创建其他的 "清理" 方法,并且明确地调用它们.看来,finalize() 只能存在于程序员很难用到的一些晦涩用法里了.不过,finalize() 还有一个有趣的用法,它并不依赖每次都要对 finalize() 进行调用,这就是对象终结条件的验证.
当对某个对象不再感兴趣--也就是它可以被清理了,这个对象应该处于某种状态,使它占用的内存可以被安全的释放.例如,要是对象代表了一个打开的文件,在对象被回收前程序员应该关闭这个文件.只要对象中存在没有被适当清理的部分,程序就存在很隐晦的缺陷 finalize() 可以用来最终发现这种情况--尽管它并不总是被调用.如果某次 finalize() 的动作使得缺陷被发现,那么就可据此找出问题所在--这才是人们真正关心的.
以下是个简单的例子:
本例的终结条件是:所有的 Book 对象在被当作垃圾回收前都应该被签入(check in).但在 main() 方法中,由于程序员的错误,有一本书未被签入.要是没有 finalize() 来验证终结条件,将很难发现这种缺陷.
class Book {
boolean checkedOut = false;
Book(boolean checkedOut) {
this.checkedOut = checkedOut;
}
void checkIn() {
checkedOut = false;
}
protected void finalize() {
if(checkedOut) {
System.out.println("Error: checked out");
// 一般需要这么做,假设基类版本的 finalize() 也要做某些事情
// 由于需要异常处理,这里省略
// super.fianlize();
}
}
public class TerminationCondition {
public static void main(String[] args) {
Book novel = new Book(true);
novel.checkIn();
new Book(true);
Sysytem.gc();
}
}
注意,System.gc() 用于强制进行终结动作.即使不这么做,通过重复地执行程序,最终也能找出错误的 Book 对象.
④垃圾回收器如何工作
Java 从堆分配空间的速度,可以和其他语言从堆栈上分配空间的速度相媲美.
打个比方,你可以把 C++ 里的堆想象成一个院子,里面每个对象都负责管理自己的地盘.一段时间后,对象可能被销毁,但地盘必须加以重用.在某些 Java 虚拟机中,堆的实现截然不同:它更像一个传送带,每分配一个新对象,它就往前移动一格.这意味着对象存储空间的分配速度非常快.Java 的 "堆指针" 只是简单地移动到尚未分配的区域,其效率比得上 C++ 在堆栈上分配空间的效率.
事实上,Java 中的堆未必完全像传送带那样工作.要真是那样的话,势必会导致频繁的内存页面调度--将其移出移进硬盘,因此会显得需要拥有比实际需要更多的内存.页面调度会显著地影响性能,最终,在创建了足够多的对象之后,内存资源将耗尽.其中的秘密在于垃圾回收器的介入.当它工作时,将一边回收空间,一边使堆中的对象紧凑排列,这样 "堆指针" 就可以很容易移动到更靠近传送带的开始处,也就尽量避免了页面错误.通过垃圾回收器对对象的重新排列,实现了一种高速的,有无限空间可供分配的堆模型.
"自适应的,分代的,停止 - 复制,标记 - 清扫" 式垃圾回收器.
Java 虚拟机中有许多附加技术用以提升速度.尤其是与加载器操作有关的,被称为 "即时" 编译器的技术.这种技术可以把程序全部或部分翻译成本机机器码,程序运行速度得以提升.当需要装载某个类(通常是在为该类创建第一个对象)时,编译器会先找到其. class 文件,然后将该类的字节码装入内存.此时,有两种方案可供选择.一种是就让即时编译器编译所有代码,另一种则为即时编译器只在需要时才编译代码,被称为惰性评估.新版 JDK 中的 Java HotSpot 技术采用了类似方法,代码每次被执行的时候都会做一些优化,所以执行的次数越多,它的速度越快.
6. 成员初始化
Java 尽力保证:所有变量在使用前都能得到恰当的初始话化.对于方法的局部变量,Java 以编译时错误的形式来贯彻这种保证.
7. 构造器初始化
可以用构造器来进行初始化.但要牢记:无法阻止自动初始化的进行,它将在构造器被调用之前发生.
①初始化的顺序
在类的内部,变量定义的先后顺序决定了初始化的顺序.即使变量定义散布于方法定义之间,它们仍旧会在任何方法(包括构造器)被调用之前得到初始化.
②静态数据的初始化
无论创建多少个对象,静态数据都只占用一份存储区域.static 关键字不能应用于局部变量,因此它只能作用于域.
静态初始化只在必要时才会进行.只有在第一次创建对象(或者第一次访问静态数据)的时候,它们才会被初始化.此后,静态对象不会再次被初始化.
初始化的顺序是先静态对象,而后是 "非静态" 对象.
总结一下对象的创建过程,假设有个名为 Dog 的类:
1. 即使没有显式地使用 static 关键字,构造器实际上也是静态方法.因此,当首次创建类型为 Dog 的对象时(构造器可以看成静态方法),或者 Dog 类的静态方法 / 静态域首次被访问时,Java 解释器必须查找类路径,以定位 Dog.class 文件.
2. 然后载入 Dog.class(这会创建一个 Class 对象),有关静态初始化的所有动作都会执行.因此,静态初始化只在 Class 对象首次加载的时候进行一次.
3. 当用 new Dog() 创建对象时,首先在堆上为 Dog 对象分配足够的存储空间.
4. 这块存储空间会被清零,这就自动地将 Dog 对象中的所有基本类型数据都设置成了默认值,而引用则设置成 null.
5. 执行所有出现于字段定义处的初始化动作.
6. 执行构造器.
③显式的静态初始化
Java 允许将多个静态初始化动作组织成一个特殊的 "静态子句"(有时叫做 "静态块"):
与其他静态初始化动作一样,这段代码仅执行一次:当首次生成这个类的一个对象时,或者首次访问属于那个类的静态数据成员时(即使从未生成过那个类的对象).
public class fool {
static int i;
static {
i = 666;
}
}
④非静态实例初始化
Java 中也有被称为实例初始化的类似语法,用来初始化每一个对象的非静态变量.
它与静态初始化子句一模一样,只不过少了 static 关键字.这种语法对于支持 "匿名内部类" 的初始化是必须的.
实例初始化子句是在构造器之前执行的.
8. 数组初始化
数组只是相同类型的,用一个标识符名称封装到一起的一个对象序列或基本类型数据序列.数组是通过方括号下标操作符 [ ] 来定义和使用的.
编译器不允许指定数组的大小.这就又把我们带回有关 "引用" 的问题上.现在拥有的只是对数组的一个引用(你已经为该引用分配了足够的存储空间),而且也没给数组对象本身分配任何空间.可以用由一对花括号括起来的值执行数组的初始化.
也可以用花括号括起来的列表来初始化对象数组.
①可变参数列表
可变参数列表表现形式如下:
void f(int... a){ }
这样就相当于有个 int[] 的参数.有了可变参数列表,就再也不用显式地编写数组语法了,当你指定参数时,编译器实际上会为你去填充数组.你获取的仍旧是一个数组.
注意:你应该总是只在重载方法的一个版本上使用可变参数列表,或者压根就不是用它.
9. 枚举类型
在 Java SE5 中添加了 enum 关键字,它使得我们在需要群组并使用枚举类型时,可以很方便地处理.下面是一个简单的例子:
这里创建了一个名为 Spiciness 的枚举类型,它具有 5 个具名值.由于枚举类型的实例是常量,因此按照命名习惯它们都用大写字母表示.
public enum Spiciness {
NOT, MILD, MEDIUM, HOT, FAMING
}
为了使用 enum,需要创建一个该类型的引用,并将其赋值给某个实例:
枚举类型可以使用 ordinal() 方法,用于表示某个特定 enum 常量的声明顺序,以及 static value() 方法,用来按照 enum 常量的声明顺序,产生这些常量值构成的数组.
public class SimpleEnumUse {
public static void main(String[] args) {
Spiciness howHot = Spiciness.MEDIUM;
Sysytem.out.println(howHot);
}
}
/*output
MEDIUM
*/
尽管 enum 看起来像是一种新的数据类型,但是这个关键字只是为 enum 生成对应的类时,产生了某些编译器行为,因此在很大程度上,你可以将 enum 当作其他任何类来处理.事实上,enum 确实时类,并且具有自己的方法.
enum 可以在 switch 语句内使用.
10. 总结
初始化在 Java 中占有至关重要的低位.
学好内存分析对深刻理解 Java 运行机制十分重要,本文总结的仍旧很不全面,只是取了一部分,任重而道远啊!
来源: http://www.bubuko.com/infodetail-2477774.html