Java编程思想(五、初始化与清理)
随着计算机革命的发展,“不安全”的编程方式已逐渐成为编程代价高昂的主因之一。
初始化和清理正是涉及安全的两个问题。C++引入了构造器的概念,在创建对象时被自动调用的特殊方法。Java也采用了构造器,并额外提供了“垃圾回收器”。对于不再使用的内存资源,垃圾回收器能自动将其释放。
1、用构造器确保初始化。创建对象时,如果其类具有构造器,Java就会在用户有能力操作对象之前自动调用相应的构造器,从而保证了初始化的进行。不接受任何参数的构造器叫做默认构造器,Java文档中通常使用术语无参构造器。
假设类Tree 有一个构造器,它接受一个整型变量来表示树的高度,那它就能这样创建一个Tree对象。
Tree t = new Tree(12);
如果Tree(int)是Tree类中唯一的构造器,那么编译器将不会允许你以其他任何方式创建Tree对象。
构造器有助于减少错误,并使代码更易于阅读。从概念上讲,“初始化”与“创建”是彼此独立的,然而在上面的代码中,你找不到对initialize()方法的调用。在Java中,“初始化”和“创建”捆绑在一起,两者不能分离。
构造器是一种特殊类型的方法,因为它没有返回值,这与返回值为空明显不同。对于空返回值,尽管方法本身不会自动返回什么,但仍选择让它返回别的东西。构造器则不会返回任何东西你别无选择(new表达式确实返回了对新建对象的引用,但构造器本身并没有任何返回值)。
2、方法重载。当创建一个对象时,也就给此对象分配到的存储空间取了一个名字。所谓方法则是给某个动作取得名字。通过使用名字,你可以引用所有的对象和方法。区分重载方法。即每个重载的方法必须有一个独一无二的参数类型列表。
涉及基本类型的重载。基本类型能从一个“较小”的类型自动提升至一个“较大”的类型。比如传入参数为常数5,某一个重载方法接受int型参数,它就会被调用。如果传入的数据类型(实际参数类型)小于方法中声明的形式参数类型,实际数据类型就会被提升。char型略有不同,如果无法找到恰好接受char参数的方法,就会把char直接提升至int型。 方法接受较小的基本类型作为参数,如果传入的实际参数比较大,则得听过类型转换来执行窄化转换(显式进行)。否则编译器则会报错。
以返回值区分重载方法是不可行的。比如有时候我们只是调用方法。比如f();并不需要任何返回值,这种情况下,编译器无法靠返回值来分辨我所使用的的具体是那一个方法。
3、默认构造器。如果写的类中没有构造器,则编译器会自动帮你创建一个默认的构造器。如果已经定义了一个构造器(无论是否有参数),编译器就不会自动帮你创建默认构造器。
4、this关键字。this关键词只能在方法内部使用,表示对“调用方法的那个对象”的引用。this的用法和其他对象引用并无不同。如果在方法内部调用同一个类的另一个方法,就不必使用this,直接调用即可。
public class User{ void pick(){}; void pit(){pick();} }
只有当需要明确指出对当前对象的引用时,才需要使用this关键字。传递当前对象给其他方法时,this关键字也很有用。
class Person { public void eat(Apple apple){ Apple peeled = apple.getPeeled();
System.out.println("Yummy"); } }
class Peeler{
static Apple peel(Apple apple){
return apple;
}
}
class Apple {
Apple getPeeled(){
return Peeler.peel(this);
}
}
public class PassingThis{
public static void main(String[] args){
new Person().eat(new Apple());
}
}
5、在构造器中调用构造器。有时候为了一个类写了多个构造器。有时候可能想在一个构造器中调用另一个构造器,以避免重复代码。可用this关键字做到这一点。
public class Flower{
Flower(int petals){}
Flower(String ss){}
Flower(String s,int petals){
this(petals);
// this(s); 尽管可以用this调用一个构造器,但却不能调用两个。此外,必须将构造器调用最起始处,否则编译器会报错
}
}
有时候,传入的形参的名称和类的数据成员的名称相同时,就可以使用this来解决这个歧义的问题。
以及,除了构造器以外,编译器禁止在其他地方调用构造器。
6、static的含义。static就是没有this的方法。在static方法的内部不能调用非静态方法。而且可以在没有创建任何对象的前提下,仅仅通过类本身来调用static方法。static方法具有全局函数的语义。使用static方法时,由于不存在this,所以不是通过“面向对象发送消息”的方式来完成的。如果在代码中出现了大量的static方法,就该重新考虑自己的设计了。
7、清理:终结处理和垃圾回收。假定你的对象(并非使用new)获得了一块“特殊”的内存区域。由于垃圾回收器只知道释放那些经由new分配的内存,所以它不知道该如何释放该对象的这块“特殊”内存。为了应对这种情况,Java允许在类中定义一个名为finalize()的方法。它的工作原理“假定”是这样的:一旦垃圾回收器准备释放好对象占用的存储空间,将先调用其finalize()方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。所以你要是打算使用finalize(),就能在垃圾回收时刻做一些重要的清理工作。
这里存在一个编程陷阱:finalize()并不是C++中的析构函数。在C++中,对象一定会被销毁(如果程序没有缺陷)。但是Java中的对象却并非总是被垃圾回收。也就是说:在Java中,1、对象可能不被垃圾回收、2、垃圾回收不等于“析构”。只要程序没有濒临存储空间用完的那一刻,对象占用的空间就总也得不到释放。如果程序执行结束,并且垃圾回收器一直都没有释放你创建的任何对象的存储空间,则随着程序的退出,那些资源也将全部交还给操作系统。因为垃圾回收本身也有开销,要是不使用它,就不必支付这部分开销了。
垃圾回收只与内存有关。使用垃圾回收器的唯一原因是为了回收程序不再使用的内存。本地方法可以让java调用非java代码。本地方法现在只支持C和C++。但是C和C++又可以调用其它语言写的代码。所以实际上可以调用任何代码。在非Java代码中,也许会调用C的malloc()函数系列来分配存储空间,而且除非使用free()函数,否则存储空间得不到释放,从而造成内存泄漏。所以要在finalize()中用本地方法调用它。
Java虚拟机并未面临内存耗尽的情形,它是不会浪费时间去执行垃圾回收以恢复内存的。如果你想看一下垃圾回收时的情形,那可以使用System.gc(),用于强制进行终结动作。
8、垃圾回收器是如何工作的。一般来说,在堆上分配对象的代价是十分高昂的。然而,垃圾回收器对于提高对象的创建速度,却有明显的效果。垃圾回收器的介入,当他工作时,将一面回收空间,一面使堆中的对象紧凑排列,这样“堆指针”就可以很容易移动到更靠近传送带(某些Java虚拟机中,堆的实现很像传送带,每分配一个新对象,就往前移动一格)的开始处。
如何判定对象已经死了呢?垃圾回收器是这样判断的:对任何“活”的对象,一定能最终追溯到其存活在堆栈或静态存储区之中的引用。这个引用链条可能会穿过数个对象层次。由此,如果从堆栈和静态存储区开始,遍历所有的引用,就能找到所有“活”的对象。对于发现的每个引用,必须追踪它所引用的对象,然后是此对象包含的所有引用。 如此反复进行,知道“根源于堆栈和静态存储区的引用”所形成的网络全部被访问为止。
如何处理找到的存活对象,取决于不同的Java虚拟机实现。有一种名为 停止--复制。先暂停程序的运行,然后将所有存活的对象从当前堆复制到另一个堆。没有被复制的全部都是垃圾。当对象被复制到新堆时,它们是一个挨着一个的,以新堆保持紧凑排列。但有时候这个方法并不好用,首先它就必须要有两个堆,这样就浪费了不少空间。其次是在程序稳定之后,只会产生少量垃圾,但是复制还是得将所有内存从一处复制到另一处。为了避免这种情况,要是没有新垃圾产生,Java虚拟机就会转换到另一种模式,这种模式称之为 标记--清扫。它的思路同样是从堆栈和静态存储区出发,遍历所有的引用,进而找出所有存活的对象。每当它找到一个存活对象,就会给对象设一个标记。这个过程中不会回收任何对象。只有全部标记工作完成的时候,清理动作才会开始。在清理过程中,没有标记的对象将被释放,不会发生任何复制工作。所以剩下的堆空间是不连续的。垃圾回收器要是希望能到连续空间的话,就得重新整理一遍。严格来说,停止--复制 要求在释放旧对象前,必须先把所有存活对象从旧堆复制到新堆。但有些对象占用内存比较大,于是就会占用单独的块。有了块之后,垃圾回收器在回收的时候就可以往废弃的块里拷贝对象了。每个块都用相应的代数来记录它是否还存活。在清理过程中,大型对象并不会被复制(只是其代数会增加),内含小型对象的那些块则被复制并整理。
Java虚拟机中还有许多附加技术用以提升速度。尤其是与加载器操作有关的。被称为“即时”编译器的技术。这种技术可以把程序全部或部分翻译成本地机器码,程序运行速度因此可以提升。当需要装载某个类时,编译器会先找到其.class文件。然后将该类的字节码装入内存。此时,有两种方案可供选择,一种是就让即时编译器编译所有代码,但是这种做法有两个缺陷,这种加载动作散落在这个程序生命周期内,累加起来要花更多时间,并且会增加可执行代码的长度,这将导致页面调度,从而降低程序速度。另一种做法称为惰性评估,意思是即时编译器只在必要的时候才编译代码。这样,从不会被执行的代码也许压根就不会被“即时”编译器所编译。新版JDK中的Java HotSpot技术就采用了类似的方法,代码每次被执行的时候都会做一些优化,所以执行的次数越多,它的速度就越快。
9、成员初始化。初始化的顺序是,先静态对象,而后是“非静态对象”。
构造器初始化。可以使用构造器进行初始化,但是自动初始化将会在构造器被调用之前发生。
静态数据的初始化。无论创建多少个对象,静态数据都只占用一份存储区域。static关键字不能应用于局部变量,因此它只能作用于域。如果一个域是静态的基本类型域,且也没有对它进行初始化。那么它会获得基本类型的标准初值。如果它是一个对象引用,那么它的默认初始值就是null。静态初始化动作只进行一次:当首次生成这个类的一个对象时,或者首次访问属于这个类的静态成员时(即便从未生成过那个类的对象)。
数组初始化。数组只是相同类型的、用一个标识符名称封装到一起的一个对象序列或基本类型数据序列。拥有的只是对数组的一个引用。数组的创建是在运行时刻进行的。可变参数列表。提供了一种方便的语法来创建对象并调用方法。由于所以的类都直接或间接继承于Object类,所以可以创建以Object数组为参数的方法。
枚举类型。当创建枚举时,编译器会自动添加一些有用的特性,比如toString()方法,显示某个enum实例的名字。ordinal()方法,用来表示某个特定enum常量的声明顺序。static values(),用来按照enum常量的声明顺序,产生由这些常量值构成的数组。