处理教材:Initialization & Cleanup of "Thinking in Java"

来源:互联网 发布:教育部中国游学数据 编辑:程序博客网 时间:2024/05/27 09:47

——关于以TIJ为课本的Java教与学的方法

本章内容远比它标题看起来要多,因为Eckel很系统地放进去了不少编程中常遇到的问题和解决方案。很多关于JAVA理解的内容我真的都想放进PPT里去,但是因为没有代码或配图可以作支撑,也只好忍痛割爱,直接作为背景知识在课堂上讲述了,所以,如果是下载了我制作并分享的PPT辅助自学的同学,因为PPT只是对其代码和部分精华的凝练,语词很少,所以一定要一边阅读一边在PPT上做一些关键词的记号,把练习做的代码写在PPT的备注上,而PPT的作用,只是给一个更精简的全局的把握而已。

首先,作者精彩地指出了this指针的三种用途:在普通方法中把自己传递给别人(一种重要的模式);返回自己以便连续调用(用得少);在构造函数中避免参数名字混淆(很小的技巧)。顺带着介绍了只能放在构造方法第一个语句的this(),其唯一的作用就在于此:用来调用其他的构造方法,以便代码复用。

在对finalize()方法的讲述中,作者引用了文献,指出除非是为了释放本地方法(即外部方法,指用java引用的和本地操作系统相关的C++/C函数,不受java平台控制)可能需要释放堆内存,或者仅仅作为检查错误的一种方式,才能使用,不可以像类似C++的析构函数一样地用,因为析构函数可以保证在对象一定会在程序结束前析构,而java的自动垃圾回收就不一样,谁都不知道这个对象在什么时候会被回收(只有在回收之前会自动调用finalize()函数,不能显式调用),甚至有可能到了程序结束都没有回收,那样的话,程序结束时java编译器就直接把使用的全部内存释放,也不会调用finalize()方法了。所以,实际上,finalize方法只能作为一种检查错误的设计模式而使用,或者调用本地方法且本地方法动态申请了堆(heap)内存需要释放时作为一种不够好的设计模式而使用(因为不知道什么时候,甚至会不会释放),如果真的要管理外部内存的话,最好还是写一个普通方法,自己来管理和调用好了。

同样,对于垃圾回收,作者也讲得非常精彩,大大拉近了读者和Java平台的距离,使读者产生亲近感。为什么需要回收内存呢,因为程序就是从main函数开始,不断地new一些对象出来,对象在使用方法的过程中又会new一些其他的对象,就好像游戏里做弓箭、做士兵一样。然后有的就死了,比如箭射出去一次就没有用了,所以“射箭”的方法就会创造很多箭,但是这些箭的reference不会被保存。在程序中一个对象死了就表示不能再通过任何可见的对象reference到了,因为如果还能reference到的话,就表示还可以给它发消息,还可以用,没有死。死了的对象就需要从内存中清理出去,有两种主要的方式:

一种方法是从正在使用的对象(main函数知道)开始,把所有引用到的对象做标记,因为只要是活着的对象(就是可见的、可引用到的对象),就一定能够通过层层引用最后回归到正在使用的可见对象上来,所以这样可以给所有活着的对象做标记,那么剩下的就是dead objects,这种方法需要临时把程序暂停,然后去做一个这种遍历,再回收dead objects所占的空间,这种方法称为mark-and-sweep

另一种方法是,把内存分成两大块,平时只用其中一块,垃圾收集时,也把程序暂停,然后把所有可见的对象转移到另一块,转移一个对象时,把所有它们引用到的对象也要一起转移。这样,完成一次大转移之后,所有无法reference到的对象就留在先前那一块里面了,然后把那一块集体清空,这样做的好处是,可以顺便整理live objects的内存分配,坏处是需要移动大量的内存,而且还要改变它们的地址,这种方法叫做stop-and-copy

当然了,如果结合内存分块(chunk)的思想,可以把内存分成很多块,当一次回收之后把live objects放到一块,回收之后可能又要运行很长时间之后才会开始下一次回收,这个时间可能又new了很多其他的对象,那么新建立的对象就放在别的块里,下次回收的时候,先在那些放置新建对象的块进行回收,因为它们需要回收的可能性会比较大;再或者动态地检查垃圾回收的效率,如果效率不高下次就采用另一种等等。那么,经过这么多年的发展,JAVA平台已经能够比较智能、高效地管理堆内存了。

接下来的初始化,作者讲解得非常细腻,对优先级做了深入的讨论,也就是有关静态field初始化和普通field初始化(就是在类定义里面直接写int length=5;之类)、两种普通的构造函数、静态构造函数,哪一个先调用,哪一个后调用的问题。这里妙在作者不仅描述了java支持的5类初始化方式,而且给出了在编程中如何互相配合使用的建议,使读者形成良好的编程思路。作者为了确切找到初始化的顺序,设计了相对冗长的实验,我在讲授时将略去一部分,因为重要的是掌握模式,谁先谁后对编程其实没有实用价值,就像运算符优先级一样,一般而言,只需要记住静态初始化在前,然后是静态构造函数,然后是普通的初始化,最后是普通构造函数就可以了。

初始化和构造函数之后,就是作者独具匠心的对数组的本质和数组初始化,以及利用Object[]数组设计可变参数表了。作者提出,数组的本质是对象,只是JAVA对数组做了一些更方便的语法,就像对String所做的一样。而且,一个对象元素数组构造时,它的每个元素会初始化为null,即使是Integer[]Double[]也不例外,需要先初始化里面的对象了之后,才能使用。为了避免这种麻烦,作者建议使用带有初始化的数组创建方式。使用Object[]作参数可以模拟出可变长度参数的效果,但是会比较麻烦,因为必须先做一个Object数组再把它传进去,所以,javaJAVASE5以后加入了对可变参数表语法的支持,实际上后者就是Object[],语法简化了,而且还能支持无参的调用,那样的话,方法内会得到一个长度为0的数组对象。由于使用可变参数表只是个人编程风格的问题,如同默认参数一样,所以Eckel只是告诉了我们通常使用可变参数表的几种模式。

在最后一部分,谈到了枚举类型,因为枚举类型常常在编程中需要,而对于枚举类型的初始化和赋值常常是人们感到困难的地方,所以,Eckel在此做了详细的讨论,JAVASE5的一个重要的改进就是Enumerated types(枚举类型),它把枚举看成一种特殊的类,给它内置了不少的功能,就像C#event所做的一样,比如静态的value()能够返回所有枚举值组成的数组,而ordinal( )则能返回某一个枚举值所处的位置。枚举配合Switch做多项选择会很适合,它最大的用途是给用户一些基本不会变的选择,比如在用户界面中使用。

    这一章,作者的内容组织确实非常精彩,只有在可变参数表部分,因为Java在这里还不完善,所以这里的讨论可能有一些钻牛角尖,其中一些实验结果在新的JavaSE6中已经发生变化了,比如f(float i, Character... args)f(Character... args)作为两个重载函数,在作者成书的时候还不可以,但是现在jdk1.6.0_18已经可以了,作者根据实验结果,建议读者在设计含有可变参数表的函数重载时最多只能有一个函数带有可变参数,那么现在可以不必了,根据我自己的实验结果,目前,JAVA自动包装尚未完善,影响了可变参数表特征的使用。如int可以自动包装成Integer,但却不能写“Long d=2;”或“Double d=2;”,但是可以“Long d=new Long(2);”或者“Double d=new Double(2);”,也就是说int并不能自动包装成其他类型的包装类,这些细节性的特征,导致可变参数表的设计中难以判断两个重载函数是否会发生混淆。在这里,希望读者从实际设计模式的角度来学习,就像运算符优先级很少被充分利用一样,使用不同的包装类型作为可变参数表也会导致难以理解的代码,而且随着Jdk的不断升级,这些现在没有考虑完善的地方以后还会发生变化。所以,学习的重点在于学会那些稳定而优秀的设计模式。