《Effective Java》读书笔记

其实无论是在学校写算法题,还是在百度实习时都是写的C++,正式工作后,突然间从C++改写了Java。但是过渡起来还好,因为由于Java的GC机制存在,内存控制准确的说是内存堆的控制直接交给了JVM,所以除基本数据类型外,万事皆引用。不过,写了这么久Java还是需要对JVM、深入的Java语法应用有一定的了解。因为这个原因,我阅读了《Effective Java》 《深入理解Java虚拟机》以及网上的一些博客。写这面文章的原因是在阅读《Effective Java》这本书时,遇到的一些平常没有思考到的问题,对于我自己觉得比较有针对性的条例,自己也做一遍笔记,算是再加深一次印象。

消除过期的对象引用,避免GC时无法回收过期对象

一个Java程序员基本对JVM的垃圾回收机制都有了解,但是可能也会出现自己实现的java代码中存在过期对象无法被回收的情况。这些场景绝大多数会出现在自己实现了一些数据结构,这些数据结构可能存储了对象的引用,但是数据结构可能保存了相关对象的引用后没有进行清除操作,这就导致了垃圾回收时,这些存储在栈中的旧对象的引用也会被当做GC-ROOT被遍历标记为非过期对象引用,导致本应该被回收的对象没有被回收,占据内存。
书上列举的Demo,很有特点:

这段程序在逻辑上并没有明显的错误,但是实际运行时,凡事存在Stack里面的对象的引用,所对应的对象都无法被GC。这样就造成了类似C++的内存泄露了。
原因是在pop方法中,返回的elements的元素只做了栈的数量更新却没有进行栈元素的初始化操作。
正确写法:

不使用finalizer

如果一个类覆盖了finalizer方法,那么在GC过程中,一个对象在被释放前会执行这个方法做一些操作。但是如果没有特殊的原因(例如在对象被GC时延长这个的对象生存周期让这个对象顺利进入新生代),是绝对不要使用这个方法的。原因大概如下几点:

不保证方法的执行时间

由于是在GC过程中,所以不会保证这个方法的执行时间,所以在不同平台的JVM上面执行这个方法的时间会存在很大的差异,导致程序执行起来截然不同。

方法执行出异常会导致对象回收失败

在方法中出异常会导致对象回收失败,这种被破坏的对象被其他线程继续使用会出现不确定的行为,且不会打出栈轨迹等异常信息。

性能问题

书上给出的正常销毁对象的时间为5.6ns,而销毁带有finalizer的对象需要消耗2400ns。

谨慎覆盖equals hashCode方法,谨慎实现Comparable接口

覆盖equal方法需要满足等价关系

自反性

一个对象需要等于自己本身。这一条基本很难违背,一旦违背,造成的后果可能会包含将这个类的对象放入Collection后,调用contains方法会返回false。

对称性

如果对象a.equals(b)返回ture,那么b.equals(a)也应该返回true。一旦违背,那么在一些场景中可能就出现不确定的情况,如Collection的contains方法。

传递性

如果对象a.equals(b)返回ture,对象b.equals(c)返回true,那么a.equals(c)也应该返回true。在子类继承父类时并且需要扩展属性时,可能会有校验父类子类的类型进行分情况判断,子类的实例a,c的扩展属性不同但父类的属性相同,父类b的属性与a,c的属性相同。那么则会有a.equals(b)为true b.equals(c)为true,但是a.equals(c)却返回false。
传递性和对称性在存在继承关系时很难同时保证,需要针对具体的情况进行分析比较。

一致性

如果对象属性没有更改,那么a.equals(b)的返回值始终一致。需要注意:equals方法不要依赖不可靠资源。

非空性

通用约定:equals方法不能抛NPE异常。基本做法是通过instanceof来类型检查,不满足时直接false掉。

覆盖equals方法总要覆盖hashCode方法

如果一个类覆盖了equals方法但是没有覆盖hashCode方法,那么两个对象a,b,a.equals(b)为true,在使用hash表相关的数据结构,如HashMap,HashSet时,map.put(a,object)再用map.get(b,object)会返回null。

始终覆盖toString方法

打印对象时打印的是对象内容而不是对象的内存地址。方便调试,排查问题。

实现Comparable接口

需要注意的是compareTo方法返回0的情况应该与equals的判断结果一致。如果不一致,那么使用不同的Collection实现类存储的结果有差别。TreeMap会调用compareTo,而HashMap会调用hashCode。例子:BigDecimal类符合描述的情况,BigDecimal(“1.0”) BigDecimal(“1.00”)两个对象放入TreeSet中只会保留一个,放入HashSet中会保存两个。

类中变量的访问

实例域不能为public

线程不安全。没有API访问变量,直接通过类名.变量名访问,想加锁控制都没办法。

除非抽象好类的常量,静态域也应为private

避免外部能直接修改导致错误&线程不安全。

用getter、setter访问私有域代替不封装直接访问公有域

一旦需要修改变量逻辑,需要改动的点为所有直接通过变量名直接访问变量的地方。

类的继承

谨慎继承

不要继承不熟悉的类,更不要覆盖不熟悉的方法。看下面的例子:

这个继承的目的是打算计算这个HashSet曾经一共添加过多少元素。按照下方写法期望的返回值应该是3,但实际addCount的值是6。因为HashSet的addAll方法也是调用的add方法,所以addCount被重复计算了。
正常的写法书中叫做“复合”,也就是继承一个转发类,转发类即实现Set接口,定义一个私有域的Set对象去实现Set的接口方法,然后再去继承。

在继承父类前想清楚使用继承还是用接口

除有典型的父类子类的关系之外,要用的继承的地方都要考虑用接口是否更合理。

用到抽象类的地方要考虑用接口是否更合理

抽象类与接口的区别是:
抽象类可以包含一些方法的实现,接口则不允许。
一个类可以同时实现多个接口但是只能继承一个抽象类。这一点的话,让类的层次性更强,既是优点也是缺点。当实体模型的层次不是那么明显,抽象类或接口仅仅用来表示功能的时候,用接口更加灵活可控,抽象类因为单继承的关系,如果需要多个“功能”能就需要多个不同抽象类相互继承,灵活性比较差。

接口仅用于定义类型

不要试图把常量写到接口里,会污染继承接口类的命名空间。用枚举或者常量类。

用策略模式代替函数指针

所谓的策略举个例子如下:

使用匿名类实现,如果需要调用多次被重复使用的话,通常的做法是实现为私有静态成员类,通过共有静态成员final域被导出。