Java编程思想第十一更

关键字:垃圾回收,finalize,数组,初始化

先例行闲聊

  • 距离上次更新过去半个月了,期间一直在准备考试,当然也没看几天书,全靠最后两天临时抱佛脚。考试监考还是挺严格的,来考试的人也比我想象的年纪大写,考场中年纪最大的一个目测也有三十多了。考英语的时候一哥们问老师为啥还不放听力,老师说再等等,其实根本就没有听力考试。

4.3 清除:收尾和垃圾收集

程序员都知道“初始化”的重要性,但通常忘记清除的重要性。毕竟,谁需要来清除一个int呢?但是对于库来说,用完后简单地“释放”一个对象并非总是安全的。当然,Java 可用垃圾收集器回收由不再使用的对象占据的内存。

现在考虑一种非常特殊且不多见的情况。假定我们的对象分配了一个“特殊”内存区域,没有使用new。垃圾收集器只知道释放那些由new分配的内存,所以不知道如何释放对象的“特殊”内存。

为解决这个问题,Java 提供了一个名为finalize()的方法,可为我们的类定义它。在理想情况下,它的工作原理应该是这样的:一旦垃圾收集器准备好释放对象占用的存储空间,它首先调用finalize(),而且只有在下一次垃圾收集过程中,才会真正回收对象的内存。所以如果使用finalize(),就可以在垃圾收集期间进行一些重要的清除或清扫工作。

  • 初始化要小心,释放同样要注意。一些特殊的内存占用可以通知GC来清理掉,而通知的方法就是调用finalize()方法。要注意的这个方法不是直接的销毁对象,而是通知GC前来处理,所以这也算不上C语言中的破坏器。

垃圾收集并不等于“破坏”!

我们的对象可能不会当作垃圾被收掉!

4.3.1 finalize() 用途何在

垃圾收集只跟内存有关!

垃圾收集器存在的唯一原因是为了回收程序不再使用的内存。所以对于与垃圾收集有关的任何活动来说,其中最值得注意的是finalize()方法,它们也必须同内存以及它的回收有关。

这是否意味着假如对象包含了其他对象,finalize()就应该明确释放那些对象呢?答案是否定的——垃圾收集器会负责释放所有对象占据的内存,无论这些对象是如何创建的。它将对finalize()的需求限制到特殊的情况。在这种情况下,我们的对象可采用与创建对象时不同的方法分配一些存储空间。

4.3.2 必须执行清除

Java 不允许我们创建本地(局部)对象——无论如何都要使用new。但在Java 中,没有“delete”命令来释放对象,因为垃圾收集器会帮助我们自动释放存储空间。

所以如果站在比较简化的立场,我们可以说正是由于存在垃圾收集机制,所以 Java 没有破坏器。

然而,随着以后学习的深入,就会知道垃圾收集器的存在并不能完全消除对破坏器的需要,或者说不能消除对破坏器代表的那种机制的需要(而且绝对不能直接调
用finalize(),所以应尽量避免用它)。若希望执行除释放存储空间之外的其他某种形式的清除工作,仍然必须调用Java 中的一个方法。它等价于 C++的破坏器,只是没后者方便。

finalize()最有用处的地方之一是观察垃圾收集的过程。

  • ?:为啥不能直接调用finalize?
  • 原文后面举了一个重写finalize的例子,表示GC可能会出问题在Java1.1里。

4.4 成员初始化

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

  • 没太看懂后一句,举例子是一个变量声明了但是没有初始化。

当然,编译器也可为i 赋予一个默认值,但它看起来更象一个程序员的失误,此时默认值反而会“帮倒忙”。若强迫程序员提供一个初始值,就往往能够帮他/她纠出程序里的“臭虫”。

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

  • 没太明白为什么基本类型就不能没有初始值,我感觉这是从底层内存出发就可以解释的,不知道为什么解释的我听不懂。
  • 基本类型都会有一个初始的默认值,有时候会帮忙,有时候会捣乱。

4.4.1 规定初始化

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

  • 也就是在类中直接就对变量进行初始化,给类型设置默认值。

4.4.2 构建器初始化

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

  • 当然更推荐通过构造器来初始化,这样会让对象更加低耦合。
  1. 初始化顺序

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

  1. 静态数据的初始化

若数据是静态的(static),那么同样的事情就会发生;如果它属于一个基本类型(主类型),而且未对其初始化,就会自动获得自己的标准基本类型初始值;如果它是指向一个对象的句柄,那么除非新建一个对象,并将句柄同它连接起来,否则就会得到一个空值(NULL)。
如果想在定义的同时进行初始化,采取的方法与非静态值表面看起来是相同的。但由于static 值只有一个存储区域,所以无论创建多少个对象,都必然会遇到何时对那个存储区域进行初始化的问题。

  • 静态变量的初始和普通变量相同,基本类型有初始值其他的为Null。但是静态数据只能去初始化一次。
  1. 明确进行的静态初始化

Java 允许我们将其他static初始化工作划分到类内一个特殊的“static 构建从句”(有时也叫作“静态块”)里。

尽管看起来象个方法,但它实际只是一个static 关键字,后面跟随一个方法主体。与其他 static初始化一样,这段代码仅执行一次——首次生成那个类的一个对象时,或者首次访问属于那个类的一个 static 成员时(即便从未生成过那个类的对象)。

  • 其实静态块和各种初始化还是区分先后顺序的,但是不要去死记硬背。
  1. 非静态实例的初始化

针对每个对象的非静态变量的初始化,Java 1.1 提供了一种类似的语法格式。

  • 语法类似与声明变量后,加大括号,括号内进行初始化,感觉像是匿名内部类的样式。

它看起来与静态初始化从句极其相似,只是static 关键字从里面消失了。为支持对“匿名内部类”的初始化(参见第7 章),必须采用这一语法格式。

4.5 数组初始化

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

也可以将方括号置于标识符后面,获得完全一致的结果。

这种格式与 C和 C++程序员习惯的格式是一致的。然而,最“通顺”的也许还是前一种语法,因为它指出类型是“一个 int 数组”。本书将沿用那种格式。

  • 数组和基本类型一样是天然会出现在一种编程语言中的,因为这是最基本的对数据的收集和处理。也正是因为数组的种种缺陷,才让各种数据结构有了自己的发挥空间。不过在访问速度等方面,数组还是无法被超越的。
  • 关于数组的初始化写成int[]还是int a[]的争论也是有不少,前一种更能清楚的IPv6宝石这个一个什么类型的数组,后一种写法则更清晰的表示这个变量是一个数组,Java中普遍还是使用后一种写法。

编译器不允许我们告诉它一个数组有多大。这样便使我们回到了“句柄”的问题上。此时,我们拥有的一切就是指向数组的一个句柄,而且尚未给数组分配任何空间。为了给数组创建相应的存储空间,必须编写一个初始化表达式。对于数组,初始化工作可在代码的任何地方出现,但也可以使用一种特殊的初始化表达式,它必须在数组创建的地方出现。这种特殊的初始化是一系列由花括号封闭起来的值。

  • 最基本的初始化就是在括号中直接写明其中具有什么元素。

所有数组都有一个本质成员(无论它们是对象数组还是基本类型数组),可对其
进行查询——但不是改变,从而获知数组内包含了多少个元素。这个成员就是 length。

与C和 C++类似,由于Java 数组从元素 0 开始计数,所以能索引的最大元素编号是“length-1”。如超出边界,C 和C++会“默默”地接受,并允许我们胡乱使用自己的内存,这正是许多程序错误的根源。然而,Java 可保留我们这受这一问题的损害,方法是一旦超过边界,就生成一个运行期错误(即一个“违例”,这是第9 章的主题)。

当然,由于需要检查每个数组的访问,所以会消耗一定的时间和多余的代码量,而且没有办法把它关闭。这意味着数组访问可能成为程序效率低下的重要原因——如果它们在关键的场合进行。但考虑到因特网访问的安全,以及程序员的编程效率,Java 设计人员还是应该把它看作是值得的。

  • 所有的数组都有一个内部的变量来表示其长度,不然一个没有长度的数组可是很可怕的。
  • 在C系列中,对数组的读取超过了边界,你就会看到很多奇奇怪怪的东西,如果没有发现问题就可能让你的程序跑偏。在Java中就不运行这样的操作,会直接生成一个错误来阻止继续运行。虽然这样会影响到运算速度,但是和安全比这还是值得的。

4.5.1 多维数组

在Java 里可以方便地创建多维数组

1
2
3
4
int[][] a1 = {
{ 1, 2, 3, },
{ 4, 5, 6, },
};
  • 形如大括号中套大括号来初始化,不要纠结多为数组的使用和计算,多为数组很少被使用,不要上了谭浩强的当。
  • 后面的例子展示了三维数组,这就更没必要了。

4.6 总结

作为初始化的一种具体操作形式,构建器应使大家明确感受到在语言中进行初始化的重要性。

由于构建器使我们能保证正确的初始化和清除(若没有正确的构建器调用,编译器不允许对象创建),所以能获得完全的控制权和安全性。

  • 不好好调用构造器都不允许插件对象以此来保护对象的安全。

在C++中,与“构建”相反的“破坏”(Destruction)工作也是相当重要的,因为用new 创建的对象必须明确地清除。

在Java 中,垃圾收集器会自动为所有对象释放内存,所以 Java 中等价的清除方法并不是经常都需要用到的。如果不需要类似于构建器的行为,Java 的垃圾收集器可以极大简化编程工作,而且在内存的管理过程中增加更大的安全性。有些垃圾收集器甚至能清除其他资源,比如图形和文件句柄等。然而,垃圾收集器确实也增加了运行期的开销。

  • GC的优缺点也是老生常谈了。

由于所有对象都肯定能获得正确的构建,所以同这儿讲述的情况相比,构建器实际做的事情还要多得多。特别地,当我们通过“创作”或“继承”生成新类的时候,对构建的保证仍然有效,而且需要一些附加的语法来提供对它的支持。大家将在以后的章节里详细了解创作、继承以及它们对构建器造成的影响。

  • 创建子类好像还和父类的构造器有关系,后面我们一起了解。

  • PS:感觉这一章翻译的很不顺口不知道为什么。

  • 下一章,隐藏实施过程。下次再见啦,我要去买个烧饼吃。

END

作者

liukun

发布于

2020-10-30

更新于

2021-05-28

许可协议