读书笔记 | 《Think in Java》Ⅴ 初始化与清理

Ⅴ 初始化与清理

---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)
//优雅
复制代码
  • 其实构造器也是一种方法重载的表现,因为构造器的方法名必须为类名,那么,根据不同的需求,对于带有不同参数列表的构造器,实质上完成了重载。

  • 前文说过,基本类型在进行运算时,将由较小的类型变为较大的类型。在涉及到基本类型作为重载方法的参数时,会有些不一样:

  1. 由小变大: 对于数字来说,若传入参数的数据类型小于方法中声明的形参数据类型,那么实际数据类型会被提升至与形参相同;对于char来说,若无法找到恰好接受char参数的方法,编译器会将char提升至int。
  2. 有大变小: 传入参数类型比形参大,那么需要手动进行参数的窄化转换,否则编译器会报错。
  • 重载方法之间一般都是以参数列表互相区分的。但是在某些情况也可以通过返回值类型来区分。(不建议使用) 举个栗子:
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后,就能更全面地理解staticstatic方法就是没有this的方法,在static内部不能调用非静态方法。它很像全局方法,但是Java中禁止全局方法,static成了一个替代品。因此有人认为static是非面向对象的,因为不是通过“向对象发送消息”来完成的。我个人同意,不过这确实在一些场景下方便了不少。不过,假如你在代码中出现了大量的的static,那就需要考虑优化了。
5.5 清理:终结处理和垃圾回收
  • Java有垃圾回收器来负责回收无用对象占据的内存空间。不过存在特殊情况:假如你的对象不是通过new出来的(垃圾回收器只知道释放那些经由new分配的内存),为了应对这种情况,Java允许在类中定义一个finalize()方法。 他的工作原理“假定”是这样的:finalize()是在垃圾回收的时刻做一些重要的清理工作。
5.5.1 finalize()的用处何在
  • finalize()并不等同于C++中的析构函数。因为在C++中,对象一定会被销毁,在Java中:
  1. 对象可能不被垃圾回收
  2. 垃圾回收不等同于析构
  • 那么上文所说的“重要的清理工作”指的是什么?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采用一种自适应的垃圾回收技术。有一种做法名为:“停止-复制”: 先暂停程序的运行,将所有存活的对象从当前堆复制到另一个堆,没有被复制的全部是垃圾。在新堆里。这些对象的空间是一个挨着一个的,保持紧凑排列,上文提到:“一边回收空间,一边使堆中的对象紧凑排列”。这样就可以简单高效地分配新空间了。 这种“复制式”回收器效率会降低,主要为两个原因:

  1. 首先得有两个堆,需要比实际多一倍的空间。
  2. 程序进入稳定状态后,可能只有少量垃圾,甚至没有垃圾。复制使得很浪费。在检查到要是没有新垃圾产生,JVM会切换到“标记-清扫”模式,这种方式速度很慢,但是只产生少量垃圾时,速度就快了。
  • Java的垃圾回收技术可以这么称呼“自适应的,分代的,停止-复制,标记-清扫”式垃圾回收器。

  • Java虚拟机中有很多附加技术以提升速度。尤其是与加载器有关的,被称为“即时编译器”的技术。

本章的5.5节,存在一些难点,我读了三遍,尽量去理解揣摩。不过毕竟初读此书,一些笔记可能会出现偏差,望斧正。

5.6 成员初始化

---4.10更新---

  • Java尽力保证:所有变量在使用前都能得到初始化。
  1. 对于局部变量,假如数据未得到初始化,编译器将用编译时错误来贯彻这种保证。其实对于编译器来说,可以给未初始化的局部变量一个默认值,但是Java设计者没那么做。因为未初始化的局部变量往往是程序员的疏忽,赋予默认值会掩盖这种失误,甚至导致程序错误。
  2. 在类的对象中,类的基本数据成员将都获得初始值。
5.8 数组初始化
  • 所有数组都有一个固有成员,就是length,最大的数组能用下标就是length-1。超过数组的边界,C++或C会默默地接受,允许你访问所有内存,这可能会导致一些程序错误。Java对这点进行了控制,假如超过数组下标,将会出现运行时异常。

  • 当然,对于Java这种每次访问数组都要检查下标的做法,肯定是需要额外的开销。但是对于安全与提高程序员的生产力来说,无疑很有价值。Java的设计者认为这种权衡是值得的。不仅如此,自动的编译期错误和运行时优化都可以提高数组访问速度,没有必要牺牲安全性去求得一点点的效率提升。

5.9 枚举类型
  • 在创建enum时,编译器会自动添加一些有用的特性,比如toString,此方法可以用来输出枚举类型常量的字符串表示。ordinal()用来标记枚举型常量的声明顺序。valus()来生成包含枚举类型常量的数组。
写在最后
  • C++的设计者在设计C++时对C语言的生产效率进行了调查,发现大量错误都来自于不正确的初始化,不恰当的清理也会产生类似问题。构造器,使得初始化和清理受到了控制,也很安全。

  • C++中析构函数非常重要,使用new创建的对象必须明确被销毁。在Java中,垃圾回收器会自动为对象释放内存。但是在有些少数场合,只能去手动释放。但是垃圾回收器也增加了开销,虽然Java的性能已经得到了长足的进步,但是速度问题也成为了在某些特定编程领域的障碍。

小白的成长探索之路,欢迎与我交流。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值