Effective JAVA 读书笔记

Categories: 工具语言, 读书
Comments: No Comments
Published on: 2013 年 01 月 07 日


一、创建和销毁对象

1、用静态工厂方法替代构造器

优点:跟构造器比有名称;不必每次调用都创建新的实例;可以返回原类型的任何子类型对象;创建参数化类型实例时代码更简洁。
缺点:静态工厂类中如果不含有公有或受保护构造器,不能子类化;与其他静态方法没有任何区别。

2、遇到多个构造器参数时考虑使用构建器

重叠构造器模式可行,但是当有许多参数的时候,客户端代码会很难编写,别且仍然难以阅读。
可以使用JavaBeans模式,先调用一个无参构造器创建对象,然后调用setter方法来设置每个必要的参数或者相关参数。这样的话阻止了类内成员不可变的可能,这需要程序员额外的工作来保证对象的线程安全。
还有第三种替代方法,就是Builder模式,不直接生成想要的对象,让客户端调用必要的参数构造器或静态工厂得到一个builder对象,然后再builder对象上调用类似setter方法来设置可选参数。最后调用无参的build方法来生成不可变的对象。示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// Builder Pattern
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 {
		// Required parameters
		private final int servingSize;
		private final int servings;
		// Optional parameters - initialized to default values
		private int calories = 0;
		private int fat = 0;
		private int carbohydrate = 0;
		private int sodium = 0;
 
		public Builder(int servingSize, int servings) {
			this.servingSize = servingSize;
			this.servings = servings;
		}
 
		public Builder calories(int val) {
			calories = val;
			return this;
		}
 
		public Builder fat(int val) {
			fat = val;
			return this;
		}
 
		public Builder carbohydrate(int val) {
			carbohydrate = val;
			return this;
		}
 
		public Builder sodium(int val) {
			sodium = val;
			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;
	}
}

builder的setter方法返回本身,可以方便的类似如下方式调用:

NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).
calories(100).sodium(35).carbohydrate(27).build();

这样就更容易编码和阅读。同时生成的时候builder也可以检验这些参数。因此在有多参数的类时,并且对效率要求不那么苛刻可以优先选择构造器,而不是很多的构造函数。

3、用私有构造器或者枚举类型强化Singleton属性

常用的单例模式类似如下:

View Code JAVAV
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton {  
 
    private static final Singleton INSTANCE = new Singleton();  
 
    private Singleton() {  
        if(INSTANCE != null) {  
            throw new IllegalStateException("Trying to get more than one instance.");  
        }  
    }  
 
    public static Singleton getInstance() {  
        return INSTANCE;  
    }  
}

其中构造函数私有化,并且内部判断是否已经实例化过,是为了防止享有特权的客户端借助AccessibleObject.setAccessible方法通过反射机制调用时有构造器。但是这种方法不能防止对象序列化之后多次反序列化形成多个实例。

可以使用enum类来轻松解决反序列化破坏单例的现象发生:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
        enum Singleton {   
	    INSTANCE;  
	    private String name;  
	    public String getName() {  
	        return name;  
	    }  
	    public void setName(String name) {  
	        this.name = name;  
	    }  
	    @Override  
	    public String toString() {  
	        return "[" + name + "]";  
	    }  
	}

4、通过私有构造器强化不可实例化的能力

并在私有构造函数内写入抛出执行异常语句。

5、避免创建不必要的对象

6、消除过期对象引用

内存自动回收机制让内存泄漏更隐蔽。如果一个对象废弃,同时应消除这个对象的引用,否则垃圾回收机制不会收回对象的引用。消除办法:myObject=null;即可。平时一两个对象的引用无所谓,但是对于服务型队栈应用,会有较长时间运行,数据结构内部数据对象出现消除,慢慢的对象引用堆积会膨胀,最终导致性能变缓和memory out of limit之类的错误。

内存泄漏的另一个常见来源是缓存。一旦对象引用放到缓存,就容易被遗忘掉。可以使用WeakHashMap来保存这些缓存,当缓存中的项过期,他们的引用自动会被删除。需要记住只有当所要的缓存项的生命期是由该键的外部引用而不是由值决定时,WeakHashMap才有用处。更为常用的是使用java.lang.ref。

内存泄漏的第三个常见来源是监听器和其他回调。如果实现一个API,客户端在这个API中注册回调,却没有显式的取消注册,那么除非采取某些动作,否则他们会堆积。确保回调立即被当作垃圾回收的最佳方式是只保存他们的弱引用(weak reference)。例如WeakHashMap中的键。

7、避免使用终结方法

终结方法(finalizer)通常是不可预测、危险的,一般情况是不必要的。会导致不稳定、降低性能以及可移植性等问题。终结方法不是C++中的析构函数的对应物。终结方法的执行时间是不确定的(一般是对象失去所有引用变成垃圾对象被对象收回器回收时,才调用finalize方法),如用终结方法来关闭已经打开的文件,这是严重的错误,因为直到finalize被执行之前,这个打开文件的描述符一直占用资源。

垃圾回收算法中,执行终结方法的时间点在不同的JVM中有较大不同,会导致移植问题。

终结方法线程的优先级比应用程序其他的线程要低得多,可能会导致堆积终结方法使OutOfMemoryError。Java语言规范不仅不保证终结方法被及时执行,而且根本就不保证他们会被执行。System.gc和System.runFinalization这两个方法确实增加终结方法的执行可能性,但他们同样不保证终结方法一定会被执行。

终结方法中被抛出的异常,那么这种异常可能会被忽略,并且该对象的终结过程也会结束。未被捕获的异常会使对象处于破坏状态,如果另外一个线程企图使用这种破坏对象,则可能发生不确定行为。更致命的是使用终结方法会使程序性能大大降低。

如果要使用终结方法,需要注意的是,在终结方法中调用父类(Object)的终结方法,这样可以保证子类终结方法过程抛出异常也不影响父类的执行。但是这样也不能防止马虎大意的发生或者恶意子类的产生,解决方案是在需要有终结方法的类中写一个匿名类,用来终结它的外围实例,这个匿名类被称为终结方法守卫者。类似:

1
2
3
4
5
6
7
8
9
10
11
// Finalizer Guardian idiom
public class Foo {
	// Sole purpose of this object is to finalize outer Foo object
	private final Object finalizerGuardian = new Object() {
		@Override
		protected void finalize() throws Throwable {
			... // Finalize outer Foo object
		}
	};
	... // Remainder omitted
}

二、对所有对象都通用的方法

8、覆盖equals时请遵守通用约定

a、自反性(reflexivity)
b、对称性(symmetry)
c、传递性(transitivity)
d、一致性(consistency)
e、非空性(Non-nullity)

高质量实现equals方法的诀窍:

a、使用==操作符检查“参数是否为这个对象的引用”。如果是就返回true。
b、使用instanceof操作符检查“参数是否为正常的类型”。如果不是则返回false。
c、把参数转换成正确的类型。
d、对于该类中的每个“关键(significant)”域,检查参数中的域是否与对象中的对应域匹配。有些类的关键域为null可能是合法的,注意判断。

另外覆盖equals方法总要覆盖hashCode方法,并且不要试图让equals方法过于智能,最后不要讲equals声明中的Object对象替换成其他的类型,这就不是覆盖,而是重载了。(使用@Overried来防止大意出错)

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

如果不这样做,就违反Object hashCode的通用约定,从而导致该类无法结合所有基于散列的集合一起正常工作。约定如下:

a、在应用程序执行期间,只要对象的equals方法的比较操作所用到的信息没有改变,那么对这同一对象调用多次hashCode必须返回同一个整数。在同一个应用程序的多次执行过程中,每次执行所返回的整数可以不同。
b、如果两个对象根据equals(Object)方法比较是相等的,那么调用任意一个对象的hashCode返回结果必须相同。
c、如果两个对象equals(Object)方法比较不相等,那么调用这两个对象中任意一个对象的hashCode方法,则不一定要产生不同的不同的整数结果,但是减少碰撞可以提高散列表性能。

所以如果覆盖equals方法不覆盖hashCode方法,则会违反第二条约定。一般覆盖hashCode方法要避免使用equals没有用到的域,否则很有可能违反hashCode第二条约定。

一般如果对象时不可变的,则可以考虑把散列码存储在对象内部,而不是每次请求都计算,或者可以延迟计算,直到第一次被调用时才进行计算。

10、始终要覆盖toString方法

这只是一条建议,但是很好的建议。类被println、printf、字符串连接符以及assert或者被调用打印,都会自动调用toString方法。

11、谨慎覆盖clone

12、考虑实现Comparable接口

四、类和接口

13、使类和成员的可访问性最小化

14、在公有类中使用访问方法而非公有域

15、使可变性最小化

16、复合优先于继承

如果父类子类出于同一程序员,继承是比较安全的;专门为继承设计的父类,有详细文档说明的类继承也是安全的,其他的都不太安全。有可能父类的一些方法实现依赖其他方法(一般文档中也不会详细说明具体的依赖关系),继承后覆盖其中一些方法则可能引发未知错误。即便不覆盖父类方法,也不能确保父类之后的更新是否会更新出子类已有的方法名,又会陷入前一种状态。

比较好的避免上述问题,是不扩展现有类,而是在新的的类中增加一个私有域,它引用现有类的一个实例,这种设计叫做复合(composition)。新类中的每个实例方法都可以调用被包含的类实例中对应的方法,兵返回它的结果,这被成为转发(forwarding),新类中的方法叫做转发方法(forwarding method)。这样的类比较稳固,不依赖现有类的实现细节。

复合也可以方便的再包装已有类添加新功能、继承接口等。复合几乎没有什么缺点,还能很好的屏蔽父类的缺陷。

17、要么为继承而设计,并提供文档说明,要不就禁止继承

18、接口优于抽象类

19、接口只用于定义类型

20、类层次优于标签类

21、用函数对象表示策略

22、优先考虑静态成员类

五、泛型

23、不要再新代码中使用原生态类型

24、消除非受检警告

25、列表优先于数组

26、优先考虑泛型

27、优先考虑泛型方法

28、利用有限制通配符来提升API的灵活性

29、优先考虑类型安全的异构容器

六、枚举和注解

30、用enum代替int常量

31、用实例域代替序数

32、用EnumSet代替位域

33、用EnumMap代替序数索引

34、用接口模拟可伸缩的枚举

35、注解 优于命名模式

36、坚持使用Override

37、用标记接口定义类型

七、方法

第38条:检查参数的有效性
第39条:必要时进行保护性拷贝
第40条:谨慎设计方法签名
第41条:慎用重载
第42条:慎用可变参数(varargs)
第43条:返回零长度的数组或者集合,而不是null
第44条:为所有导出的API元素编写文档注释第8章 通用程序设计
第45条:将局部变量的作用域最小化
第46条:for-each循环优先于传统的for循环
第47条:了解和使用类库
第48条:如果需要精确的答案,请避免使用float和double
第49条:原语类型优先于装箱的原语类型
第50条:如果其他类型更适合,则尽量避免使用字符串
第51条:了解字符串连接的性能
第52条:通过接口引用对象
第53条:接口优先于反射机制
第54条:谨慎地使用本地方法
第55条:谨慎地进行优化
第56条:遵守普遍接受的命名惯例

第9章 异常

第57条:只针对异常的条件才使用异常
第58条:对可恢复的条件使用受检异常,对编程错误使用运行时异常
第59条:避免不必要地使用受检的异常
第60条:尽量使用标准的异常
第61条:抛出与抽象相对应的异常
第62条:每个方法抛出的所有异常都要有文档
第63条:在细节消息中包含失败-捕获信息
第64条:努力使失败保持原子性
第65条:不要忽略异常第10章 并发
第66条:同步访问共享的可变数据
第67条:避免过多同步
第68条:executor和task优先于线程
第69条:并发工具优先于wait和notify
第70条:线程安全性的文档化
第71条:慎用延迟初始化
第72条:不要依赖于线程调度器
第73条:避免使用线程组

第11章 序列化

第74条:谨慎地实现Serializable
第75条:考虑使用自定义的序列化形式
第76条:保护性地编写readObject方法
第77条:对于实例控制,枚举类型优先于readResolve
第78条:考虑用序列化代理代替序列化实例

我猜你可能也喜欢:

No Comments - Leave a comment

Leave a comment

电子邮件地址不会被公开。 必填项已用*标注

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>


Welcome , today is 星期日, 2017 年 11 月 19 日