说明
这里是阅读《Effective Java中文版第二版》的读书笔记,这里会记录一些个人感觉稍微有些重要的内容,方便以后查阅,可能会因为个人实力原因导致理解有误,若有发现欢迎指出。一些个人还不理解的会用斜线标注。
第一章是引言,所以跳过。
第二章 创建和销毁对象
第1条:考虑用静态工厂方法代替构造器
含义
静态工厂方法是指一个返回类的实例的静态方法,例如:
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean,FALSE;
}
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean,FALSE;
}
优点
相对于一个类的构造器,静态工厂方法的名称没有限制。
众所周知,构造器的方法名是必须和类名一样的,因此对于有多个参数类型相同的构造方法,一种方法是更改参数的顺序,另一种是增加一个flag来判断执行哪个构造方法。但是这样对于使用者是不友好的,他必须熟悉API或者查阅开发文档。倘若使用静态工厂方法,那么可以通过方法名来给予使用者良好的提示与说明。
不用再每次调用的时候创建一个新的对象。
这句话的典型应用是在设计模式的单例模式中,静态工厂方法能够为重复的调用返回相同的对象。
静态工厂方法可以返回原返回类型的任何子类型的对象。
构造方法是不能使用return语句的,它在使用时也只能产生自身这个类的一个对象,而静态工厂方法可以使用return语句,因此在选择返回对象时就有了更大的灵活性。这个优势的应用很多,比如服务提供者框架模式。
小结
应当熟悉静态工厂方法和构造器的各自的长处,在合适的场景使用合适的方法。
第2条:遇到多个构造器参数时要考虑用构建器
在面对一个拥有多个属性的类且构造方法拥有多个可选参数时,一个常见的方法是使用重叠构造器模式(创建多个构造方法,每个构造方法比前一个构造方法有新的参数)。例如,第一个构造方法有两个必须参数,第二个构造方法有两个必须参数和一个可选参数,第三个构造方法有两个必须参数和两个可选参数,以此类推。但是当有许多参数的时候,代码会变得很难编写,也很难阅读,甚至会容易出错。
另一个方法是使用javabean模式。因为构造过程被分到了多个调用中(为每个属性的赋值调用该属性的set方法),在构造过程中,javabean可能处于不一致的状态,这种问题难以发现。
第三种方法就是构建器模式(Builder模式)的一种形式。
public class NutritionFacts {
private final int servingSize;
private final int servings;
private final int calories;
private final int fat;
private final int sodium;
private final int carbohydrate;
public static class Builder {
// 必须属性
private final int servingSize;
private final int servings;
// 可选属性
private int calories = 0;
private int fat = 0;
private int sodium = 0;
private int carbohydrate = 0;
public Builder(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}
public Builder setCalories(int calories) {
this.calories = calories;
return this;
}
public Builder setFat(int fat) {
this.fat = fat;
return this;
}
public Builder setSodium(int sodium) {
this.sodium = sodium;
return this;
}
public Builder setCarbohydrate(int carbohydrate) {
this.carbohydrate = carbohydrate;
return this;
}
public NutritionFacts build() {
return new NutritionFacts(this);
}
}
private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
}
// 使用方法
NutritionFacts n = new NutritionFacts.Builder(200,10).setCalories(20).setFat(30).build();
public class NutritionFacts {
private final int servingSize;
private final int servings;
private final int calories;
private final int fat;
private final int sodium;
private final int carbohydrate;
public static class Builder {
// 必须属性
private final int servingSize;
private final int servings;
// 可选属性
private int calories = 0;
private int fat = 0;
private int sodium = 0;
private int carbohydrate = 0;
public Builder(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}
public Builder setCalories(int calories) {
this.calories = calories;
return this;
}
public Builder setFat(int fat) {
this.fat = fat;
return this;
}
public Builder setSodium(int sodium) {
this.sodium = sodium;
return this;
}
public Builder setCarbohydrate(int carbohydrate) {
this.carbohydrate = carbohydrate;
return this;
}
public NutritionFacts build() {
return new NutritionFacts(this);
}
}
private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
}
// 使用方法
NutritionFacts n = new NutritionFacts.Builder(200,10).setCalories(20).setFat(30).build();
Builder模式十分灵活,可以利用一个builder来创建多个相同的对象,并且对必须参数和可变参数的实现符合人类的正常思维。另外,对于使用者而言,使用时的代码更容易阅读和编写。
这种方法我在google的protobuf的java实现中见到过。
第3条:用私有构造器或者枚举类型强化Singleton属性
私有构造方法就不提了,这里记录一下第二个:
public enum A {
INSTANCE;
public void leaveTheBuilding() {...}
}
public enum A {
INSTANCE;
public void leaveTheBuilding() {...}
}
第4条:通过私有构造器强化不可实例化的能力
对于一些只包含静态方法或者静态属性的类(比如工具类),我们不希望他们被实例化。众所周知,在缺少显式构造方法的时候,编译器会默认添加一个无参的构造方法。如果为了严谨,我们可以添加一个私有的构造方法,更可以在这个构造方法中throw异常来中止程序。
第5条:避免创建不必要的对象
一般来说,最好能重用对象而不是在每次需要的时候就创建一个相同功能的新对象。
除了重用不可变的对象之外,也可以重用那些已知不会被修改的可变对象。
能使用基本数据类型,就尽量不要用对应的封装类。
第6条:消除过期的对象引用
不能以为有了垃圾回收机制后,就不需要考虑内存管理的事情了。
例如用数组来实现栈,当实现出栈操作,size-1后,栈顶坐标后的元素对使用者来说就已经是无效部分了,但是数组仍然拥有对它们的引用,因此垃圾回收机制不会将它们回收。解决办法是在出栈时,将引用置空。
第7条:避免使用终结方法
除了特定情况,不要使用终结方法(finalize)。
子类覆盖了父类的终结方法后,子类的终结方法不会自动调用父类的终结方法,需要手动调用。
第三章 对于所有对象都通用的方法
第8条:覆盖equals请遵守通用约定
约定的内容:
equals方法实现了等价关系。
- 自反性:对于任何非null的引用值x,x.equals(x)都必须返回true。
- 对称性:对于任何非null的引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true。
- 传递性:对于任何非null的引用值x、y和z,如果x.equals(y)返回true,并且y.equals(z)也返回true,那么x.equals(z)也必须返回true。
- 一致性:对于任何非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)就会一致地返回true,或者一致地返回false。
- 非空性:对于任何非null的引用值x,x.equals(null)必须返回false。
实现高质量equals方法的诀窍:
- 使用==操作符检查“参数是否为这个对象的引用”。如果是,则返回true。
- 使用instanceof操作符检查“参数是否为正确的类型”。如果不是,则返回false。
- 把参数转化为正确的类型。因为转换前进行过instanceof测试,所以确保会成功。
- 对于该类中的每个“关键”字段,检查参数中的字段是否与该对象中对应的字段相匹配。如果这些测试全部成功,则返回true;否则返回false。
- 当你编写完成了equals方法之后,应该质问自己并且测试这三个问题:它是否是对称的、传递的、一致的?当然,equals方法也必须满足自反性和非空性,不过通常都会自动满足。
一个简单的列子:
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof MyClass))
return false;
MyClass obj = (MyClass) o;
return obj.field0 == this.field0 && obj.field1 == this.field1;
}
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof MyClass))
return false;
MyClass obj = (MyClass) o;
return obj.field0 == this.field0 && obj.field1 == this.field1;
}
告诫:
- 覆盖equals时总要覆盖hashCode。
- 不要企图让equals方法过于智能。
- 不要将equals声明中的Object对象替换为其他的类型。
public boolean equals(MyClass o); // Don't do this!
public boolean equals(MyClass o); // Don't do this!
第9条:覆盖equals时总要覆盖hashCode
如果没有共同覆盖equals方法和hashCode方法,那么该类将无法结合所有基于散列的集合一起正常运作,这样的集合包括HashMap、HashSet和HashTable。
约定:相等的对象必须具有相等的散列码(HashCode)。
在散列码的计算过程中,必须排除equals比较计算中没有用到的任何字段,可以把冗余字段(它的值可以根据参与计算的其他字段计算出来)排除在外。
不要试图从散列码计算中排除掉一个对象的关键部分来提高性能。
第10条:始终要覆盖toString
提供好的toString实现可以使类用起来更加舒适。
第11条:谨慎地覆盖clone
如果你继承了一个实现了Cloneable接口的类,那么你除了实现一个行为良好的clone方法外,没有别的选择。否则,最好提供某些其他的途径来代替对象拷贝,或者干脆不提供这样的功能。
另一个实现对象拷贝的好方法是提供一个拷贝构造方法或者拷贝工厂。
// 拷贝构造方法
public MyClass(MyClass mc);
// 拷贝工厂
public static MyClass newInstance(MyClass mc);
// 拷贝构造方法
public MyClass(MyClass mc);
// 拷贝工厂
public static MyClass newInstance(MyClass mc);
第12条:考虑实现Comparable接口
类实现了Comparable接口,就表明它的实例具有自然顺序关系(natural ordering)。
约定:(符号sgn(表达式)表示数学中的signum函数,根据表达式的值为负值、零和正值,分别返回-1、0和1)
- 必须确保所有的x和y都满足
sgn(x.compareTo(y)) == -sgn(y.compareTo(x))
。(这也意味着,当且仅当y.compareTo(x)抛出异常时,x.compareTo(y)才必须抛出异常) - 必须确保这个比较关系是可传递的。
x.compareTo(y) > 0 && y.compareTo(z) > 0
成立意味着x.compareTo(z) > 0
。 - 必须确保
x.compareTo(y) == 0
意味着所有的z都满足sgn(x.compareTo(z)) == sgn(y.compareTo(z))
。 - 强烈建议
(x.compareTo(y) == 0) == (x.equals(y))
,但这绝非必要。若违反了这个条件,应当给予说明。
比较浮点字段用Double.compare或者Float.compare。
如果一个类有多个关键字段,按照什么样的顺序来比较是非常重要的。
compareTo方法中,如果两个对应字段不相等,可以使用该类的字段与传入参数的字段的差值作为返回值,但应确保差值是绝对正确的。