Thinking-In-Java——初始化和清除

By timebusker on May 4, 2018

“随着计算机的进步,‘不安全’的程序设计已成为造成编程代价高昂的罪魁祸首之一。”

“随着计算机的进步,‘不安全’的程序设计已成为造成编程代价高昂的罪魁祸首之一。”

“初始化”和“清除”是这些安全问题的其中两个。许多 C程序的错误都是由于程序员忘记初始化一个变量 造成的。对于现成的库,若用户不知道如何初始化库的一个组件,就往往会出现这一类的错误。清除是另一 个特殊的问题,因为用完一个元素后,由于不再关心,所以很容易把它忘记。这样一来,那个元素占用的资 源会一直保留下去,极易产生资源(主要是内存)用尽的后果。

C++为我们引入了“构建器”的概念。这是一种特殊的方法,在一个对象创建之后自动调用。Java 也沿用了 这个概念,但新增了自己的“垃圾收集器”,能在资源不再需要的时候自动释放它们。本章将讨论初始化和 清除的问题,以及Java 如何提供它们的支持。

用构建器自动初始化

对于方法的创建,可将其想象成为自己写的每个类都调用一次 initialize()。这个名字提醒我们在使用对象 之前,应首先进行这样的调用。但不幸的是,这也意味着用户必须记住调用方法。在 Java 中,由于提供了名 为“构建器”的一种特殊方法,所以类的设计者可担保每个对象都会得到正确的初始化。若某个类有一个构 建器,那么在创建对象时,Java 会自动调用那个构建器——甚至在用户毫不知觉的情况下。所以说这是可以 担保的!
接着的一个问题是如何命名这个方法。存在两方面的问题。第一个是我们使用的任何名字都可能与打算为某 个类成员使用的名字冲突。第二是由于编译器的责任是调用构建器,所以它必须知道要调用是哪个方法。C++ 采取的方案看来是最简单的,且更有逻辑性,所以也在Java 里得到了应用:构建器的名字与类名相同。这样 一来,可保证象这样的一个方法会在初始化期间自动调用。

方法过载(重载)

在任何程序设计语言中,一项重要的特性就是名字的运用。我们创建一个对象时,会分配到一个保存区域的 名字。方法名代表的是一种具体的行动。通过用名字描述自己的系统,可使自己的程序更易人们理解和修 改。它非常象写散文——目的是与读者沟通。

我们用相同的词表达多种不同的含义——即词的“过载”。

区分过载方法

若方法有同样的名字,Java 怎样知道我们指的哪一个方法呢?这里有一个简单的规则:每个过载的方法都必 须采取独一无二的自变量类型列表。

方法的签名( signature)由它的名称和所有参数类型组成;签名不包括它的返回类型。

this 关键字(注意只能在方法内部使用) 可为已调用了其方法的那个对象生成相应的句柄。可象对待其他任何对象句柄一样对待这个句柄。但要注 意,假若准备从自己某个类的另一个方法内部调用一个类方法,就不必使用this。只需简单地调用那个方法 即可。当前的this 句柄会自动应用于其他方法。

若为一个类写了多个构建器,那么经常都需要在一个构建器里调用另一个构建器,以避免写重复的代码。可 用this 关键字做到这一点。
通常,当我们说this 的时候,都是指“这个对象”或者“当前对象”。而且它本身会产生当前对象的一个句 柄。在一个构建器中,若为其赋予一个自变量列表,那么 this 关键字会具有不同的含义:它会对与那个自变 量列表相符的构建器进行明确的调用。

class object1 {
    public void dopritln() {
        System.out.println("\t1.....\t" + this.hashCode());
    }

    public void dopritlns() {
        object2 obj = new object2();
        obj.dopritln();
    }

    @Override
    public int hashCode() {
        return 11111;
    }
}

class object2 {
    public void dopritln() {
        System.out.println("\t2.....\t" + this.hashCode());
    }

    public void dopritlns() {
        object3 obj = new object3();
        obj.dopritln();
    }

    @Override
    public int hashCode() {
        return 22222;
    }
}

class object3 {
    public void dopritln() {
        System.out.println("\t3.....\t" + this.hashCode());
    }

    public void dopritlns() {
        object1 obj = new object1();
        obj.dopritln();
    }

    @Override
    public int hashCode() {
        return 33333;
    }
}

// *******************************************
    @org.junit.Test
    public void testThis() {
        object1 obj1 = new object1();
        object2 obj2 = new object2();
        object3 obj3 = new object3();
        System.out.println("\t0.....\t" + this.hashCode());
        obj1.dopritln();
        obj2.dopritln();
        obj3.dopritln();
        System.out.println("************************");
        obj1.dopritlns();
        obj2.dopritlns();
        obj3.dopritlns();
    }
	
// *******************************************  
	0.....	254413710
	1.....	11111
	2.....	22222
	3.....	33333
************************
	2.....	22222
	3.....	33333
	1.....	11111	

清除:收尾和垃圾收集

Java 可用垃圾收集器回收由不再使用的对 象占据的内存。现在考虑一种非常特殊且不多见的情况。假定我们的对象分配了一个“特殊”内存区域,没 有使用new。垃圾收集器只知道释放那些由new分配的内存,所以不知道如何释放对象的“特殊”内存。为 解决这个问题,Java 提供了一个名为finalize()的方法,可为我们的类定义它。在理想情况下,它的工作原 理应该是这样的:一旦垃圾收集器准备好释放对象占用的存储空间,它首先调用finalize(),而且只有在下 一次垃圾收集过程中,才会真正回收对象的内存。所以如果使用finalize(),就可以在垃圾收集期间进行一 些重要的清除或清扫工作。

垃圾收集只跟内存有关!

也就是说,垃圾收集器存在的唯一原因是为了回收程序不再使用的内存。所以对于与垃圾收集有关的任何活 动来说,其中最值得注意的是finalize()方法,它们也必须同内存以及它的回收有关。 但这是否意味着假如对象包含了其他对象,finalize()就应该明确释放那些对象呢?答案是否定的——垃圾 收集器会负责释放所有对象占据的内存,无论这些对象是如何创建的。它将对finalize()的需求限制到特殊 的情况。在这种情况下,我们的对象可采用与创建对象时不同的方法分配一些存储空间。

成员初始化

Java 尽自己的全力保证所有变量都能在使用前得到正确的初始化。若被定义成相对于一个方法的“局部”变 量,这一保证就通过编译期的出错提示表现出来。

关于类成员变量,由于任何方法都可以初始化或使用那个数据,所以在正式使用数据前,若还是强迫程序员将其初始化成一个适当的值, 就可能不是一种实际的做法。然而,若为其赋予一个垃圾值,同样是非常不安全的。因此,一个类的所有基本类型数 据成员都会保证获得一个初始值

规定初始化

如果想自己为变量赋予一个初始值,又会发生什么情况呢?为达到这个目的,一个最直接的做法是在类内部 定义变量的同时也为其赋值。

这种初始化方法非常简单和直观。它的一个限制是类型Measurement的每个对象都会获得相同的初始化值。 有时,这正是我们希望的结果,但有时却需要盼望更大的灵活性。

构建器初始化

可考虑用构建器执行初始化进程。这样便可在编程时获得更大的灵活程度,因为我们可以在运行期调用方法 和采取行动,从而“现场”决定初始化值。但要注意这样一件事情:不可妨碍自动初始化的进行,它在构建 器进入之前就会发生。

  • 初始化顺序
    在一个类里,初始化的顺序是由变量在类内的定义顺序决定的。即使变量定义大量遍布于方法定义的中间, 那些变量仍会在调用任何方法之前得到初始化——甚至在构建器调用之前。

  • 静态数据的初始化
    若数据是静态的(static),那么同样的事情就会发生;如果它属于一个基本类型(主类型),而且未对其 初始化,就会自动获得自己的标准基本类型初始值;如果它是指向一个对象的句柄,那么除非新建一个对 象,并将句柄同它连接起来,否则就会得到一个空值(NULL)。

如果想在定义的同时进行初始化,采取的方法与非静态值表面看起来是相同的。但由于static 值只有一个存
储区域,所以无论创建多少个对象,都必然会遇到何时对那个存储区域进行初始化的问题。

  • 明确进行的静态初始化(静态代码块) 静态代码块仅执行一次——首次生成那个类的一个对象时,或者首次访问属于那个类的一个 static 成员时 (即便从未生成过那个类的对象)。

  • 非静态实例的初始化
    它看起来与静态代码块极其相似,只是static 关键字从里面消失了。为支持对“匿名内部类”的初始化 ,必须采用这一语法格式。

    {
     c1 = new Mug(1);
     116
     c2 = new Mug(2);
     System.out.println("c1 & c2 initialized");
    }
    

类的初始化总结

以一个名为 Dog的类为例:

  • (1) 类型为 Dog的一个对象首次创建时,或者Dog 类的static方法/static 字段首次访问时,Java 解释器必须找到Dog.class(在事先设好的类路径里搜索)。
  • (2) 找到Dog.class 后(它会创建一个 Class对象),它的所有 static初始化模块都会运行。因此,static初始化仅发生一次——在 Class 对象首次载入的时候(加载class时)。
  • (3) 创建一个new Dog()时,Dog 对象的构建进程首先会在内存堆(Heap)里为一个 Dog对象分配足够多的存储空间。
  • (4) 这种存储空间会清为零,将Dog中的所有基本类型设为它们的默认值(零用于数字,以及 boolean和char 的等价设定)。
  • (5) 进行字段定义时发生的所有初始化都会执行。
  • (6) 执行构建器。这实际可能要求进行相当多的操作,特别是在涉及继承的时候。

数组初始化

数组代表一系列对象或者基本数据类型,所有相同的类型都封装到一起——采用一个统一的标识符名称。数 组的定义和使用是通过方括号索引运算符进行的([])。为定义一个数组,只需在类型名后简单地跟随一对 空方括号即可:int[] al;

总结

  • 作为初始化的一种具体操作形式,构建器应使大家明确感受到在语言中进行初始化的重要性。与 C++的程序 设计一样,判断一个程序效率如何,关键是看是否由于变量的初始化不正确而造成了严重的编程错误(臭 虫)。这些形式的错误很难发现,而且类似的问题也适用于不正确的清除或收尾工作。由于构建器使我们能 保证正确的初始化和清除(若没有正确的构建器调用,编译器不允许对象创建),所以能获得完全的控制权 和安全性。

  • 在Java 中,垃圾收集器会自动为所有对象释放内存,所以 Java 中等价的清除方法并不是经常都 需要用到的。如果不需要类似于构建器的行为,Java 的垃圾收集器可以极大简化编程工作,而且在内存的管 理过程中增加更大的安全性。有些垃圾收集器甚至能清除其他资源,比如图形和文件句柄等。然而,垃圾收 集器确实也增加了运行期的开销。但这种开销到底造成了多大的影响却是很难看出的,因为到目前为止, Java 解释器的总体运行速度仍然是比较慢的。随着这一情况的改观,我们应该能判断出垃圾收集器的开销是 否使Java 不适合做一些特定的工作(其中一个问题是垃圾收集器不可预测的性质)。

  • 构建器实际做的事情还要多得多。特别地,当我们通过“创作”或“继承”生成新类的时候,对构建的保证仍然有效, 而且需要一些附加的语法来提供对它的支持。