Ⅴ 初始化与清理
---4.9更新---
随着计算机革命的发展,“不安全”的编程方式已经逐渐成为编程代价高昂的主因之一。 其中主要为“初始化”和“清理”两方面。Java提供了构造器和“垃圾回收器”。构造器为在创建对象时被自动调用的方法,垃圾回收器用于自动释放不再使用的内存资源。
5.1 用构造器确保初始化
-
因为构造器名称必须与类名完全相同,所以“每个方法首字母小写”的编码风格不适用于构造器。
-
构造器有助于减少错误,并且使代码更容易阅读。
-
概念上来讲,“初始化”和“创建”是彼此独立的,但是在Java中,这两者是项目捆绑,不能分离的。
5.2 方法重载
- 为什么要设计“重载”呢?因为人类语言中存在一些细微差别的概念,在将其“映射”到程序设计中时,需要将语言“重载”。 举个栗子,假如我们在日常生活中说:“以洗车的方式洗车”、“以洗衣服的方式洗衣服”,这虽然没有什么不对,但是会显得很多余和愚蠢。程序设计语言也是一样,比如:
Cleaner c=new Cleaner()Car car=new Car()Clothe clothe=new Clothe()//这样写就很麻烦//c.cleanCar(car)//c.cleanClothe(clothe)c.clean(car)c.clean(clothe)//优雅复制代码
-
其实构造器也是一种方法重载的表现,因为构造器的方法名必须为类名,那么,根据不同的需求,对于带有不同参数列表的构造器,实质上完成了重载。
-
前文说过,基本类型在进行运算时,将由较小的类型变为较大的类型。在涉及到基本类型作为重载方法的参数时,会有些不一样:
- 由小变大: 对于数字来说,若传入参数的数据类型小于方法中声明的形参数据类型,那么实际数据类型会被提升至与形参相同;对于char来说,若无法找到恰好接受char参数的方法,编译器会将char提升至int。
- 有大变小: 传入参数类型比形参大,那么需要手动进行参数的窄化转换,否则编译器会报错。
- 重载方法之间一般都是以参数列表互相区分的。但是在某些情况也可以通过返回值类型来区分。(不建议使用) 举个栗子:
void f() {}int f() { return 1;}int x=f();//正确。如果此时编译器可以判断出正确语义,可以使用。f();//错误,无法判断。复制代码
5.3 默认构造器
- 假如没有定义构造器,那么编译器会自动创建一个无参构造器已供使用; 但是已经定义了含参构造器,那么此时编译器不会自动创建无参构造器,使用无参方法构造一个对象将会出错。
5.4 this关键字
-
this需要自己理解,具体用法我就不做赘述了。
-
this一个特殊的使用场景是用来在构造器中调用构造器。但只能调用一次。
public class Car{int p=0;String s="";Car () {}Car (int p){this.p=p;}Car (int p,String s) {this(p); //在这里通过this在构造器中调用构造器。其他this是基本常见用法。this.s=s;}复制代码
- 在理解了this后,就能更全面地理解
static
。static
方法就是没有this的方法,在static
内部不能调用非静态方法。它很像全局方法,但是Java中禁止全局方法,static
成了一个替代品。因此有人认为static
是非面向对象的,因为不是通过“向对象发送消息”来完成的。我个人同意,不过这确实在一些场景下方便了不少。不过,假如你在代码中出现了大量的的static
,那就需要考虑优化了。
5.5 清理:终结处理和垃圾回收
- Java有垃圾回收器来负责回收无用对象占据的内存空间。不过存在特殊情况:假如你的对象不是通过new出来的(垃圾回收器只知道释放那些经由new分配的内存),为了应对这种情况,Java允许在类中定义一个
finalize()
方法。 他的工作原理“假定”是这样的:finalize()
是在垃圾回收的时刻做一些重要的清理工作。
5.5.1 finalize()的用处何在
finalize()
并不等同于C++中的析构函数。因为在C++中,对象一定会被销毁,在Java中:
- 对象可能不被垃圾回收
- 垃圾回收不等同于析构
- 那么上文所说的“重要的清理工作”指的是什么?
finalize()
用于什么场景下? 在使用“本地方法”(本地方法是一种在Java中调用非Java代码的方式)的情况下,分配内存的方式不是使用Java中的通常做法,而是使用了类似C语言中的做法,所以需要使用finalize()
。
5.5.2 你必须实施清理
-
Java中的垃圾回收器能帮助你释放不再使用的对象的内存空间。但是随着学习的深入,你会明白垃圾回收器并不能完全代替C++中的析构函数。(也不能直接调用
finalize()
),此时如果想要去进行除去释放存储空间之外的清理工作,还是得明确调用某个恰当的Java方法来操作(相当于析构函数,但是没有析构函数方便) -
垃圾回收并不保证一定发生。如果JVM没有面临内存耗尽的情况,他是不会浪费时间去恢复内存的。
5.5.3 终结条件
-
上文说了,
finalize()
只存在于很少用到的一些晦涩用法里。但是finalize()
有一些有趣的用法,用来对对象是否可以被释放的条件(终结条件)的验证,并不依赖于每次都要调用finalize()
这个方法。 -
举个栗子:
Class Book { boolean checkedOut=false; Book(boolean checkOut){ checkedOut=checkOut;} void checkIn(){ checkedOut=false;} protected void finalize(){ if(checkedOut) System.out.println("Error: checked out");}}public static void main(String[] args){ Book novel =new Book(true); novel.checkIn(); new Book(true); System.gc();//gc()函数的作用只是提醒虚拟机:程序员希望进行一次垃圾回收。}输出:Error: checked out复制代码
在这个例子中,对象的终结条件是对象是否被checkIn,在主函数中,由于new Book(true);
这本书未被checkIn。如果没有finalize()
中来验证终结条件,将很难发现这种缺陷。
5.5.4 垃圾回收器如何工作
-
在通常的程序设计语言中,在堆上分配内存代价比较高,由于Java的对象都是创建在堆上的,所以大家会觉得Java效率低下。事实是Java在堆上的分配方式和诸如C++等语言不同,它的“堆指针”只是简单地移动到未分配的下一区域,效率比得上C++在栈上分配空间的效率。 但是,频繁的内存页面调度会显著影响性能,而由于垃圾回收器的存在,一边回收空间,一边使堆中的对象紧凑排列,这样尽可能地减少了页面调度,避免页面错误。
-
先了解一些基本垃圾回收机制:引用计数垃圾回收技术。但是“引用计数垃圾回收”存在很多问题,已经渐渐淘汰。
-
在一些更快的模式中,针对每个发现的引用,追踪其引用的对象,再通过这个对象所包含的引用,去追踪这些引用的对象,如此反复,直到“根源于堆栈和静态存储区的引用”所形成的网络被全部访问为止。
-
在这种方式下,JVM采用一种自适应的垃圾回收技术。有一种做法名为:“停止-复制”: 先暂停程序的运行,将所有存活的对象从当前堆复制到另一个堆,没有被复制的全部是垃圾。在新堆里。这些对象的空间是一个挨着一个的,保持紧凑排列,上文提到:“一边回收空间,一边使堆中的对象紧凑排列”。这样就可以简单高效地分配新空间了。 这种“复制式”回收器效率会降低,主要为两个原因:
- 首先得有两个堆,需要比实际多一倍的空间。
- 程序进入稳定状态后,可能只有少量垃圾,甚至没有垃圾。复制使得很浪费。在检查到要是没有新垃圾产生,JVM会切换到“标记-清扫”模式,这种方式速度很慢,但是只产生少量垃圾时,速度就快了。
-
Java的垃圾回收技术可以这么称呼“自适应的,分代的,停止-复制,标记-清扫”式垃圾回收器。
-
Java虚拟机中有很多附加技术以提升速度。尤其是与加载器有关的,被称为“即时编译器”的技术。
本章的5.5节,存在一些难点,我读了三遍,尽量去理解揣摩。不过毕竟初读此书,一些笔记可能会出现偏差,望斧正。
5.6 成员初始化
---4.10更新---
- Java尽力保证:所有变量在使用前都能得到初始化。
- 对于局部变量,假如数据未得到初始化,编译器将用编译时错误来贯彻这种保证。其实对于编译器来说,可以给未初始化的局部变量一个默认值,但是Java设计者没那么做。因为未初始化的局部变量往往是程序员的疏忽,赋予默认值会掩盖这种失误,甚至导致程序错误。
- 在类的对象中,类的基本数据成员将都获得初始值。
5.8 数组初始化
-
所有数组都有一个固有成员,就是
length
,最大的数组能用下标就是length-1
。超过数组的边界,C++或C会默默地接受,允许你访问所有内存,这可能会导致一些程序错误。Java对这点进行了控制,假如超过数组下标,将会出现运行时异常。 -
当然,对于Java这种每次访问数组都要检查下标的做法,肯定是需要额外的开销。但是对于安全与提高程序员的生产力来说,无疑很有价值。Java的设计者认为这种权衡是值得的。不仅如此,自动的编译期错误和运行时优化都可以提高数组访问速度,没有必要牺牲安全性去求得一点点的效率提升。
5.9 枚举类型
- 在创建enum时,编译器会自动添加一些有用的特性,比如
toString
,此方法可以用来输出枚举类型常量的字符串表示。ordinal()
用来标记枚举型常量的声明顺序。valus()
来生成包含枚举类型常量的数组。
写在最后
-
C++的设计者在设计C++时对C语言的生产效率进行了调查,发现大量错误都来自于不正确的初始化,不恰当的清理也会产生类似问题。构造器,使得初始化和清理受到了控制,也很安全。
-
C++中析构函数非常重要,使用new创建的对象必须明确被销毁。在Java中,垃圾回收器会自动为对象释放内存。但是在有些少数场合,只能去手动释放。但是垃圾回收器也增加了开销,虽然Java的性能已经得到了长足的进步,但是速度问题也成为了在某些特定编程领域的障碍。
小白的成长探索之路,欢迎与我交流。