Effective Java章节笔记
1> 对象的创建和销毁
1.1> 优先考虑使用静态工厂方法代替构造器
静态工厂方法相对于构造器的优势 | 描述 |
---|---|
静态工厂方法可以自定义名称 | 常用的构造器名称只能和类名相同,而静态工厂方法则可以拥有更有意义更有指向性的名称。 |
不必在每次调用时都创建一个新对象 | 在需要一个对象时,将预先创建好的对象返回,或者将构建好的对象缓存起来重复利用。 |
可以返回任何子类型的对象 | 静态工厂方法可以根据传入的参数决定返回特定的子类型对象,使得代码更灵活。 |
1.2> 遇到多个构造器参数时要考虑使用构建器(builder)
当有需要使用多个参数构造一个对象时,最容易想到的方法是:重叠构造器,首先创建一个全属性构造器,然后一一创建所需的去掉某些可选属性的构造器。如下我写了仅仅八个属性的Person类,并使用重叠构造器方法为其产生多个适用于不同场景的构造器:
public class Person {
// 必要属性
private final String id;
private final String name;
// 可选属性
private byte age;
private char gender;
private float stature;
private float weight;
private String nativePlace;
private String nation;
// 包含所有属性的构造器
public Person(String id, String name, byte age, char gender,
float stature, float weight, String nativePlace, String nation) {
super();
this.id = id;
this.name = name;
this.age = age;
this.gender = gender;
this.stature = stature;
this.weight = weight;
this.nativePlace = nativePlace;
this.nation = nation;
}
// 下面为去掉某些可选属性的构造器
public Person(String id, String name, byte age, char gender,
float stature, float weight, String nativePlace) {
this(id, name, age, gender, stature, weight, nativePlace, null);
}
public Person(String id, String name, byte age, char gender,
float stature, float weight) {
this(id, name, age, gender, stature, weight, null);
}
public Person(String id, String name, byte age, char gender) {
this(id, name, age, gender, 0, 0);
}
public Person(String id, String name, byte age) {
this(id, name, age, '男');
}
public Person(String id, String name) {
this(id, name, (byte)0);
}
}
重叠构造器是最容易想到的方法了,但在属性较多的时候会使得代码难以阅读,且难以编写,一长串类型相同的属性也容易产生微妙的错误,例如参数位置填错但它们类型相同,编译器根本无法检查出错,但在运行中程序逻辑必定有问题。而比它稍微高级些的,就是使用JavaBean模式来编写这个类。在这种模式下,首先调用无参构造函数来创建对象,然后使用setter方法来一一为其中属性赋值:
public class Person {
// 必要属性。请注意这里无法使用final修饰符
private String id;
private String name;
// 可选属性
private byte age;
private char gender;
private float stature;
private float weight;
private String nativePlace;
private String nation;
// 无参构造函数
public Person() {
super();
}
// setter
public void setId(String id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
public void setAge(byte age) {
this.age = age;
}
public void setGender(char gender) {
this.gender = gender;
}
public void setStature(float stature) {
this.stature = stature;
}
public void setWeight(float weight) {
this.weight = weight;
}
public void setNativePlace(String nativePlace) {
this.nativePlace = nativePlace;
}
public void setNation(String nation) {
this.nation = nation;
}
}
JavaBean模式也是比较常用的方法,它弥补了重叠构造器方法代码难以阅读的问题,同时也不会出现传参错误。但它有自己的缺陷:
1、在javabean的构造过程中,它可能处于不一致的状态。即构造对象与属性赋值操作被分离开,这使得调用者可能获取到尚未完成赋值操作的对象,程序将会出现莫名其妙的错误,而且java异常栈指示的错误位置往往并非其真实位置;
2、javabean使得将类设置为不可变的操作更为困难。通常我们只需要在规定为不可变的属性上使用final修饰符,但javabean不允许这么做,因为它的逻辑是先构造后赋值。
现在就可以引入第三种方式:构建器模式(builder),它既能保证重叠构造器法的安全性,亦能保证javabean模式的可读性,还可以支持不可变的类构造对象。builder模式并不直接生成想要的对象,而是让调用者利用所有必要的参数调用构造器(或者静态工厂),先得到一个builder对象,然后在builder对象上调用类似于setter的方法,来设置每个相关的可选参数。最后再调用无参的build方法来生成想要的不可变的对象,如此,就保证了先赋值后构造的逻辑。这个builder是它构建的类的静态成员类。如下:
public class Person {
// 必要属性
private final String id;
private final String name;
// 可选属性
private final byte age;
private final char gender;
private final float stature;
private final float weight;
private final String nativePlace;
private final String nation;
// 内部构建器builder类
public static class Builder {
// 构建器中属性与外部类相同,因为外部类构造器/静态工厂方法还需要使用构建器来为属性赋值
// 必要属性
private final String id;
private final String name;
// 可选属性
private byte age;
private char gender;
private float stature;
private float weight;
private String nativePlace;
private String nation;
public Builder(String id, String name) {
this.id = id;
this.name = name;
}
public Builder age(byte age) {
this.age = age;
return this;
}
public Builder gender(char gender) {
this.gender = gender;
return this;
}
public Builder stature(float stature) {
this.stature = stature;
return this;
}
public Builder weight(float weight) {
this.weight = weight;
return this;
}
public Builder nativePlace(String nativePlace) {
this.nativePlace = nativePlace;
return this;
}
public Builder nation(String nation) {
this.nation = nation;
return this;
}
// 使用builder对象构造外部类对象。
public Person build() {
return new Person(this);
}
}
// 使用builder对象为属性赋值
public Person(Builder builder) {
super();
this.id = builder.id;
this.name = builder.name;
this.age = builder.age;
this.gender = builder.gender;
this.stature = builder.stature;
this.weight = builder.weight;
this.nativePlace = builder.nativePlace;
this.nation = builder.nation;
}
}
后续可以使用下列方法创建参数可选的Person类对象:
Person person = new Person.Builder("id", "name")
.age((byte)0).gender('男').nation("汉").build();
不过builder模式就一定完美吗?不,要想构造一个对象就要先创建它的构建器builder,在对性能要求很高时,就值得考量了。它的代码比重叠构造器还要冗长,因此它只在参数数量较多时使用,比如四个以上。特别是在很多参数都为可选的时候,builder模式应该是优先考虑的。使用构建器使得代码更易于阅读和编写,同时保证安全性。
1.3> 用私有构造器或枚举类型强化单例模式(singleton)
当需要系统只持有某个类的唯一对象时,比如文件系统、任务管理器等。可以使用单例模式。实现单例模式有两种常用方法,它们都需要将构造器私有化:
法1、将构造器私有化,并暴露出final修饰的公有化静态成员以供使用。 如下:
public class SingletonModel1 {
public static final SingletonModel1 INSTANCE = new SingletonModel1();
private SingletonModel1() {
}
}
法2、将构造器私有化,并通过静态工厂方法来暴露出实例。如下:
public class SingletonModel1 {
private static final SingletonModel1 INSTANCE = new SingletonModel1();
private SingletonModel1() {
}
public static SingletonModel1 getInstance() {
return INSTANCE;
}
}
但是调用者可以通过反射机制来攻击此单例模式,先获取此类的构造器,然后通过setAccessible()
方法使得构造器可访问。因此我们需要稍作修改,来防止此类攻击:
public class SingletonModel1 {
private static final SingletonModel1 INSTANCE = new SingletonModel1();
// 全局检测开关,第一次创建对象时关闭,防止反射二次创建对象。
private static boolean isFirst = true;
private SingletonModel1() {
synchronized (SingletonModel1.class) {
if (isFirst) {
isFirst = false;
} else {
throw new RuntimeException("抱歉,此操作会破坏单例模式,已终止。");
}
}
}
public static SingletonModel1 getInstance() {
return INSTANCE;
}
}
而不仅反射能攻击单例模式,反序列化操作也可以生成一个新的实例对象从而破坏单例模式。因此我们需要在单例模式设计的类中添加readResolve()
方法来防止单例模式被反序列化破坏。可以理解为只要JVM检测发现待反序列化的类中带有readResolve()
方法,就会调用该方法按程序员的指定逻辑来执行反序列化操作:
private Object readResolve(){
return INSTANCE;
}
再等等,不仅仅反序列化能破坏单例模式,克隆操作也能如此!因此我们还需要在单例模式类中重写clone方法,使其直接返回已创建的实例:
@Override
protected Object clone() throws CloneNotSupportedException {
return INSTANCE;
}
至此,一个完整的单例模式类就构建出来了。而在Java1.5版本后,我们还有第三种方式来实现单例模式。那就是使用单元素的枚举类型。它堪称“实现Singleton的最佳方法”。首先枚举类型实例创建默认即是线程安全的, 同时防止反射创建对象的可能,并且反序列化时依然为单例。
public enum SingletonModel2(){
INSTANCE;
}
1.4> 通过私有构造器强化不可实例化的能力
通常,我们会将只含有静态方法和静态域的工具类规定为“不可实例化”的。而将类做成抽象类是不行的,因为可以很容易地使用子类来继承而后实例化。因此,我们需要彻底地将不想被实例化的类的构造器私有化,禁止其他类访问。如下:
public class DontInstantiable {
// 抛出断言错误终止程序,以禁止本类中使用构造器。而private修饰符同时禁止外部类使用构造器
private DontInstantiable() {
throw new AssertionError();
}
}
1.5> 避免创建不必要的对象
如果一个对象是不可变的,那它就始终可以被重用。因此对于同时提供了构造方法和静态工厂方法的类,应该使用它的静态工厂方法,我们也应给自己编写的不可变类提供静态工厂方法;
如果一个对象是可变的,但它后续不会被修改,那么我们应该将它放进静态代码块中,而不是每次调用就创建一个新的。尤其是Calendar类的实例,创建代价极其昂贵。例如:
// 最原始的写法,每次调用isPost_90方法时都会产生Calendar实例,效率极低。
public class Person {
private final Date birthDate;
public boolean isPost_90() {
Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
cal.set(1990, Calendar.JANUARY, 1, 0, 0, 0);
Date startDate = cal.getTime();
cal.set(2000, Calendar.JANUARY, 1, 0, 0, 0);
Date endDate = cal.getTime();
return birthDate.compareTo(startDate) >= 0 &&
birthDate.compareTo(endDate) < 0;
}
}
// 优化后,将不需要修改的可变对象放入静态代码块中,只在类加载时创建一次。
/*
* 在这里讨论一下延迟初始化的情况:将代码放入静态代码块中时,每次类加载会实例化Calendar一次,
* 然而假设并没有调用isPost_90方法,Calendar依然会实例化一次。
* 而延迟初始化甚至可以做到只在第一次调用isPost_90方法时实例化Calendar。
* 但实际上并不提倡这样做,因为延迟初始化会使方法的实现更加复杂,从而无法在此基础上显著提升性能
*/
public class Person {
private final Date birthDate;
private static final Date START_DATE;
private static final Date END_DATE;
static {
Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
cal.set(1990, Calendar.JANUARY, 1, 0, 0, 0);
Date startDate = cal.getTime();
cal.set(2000, Calendar.JANUARY, 1, 0, 0, 0);
Date endDate = cal.getTime();
}
public boolean isPost_90() {
return birthDate.compareTo(START_DATE) >= 0 &&
birthDate.compareTo(END_DATE) < 0;
}
}
而在Java1.5版本后,还有一种隐式地创建多于对象的方法:自动装箱。自动装箱使得基本类型和包装类型可以混用,但它们在性能上依然有比较明显的差别,因为包装类型会根据基本类型创建一次包装类型的实例对象。因此要优先使用基本类型,当心无意识地大量自动装箱。例如:
/*
* 此代码在+= i操作时,每次都会自动装箱创建一个Long类型实例,因此将有2^31个多余的对象被创建。
* 优化仅仅是将sum改为long类型
*/
Long sum = 0L;
for( long i = 0L; i < Integer.MAX_VALUE; i ++) {
sum += i;
}
不必因此而觉得“创建对象就是代价昂贵的,我们要尽可能避免创建对象,那么我们来使用对象池吧”,事实上小对象的创建和销毁都是很廉价的。所以如果想要维护一个自己的对象池,那么必须确保这些对象是非常重量级的,比如数据库连接池。而轻量级对象池可以说是得不偿失,JVM自身的垃圾回收器轻易就能超过轻量级对象池的性能。
1.6> 消除过期的对象引用
消除过期的对象引用是针对于:无意识的对象保持来说的,即内存泄漏。内存泄漏有三种常见场景:栈、缓存、监听器和回调。
栈:讲栈事实上是指所有会自己管理内存的类,栈只是其中一个例子。即在一个元素出栈后,栈依然会维持该元素的引用,这也导致了垃圾回收器认为该引用依然有效,从而不回收该内存区域。内存泄露就产生了。这种情况的解决办法也很简单,那就是元素出栈或者释放后,程序员再手动将该引用置空。这样不仅垃圾回收器能回收该内存区域,而且在以后对该引用有错误的运用时,程序会报NullPointerException
,而不是悄悄地运行下去导致你抓一晚上虫子的惨剧。
缓存:缓存主要是需要判断其中项是否有意义来解决内存泄漏问题。有以下几种情况:
1、当所要缓存的项的生命周期由该键的外部引用而不是由值决定的,则可以使用WeakHashMap
;
2、当所要缓存的项的价值随时间推移而降低,那么我们需要给缓存一个后台线程来周期性清理过期项。或者在添加新项目时同时清理,比如LinkedHashMap
类利用removeEldestEntry()
方法清理,其他复杂缓存则必须使用java.lang.ref
;
回调:当一个客户端在API中注册了一个回调后,又没有取消注册,那么除非API自主清理这些无用对象,否则也会不断产生内存泄漏。而确保回调能立即被当作垃圾回收的方法是只维持它的弱引用,比如只将它们保存为WeakHashMap
的键。
要检测内存泄漏可以使用Heap剖析工具。
1.7> 避免使用终结方法finalizer
因为终结方法通常是不可预测的,也就是指自从调用终结方法后,它并不会立刻执行,甚至根本不保证被执行,从调用开始到真正执行的时间间隔是任意长的。且不同的JVM中终结方法的实现是大相径庭的,因此它会使得程序不稳定、性能降低、可移植性下降。也不要使用System.gc()
和System.runFinalization()
这两个方法,因为它们确实增加了终结方法执行的机会,但是依然不保证一定被执行。
而终结方法还有一个麻烦是:当未被捕获的异常在终结过程中被抛出,那么该异常会被忽略,程序不会停止,不会打印异常栈,并且终结过程也会中断。此时对象会处于中断状态,而且因为未被终结,所以依然可以调用到它,调用它会导致任何不确定的行为。
如果需要终止某个资源,那么只需提供一个显式的终止方法,并要求该类的客户端在每个实例不再有用的时候调用这个方法,在一个私有域中记录下自己是否已经被终止了。其他的方法就必须检查这个域后使用,如果发现已经被终止,则抛出异常。典型例子:Java各种流中的close()
方法。显式终止方法需要配合try-catch
块使用,以保证即使有异常抛出,显式终结方法也能被调用。
而终结方法为什么还存在呢?因为它还是有两种合法用途:
1、当客户端忘记调用显示的终结方法时,终结方法可以充当安全网。毕竟迟一些终结比永远不终结好些。但如果终结方法发现资源还未终止,应该在日志中记录一条警告,方便向客户端报告这个bug,它应该被修复;
2、当普通对象通过本地方法委托给一个本地对象,就形成了本地对等体。而本地对等体不是一个普通对象,因此垃圾回收器不会知道它。当本地对等体被回收的时候,其中的本地对象无法被回收,此时就需要终结方法。
2> 对于所有对象都通用的方法
2.1> 覆盖equals方法时请遵守约定
自反性:对于任何非null的引用x,x.equals(x)
必须返回true;
对称性:对于任何非null的引用x和y,当且仅当x.equals(y)
返回true时,y.equals(x)
必须返回true;
传递性:对于任何非null的引用x,y和z,如果x.equals(y)
返回true,并且y.equals(z)
返回true时,x.equals(z)
也必须返回true;
一致性:对于任何非null的引用x和y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)
返回的结果必须不变。
实现高质量equals方法的诀窍:
1、第一步:使用==
操作符检查“参数是否为本对象的引用”;
2、第二步:使用instanceof
操作符检查“参数是否为正确的类型”;
3、第三步:将参数转换成正确的类型;
4、第四步:对于该类中的每个关键的域,要检查参数中的域是否与该对象中对应的域相匹配;
5、覆盖equals
时总要覆盖hashCode
;
6、不要企图让equals
太过智能,其中的代码逻辑越简单越好;
7、不要将equals
声明中的参数类型:Object
替换为其他类型(当然@Override
注解也不允许这么做);
2.2> 覆盖hashCode规范
1、对象的equals
方法比较操作中用到的数据未被修改,则hashCode
值不变;
2、两对象equals
方法为true
,即相等,则hashCode
值也相等;
3、两对象equals
方法为false
,即不等,hashCode
值不一定不等。
4、equals
方法比较操作中用到的数据也应该被hashCode
计算;
5、冗余域(可被其他关键域计算得到的域)不必也不能被hashCode
计算在内,
下面给出一种简单的解决办法:
1、把某个非零的常数值,比如说17
,保存在一个名为result
的int
类型的变量中;
2、对于对象中每个关键域f
(指equals
方法中涉及的每个域),完成以下步骤:
a.为该域计算int
类型的散列码c
:
i、如果该域是boolean
类型,则计算(f ? 1 : 0
);
ii、如果该域是byte
、char
、short
或者int
类型,则计算(int)f
;
iii、如果该域是long
类型,则计算(int)(f ^ (f >>> 32)
;
iv:如果该域是float
类型,则计算Float.floatToIntBits(f)
。
v:如果该域是double
类型,则计算Double.doubleToLongBits(f)
,然后按照步骤2.a.iii,为得到的long
类型值计算散列值。
vi:如果该域是一个对象引用,并且该类的equals
方法通过递归地调用equals
的方式来比较这个域,则同样为这个域递归地调用hashCode
。如果需要更复杂的比较,则为这个域计算一个范式,然后针对这个范式调用hashCode
。如果这个域的值为null
,则返回0
(或者其他某个常数,但通常是0)。
vii:如果该域是一个数组,则要把每一个元素当做单独的域来处理。也就是说,递归地应用上述规则,对每个重要的元素计算一个散列码,然后根据步骤2.b中的做法把这些散列值组合起来。如果数组域中的每个元素都很重要,可以利用发行版本1.5中增加的其中一个Arrays.hashCode
方法。
b、按照下面的公式,把步骤2.a中计算得到的散列码c
合并到result
中:result = 31 + result + c;
3,返回result
。
4.写完了hashCode
方法之后,问问自己“相等的实例是否都具有相等的散列码”。要编写单元测试来验证你的推断。如果相等的实例有着不相等的散列码,则要找出原因,并修正错误。
2.3> 始终要覆盖toString
应该在注释中说明输出字符串中的各部分的意思,使其他人能快速理解输出。