Java编程思想:第五章:初始化与清理

第五章:初始化与清理


    随着计算机革命的发展,“不安全”的编程方式已逐渐成为编程代价昂贵的代价之一。

    初始化与清理正式涉及安全的两个问题,Java提供了构造器,垃圾回收器,对于不在使用的内存资源,垃圾回收器自动回收。


5.1 用构造器确保初始化

    Java中通过构造函数,类的设计者可以确保每个对象都会被初始化。创建对象时,如果其了具有构造器,Java就会在用户有能力操作对象之前自动调用相应的构造器,从而保证了初始化的进行。

    接下来的问题就是如何命名这个方法,有两种问题:

    • 所取的任何名字都可能与类的某个成员名称相冲突。

    • 调用构造器是编译器的责任,所以必须让编译器知道应该调用那个方法。

    Java中构造器用于类相同的名字,考虑到初始化时自动调用构造器。

    请注意,由于构造器的名字必须与类名相同,所以“每个方法首字母小写“编程风格并不适合构造器。不接收任何参数的构造器称为:默认构造器。或者无参数构造器。构造器也可以有参数,以便制定如何创建对象。有了构造器形式参数,就可以在初始化对象时提供实际参数。

    构造器有助于减少错误,并使代码更容易阅读。从概念上讲“初始化”和“创建“是彼此独立的,然而在上面的代码中,你却早不到initialize()方法的明确调用。在Java中,”初始化“和”创建”捆绑在一起,两者不能分离。

    构造器是一种特殊类型的方法,因为它没有返回值。这与返回值为空(void)明显不同,对于空返回值,尽管方法本身不会自动返回什么,单仍可选择它返回别的东西。构造器则不会返回任何东西。


5.2 方法重载

    任何程序设计语言都具备一项重要特性就是对名字的运用。当创建一个对象时,也就给此对象分配到了存储空间取了一个名字。所谓方式则是给某个动作取的名字。通过使用名字,你可以引用所有的对象和方法。名字起的好可以使系统更易于理解和修改。

    相同的词儿可以表达集中不同的含义,这就是重载。特别是含义之间差异特别小,这种方式十分有用。大多数人类语言有很强的冗余性,所以即使漏掉了几个词儿,仍然可以推断出含义。

    大多数程序设计语言要求每一个方法都提供一个独一无二的标识。所以绝对不能用print()函数显示了整数以后,又用一个名字为print()的函数显示浮点数。每个函数都要唯一的名称。

    为了让方法名一样,形参名字不一样的构造器同时存在,必须用到方法重载。同时,方法重载是构造器所必须的。


区分重载方法

    有几个方法有相同的名字,Java怎么知道你指的是哪一个呢?其实规则很简单:每个重载的方法都必须有一个独一无二的参数类型列表。毕竟,对于名字相同的方法,除了参数类型的差异之外,还有什么办法把它们区分呢。


涉及基本类型的重载

    基本类型可以从一种较小的类型自动提升到较大的类型。此过程一旦涉及到重载,可能会造成混乱。

    方法接收较小的基本类型作为参数,如果传入的实际参数较大,就得通过类型转换来进行窄化转换,如果不这样做,编译器就会报错。


以返回值来区分重载方法

    方法的返回值来区分重载方法是行不通的。


5.3 默认构造器

    默认构造器是没有形式参数的,它的作用就是创建一个默认对象。如果你写的类中没有默认构造器,则编译器会自动帮你创建一个默认构造器。如果已经定义了一个构造器(无论是有参还是无参),编译器都不会帮你创建构造器。


5.4 this关键字

    为了面向对象方式来编写代码,编译器幕后做了些工作,它暗自把“所操作对象的应用”作为第一个参数传递给方法x()中,比如:引用名.方法名()。这是内部的表示形式,我们并不能这样书写代码,并试图通过编译。但这种写法的确帮你理解实际所发生的事情。

    假设,你希望在方法的内部获得当前对象的引用。由于这个引用是编译器“偷偷”传入的,所以没有标识符可用。但是,为此有个专门的关键字:this。this关键字只能在方法的内部使用,表示对”调用方法的那个对象“的引用。this的用法和其他对象引用并无不同。但是要注意,如果再方法内部调用同一个类的另一个方法,就不必使用this,直接调用即可。当前方法中的this引用自动应用于同一个类中的其他方法。

    只要当需要明确指出对当前对象的引用时,才需要使用this关键字。例如需要返回当前对象引用时,常常return语句这样写:return this;


在构造器中调用构造器

    可能在一个类写了多个构造器,有时可能想在一个构造器中调用另一个构造器,以避免重复代码。this关键字可以做到这一点。

    通常写this的时候,都是指“当前对象”,而且它本身表示对当前对象的引用。在构造器中,如果为this添加了参数列表,那么就用了不同的含义。这将产生对符合此参数列表的某个构造器的明确调用。这样,调用其他构造器就有了直接的途径。

    尽管用this可以调用另一个构造器,但是不能调用两个,此处,必须将构造器调用置于第一行,否则编译器出错。

    除了构造器内之外,编译器不允许其他任何地方调用构造器。


static的含义

    了解this关键字后,就能更全面的理解static(静态)方法的含义。static方法就是没有this的方法。在static方法的内部不能调用非静态的方法。反过来倒是可以的。而且没有创建任何对象的前提下,仅仅通过类本身来调用static方法。这实际上就是static方法的主要用途。它很像全局方法。Java中禁止使用全局方法,但你在类中置入static方法就可以访问其他static方法和static域。

    有些人认为static方法不是面向对象的,因为它们的确具有全局函数的语义;使用static方法时,确实不存在this,所以不是通过“向对象发送消息”的方式来完成的。的确,代码中出现了大量的static方法,就该重新考虑自己的设计了。然而,static的概念有其适用之处,许多时候需要用到它。至于它是否是面向对象的,就留给理论家去讨论吧。


5.5 清理,终结处理和垃圾回收

    清理工作很重要,当然,垃圾回收器负责回收无用的内存资源,有些情况下(非new创建的对象)获得了一个特殊的区域。由于垃圾回收器只回收new的方式创建出来的对象的资源,所以它不知道该如何释放该对象的这一块特殊的内容。为了应对这种情况,Java允许在类中定义一个finalize()方法。它的工作原理假设是这样的,一旦垃圾回收期准备好释放对象占用的存储空间,将首先调用其finalize()方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。所以要是你打算用finalize()方法,就能在垃圾回收时刻做一些重要的清理工作。

    Java里的对象却并非总是被垃圾回收,或者换句话说:

    • 对象可能不被垃圾回收。

    • 垃圾回收并不等于“析构“。

    这意味着在你不在需要某个对象之前,如果必须执行某些动作,那么你的自己去做。Java并未提供析构函数或者相似的概念,要做类似的清理工作,必须自己动手创建一个执行清理工作的普通方法。

    也许你会发现,程序没有用完存储空间,对象占用的空间也得不到释放。如果程序执行结束,并且垃圾回收期一直都没有释放你创建的任何对象的存储空间,则随着程序的退出,那些资源也会全部交换给操作系统。这个策略是恰当的,因为垃圾回收本身也有开销,要是不适用它,那就不用支付这些开销了。


finalize()的用途何处

    垃圾回收至于内存有关,也就是说,使用垃圾回收器的唯一原因是为了回收程序不再使用的内存。所以对垃圾回收器有关的任何行为来说(尤其是finalize()方法),它们也必须同内存及其回收有关。

    这就将对finalize()的需求限制到了一种特殊情况,即通过某种创建对象方式以外的方式为对象分配了存储空间。在Java中,一切都是对象,这个特殊情况到底是什么了呢?

    看来之所以要有finalize(),是由于在分配内存时可能采用了类似于C语言的做法,而非Java中的通常做法。这种情况主要发生在使用“本地方法”的情况下。本地方法是一种在Java中调用非Java代码的方式。本地方法目前(jdk6)只支持C和C++。但是它们可以调用其他语言写的代码,所以实际上可以调用任何语言的代码。


你必须实施清理

    要清理一个对象,用户必须在需要清理的时刻调用执行清理动作的方法。如果希望进行除释放存储空间之外的清理工作,还是得明确调用某个恰当的Java方法,这就等于使用析构函数了,就没它方便而已。

记住,无论是垃圾回收还是终结,都不保证一定会发生。如果Java虚拟机并未面临内存消耗的情况,它是不会浪费时间去执行垃圾回收以恢复内存的。


终结条件

    通常,不能指望finalize(),必须创建其他的清理方法,并且明确的调用它们。不过,finalize()还有一个有趣的用法,它并不依赖于每次都要对finalie()进行调用,这就是对象终结条件的验证。

如果某个对象不再使用时,这个对象处于某种状态,使它专用的内存可以被安全的释放。

垃圾回收器如何工作

    垃圾回收器对于提高对象的创建速度,却具有明显的效果。听起来很奇怪,存储空间的释放会影响存储空间的分配,但这确实某些Java虚拟机的工作方式。这也意味着,Java从堆分配空间的速度,可以和其他语言从堆栈上分配空间的速度相近。

    垃圾回收算法多,Java采用自适应的选择一个执行。取决于不同的虚拟机实现。

 

5.6 成员初始化

    Java尽力保证,所有的变量在使用前都能得到恰当的初始化。对于方法的局部变量,Java以编译时错误的形式来保证必须初始化。

    类的基本类型的成员变量,情况就会变得有些不同,类的每一个基本类型数据成员保证都会有一个初始值。

    类中定义了的对象应用,如果不将其初始化,此引用就会获得一个特殊的null值。


指定初始化

    如果想某个变量赋初值,该怎么做呢?有一种很直接的办法,就是在定义类成员变量的地方为其赋值,比如 String x="alpha";,成员对象初始化也类似。

    定义,调用按照顺序出现,先定义,后调用,反了编译器就会发出警告。


5.7 构造器初始化

    可以用构造器来进行初始化。在运行时刻,可以调用方法或执行某些动作来决定初值,这为编程带来了更多的灵活性。记住,无法阻止自动初始化的进行,它将在构造器被调用之前发生。

    包括所有基本类型和对象引用,包括在定义时已经指定初始值的变量,这种情况都是成立的;因此,编译器不会强制你一定要在构造器的某个地方或使用它们之前对元素进行初始化:因为初始化早已得到了保证。


初始化顺序

    在类内部,变量定义的先后顺序决定了初始化的顺序。即使变量定义散步月方法定义之间,它们仍旧会在任何方法被调用之前得到初始化。


静态数据的初始化

    无论创建多少个对象,静态数据都只占用一份存储空间。static关键字不能用于局部变量,因此它只能作用于域。

    如果一个域是基本类型的,且也没有对它进行初始化,那么它就会获得基本类型的标准赋值。

    如果它是一个对象引用,那么它的默认初始化值就是null了。

    静态初始化只有在必要时刻才进行,只一次初始化的。初始化的顺序是静态对象,而后面是非静态对象。

    总结下对象的创建过程,如下:

    • 首次创建对象时,或者静态方法或者静态域首次访问时,Java解析器必须查找类路径,定位Class文件。

    • 加载Class文件,有关静态初始化的所有动作都会执行。因此,静态初始化仅在Class对象首次加载时执行一次。

    • 当创建对象的时候,首先在堆上给该对象分配内存空间。

    • 这块存储空间会被清零,这就自动的对象中的所有的基本类型数据设置成了默认值。而引用被设置成null。

    • 执行所有出现于字段定义处的初始化动作。

    • 执行构造器。


显示的静态初始化

    Java允许将多个静态初始化组织成一个特殊的静态子句(静态块),如:static {}。与其他静态初始化动作一样,这类代码也执行一次;当首次生成这个类的对象时,或者首次访问这个类的静态数据成员时(即便从未生成过这个了的对象)。就会被执行。


非静态实例初始化

    Java中也有类似于实例初始化的语法,用来初始化每一个对象的非静态变量。比如:{}。

    看起来它与静态初始化语句一模一样,只不过少了个static关键字,这种语法对于支持“匿名内部类"的初始化是必须的,但是他也使得你可以保证无论调用了哪个显示构造器,某些操作都会发生。

    实例初始化子句在构造器执行之前执行。


5.8 数组初始化

    数组只是相同类型的,用一个标识符名称封装到一起的一个对象序列或基本数据类型序列。数组是通过方括号[]来定义和使用,要定义一个数组,只需要在类型后加上一对空方括号即可。比如:int[] a1;

    编译器不允许制定数组的大小,这就又把我们带回到有关“引用”的问题上了。现在拥有的只是对数组的一个引用(你已经该数组分配了足有的内存空间),而且也没有给数组对象本身分配任何内存空间。为了给数组创建响应的存储空间,必须写初始化表达式。对于数组,初始化动作可以出现在代码的任何地方,但也可以使用一种特殊的初始化方式,它必须在创建数组的地方出现,这种特殊的初始化是由一对花括号{}括起来的值组成的,在这种情况下,存储空间的分配(等价于使用new)将由编译器负责。如:int[] a = {1, 2, 3, 4};

    所有的数组(无论他们的元素是基本类型的还是对象)都有一个固定的成员,可以通过它获知数组内包含了多少个元素,但不能其修改。这个成员就是length。Java中的数组计数也是0开始的,所以能使用的最大下标数是length-1。要是超出这个边界,就抛出运行时错误。

    如果编写程序时不能确定数组到底有多少个元素时,那么该怎么办呢?可以直接用new在数组里创建元素。尽管创建的是基本类型的数组,new仍然可以工作(不用new创建单个的基本类型数据)。比如:int[] a= new int[];

    数组的创建是运行时进行的。数据元素中的基本类型自动初始化为空值(对于数字和字符,是0,布尔类型是,false)。

    如果你创建了一个非基本类型的数组,那么你就创建了一个已用数组,比如:Integer i=new Integer[20];,如果忘记初始化直接调用,会产生运行时异常。


可变参数列表

    参数类型和个数不确定的场合,在Java中所有的类直接或者间接的继承Object类,所以可以创建Object数组,比如:Object[] obj;

    可变参数,如下:Object... obj;

    有了可变参数,再也不用显示的编写数组语法了,当你指定参数时,编译器实际上会为你去填充数组。你获得的仍然是一个数组。但是,不仅仅是从元素列表到数组的自动转换。因此,如果你有一组事务,可以把它当做列表传递,而如果你已经有了一个数组,该方法可以把它们当做可变参数来接收。

    可变参数把重载变复杂了,你可以在重载方法的一个版本上使用可变参数,当然有可以不适用。


5.9 枚举类型

    enum关键字,它使得我们在需要群组并用枚举类型集时,可以很方便地处理。比如: public enum Enumber {N, A, C, D}。

    由于枚举类型的实例是常量,因此按照命名惯例用大写字母表示(多个单词下划线分割哦)。

    为了使用enum,必须创建一个该类型的应用,比如:Enumber  h = new Enumber.N;

    在你创建enum时,编译器会自动添加一些有用的特性。如:toString()方法,ordinal()方法,values方法。

    尽管enum看起来是新的数据类型,但是这个关键字只是为enum生成对应的类时,产生了某些编译器行为,因此在很大程度上,你可以将enum当做其他任何类来处理,事实上,enum确实是类,并且具有自己的方法。

    由于switch在要在有限的可能值集合中进行选择,因此它与enum正式绝对组合,大体上,你可以enum做用另一种创建数据类型的方式,然后直接将所得到的类型拿来用。这正是关键所在,因此你不必过度的考虑他们。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章