《Effect Java》第二章"创建和销毁对象”笔记
第一条:考虑用静态工厂方法代替构造器
首先要指明一个误区:静态工厂方法不是指的设计模式里面的工厂方法,他是指以静态方法的形式创建对象(工厂就是用来创建对象的),例如:
public static User createNormalUser(){
return new User();
}
public static User createVIPUser(){
return new User(100);
}
书中描述这样做的优点有以下几点:
1.他们有名称。通过以上例子就能看出来,有无名称的好处,创建普通用户和创建VIP用户的区别仅仅在于创建是是否传入参数,显然静态工厂方法更容易使用,产生的客户端代码也更易阅读。
2.不必在每次调用它们的时候创建一个新对象。这条指的是单例模式的用法。
3.它们可以返回类型的任何子类型的对象。这句话不太好理解,最起码我是这样。刚开始我以为是向上转型,可是转念一想,向上转型还需要静态工厂方法?然后仔细一看是返回的子类对象。像Collections里的方法:
public static <T> List<T> unmodifiableList(List<? extends T> list) {
return (list instanceof RandomAccess ?
new UnmodifiableRandomAccessList<>(list) :
new UnmodifiableList<>(list));
}
不管是UnmodifiableRandomAccessList类还是UnmodifiableList类,它们都是在Collections里面的内部类。Collections里面包含大量的这样的内部类。书中也说明了这样的好处,可以返回对象,同时又不会使对象的类变为共有,这样使得实现类变得非常简洁。大家可以自己去查看Collections类去体会一下。
4.在创建参数化类型实例的时候,他们使得代码更简单。书中例子:
//以前版本
Map<String, List<String>> m = new HashMap<String, List<String>>();
//改为静态工厂后
public static <K, V> HashMap<K, V> newInstance() {
return new HashMap<K, V>();
}
//实例化
Map<String, List<String>> m = HashMap.newInstance();
其实这个优点对于现在的JDK版本来说也不叫优点了。
//1.7之后
Map<String, List<String>> m =new HashMap<>();
书中阐述的静态工厂方法缺点有两个。
1.类如果不含共有的或者受保护的构造器,就不能被实例化。例如Collections里面的UnmodifiableRandomAccessList和UnmodifiableList就不能被实例化。
2.与其他静态方法没有任何区别。所以无法区别哪些是静态工厂方法和其他静态方法。书中鼓励用命令规范来弥补这个缺点。以下是常用名称:
valueOf(),of(),getInstance(),newInstance(),getType(),newType()
第二条:遇到多个构造器参数时要考虑选用构建器
大家肯定都遇到过这种情况:
public Human(int height, int age) {
this.height= height;
this.age= age;
}
public Human(int age) {
this.age= age;
}
public Human() {}
public static void main(String[] args) {
Human h= new Human(170,17);
}
当创建一个对象时,对于某些可选的参数属性,大多数人都是用重载构造器来实现,也就是书中所述重叠构造器。这样做的缺点在于创建对象的时候,表达不清晰,容易出错。例如new Human(170,17),如果你把170和17填返了,那么程序还是会正常运行的。对于参数更多的对象,可能一点不小心就会造成一些微妙的错误,而且程序并不能给我们这些错误任何提示。
然后书中阐述了另外一种方式,javabean模式。即:
private int height;
private int age;
public void setHeight(int height) {
this.height = height;
}
public void setAge(int age) {
this.age = age;
}
public static void main(String[] args) {
Human h= new Human();
h.setHeight(170);
h.setAge(17);
}
这样的好处在于设置参数的时候有了名字,这样就大大减轻了出错的几率。阅读起来也很容易。可是这也同样有着严重的缺陷,因为构造被分到了几个调用中,所以可能会出现线程安全的问题。实例化类应该是一气呵成的。
因此书中阐述了第三个解决方案:构建器
public class Human {
private int height;
private int age;
private String body;
public Human(Builder builder) {
this.age = builder.age;
this.body = builder.body;
this.height = builder.height;
}
static class Builder{
private int height=0;
private int age;
private String body="";
public Builder(int age) {
this.age = age;
}
public Builder setHeight(int height) {
this.height = height;
return this;
}
public Builder setAge(int age) {
this.age = age;
return this;
}
public Builder setBody(String body) {
this.body = body;
return this;
}
public Human build(){
return new Human(this);
}
}
public static void main(String[] args) {
Human human = new Human.Builder(17).setHeight(170).build();
}
}
这样优点在于既能保证重叠构造器的安全性,又能保证javaBean模式的可读性。完美融合了前面两种的优点。而且还能保证某些必要的参数,例如age属性。构建器的缺点在于创建对象的时候,必须先创建Builder构建器,增加了创建对象的额外开销。
第三条:用私有构造器或者枚举类型强化Singleton属性
书中阐述了单例模式的其中一种饿汉式的实现来作例子, 饿汉式是保证线程安全的一种单例模式实现方式。但是,书中通过序列化和序列化,让这种单例模式产生了多个实例。解决序列化产生的多个实例的方案是加上readResolve()方法;
然后书中也推荐了用枚举来实现单例模式,枚举拥有固定个数的实例,拥有与普通类相同的权限,除了不能继承。
public enum SingletonDemo {
INSTANCE1("demo1");
SingletonDemo(String name) {
this.name = name;
}
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
//normal method
public void method(){}
}
第四条:通过私有构造器强化不可实例化的能力
书中阐述了工具类等全静态方法类不必让类可以实例化。但是,企图将类做成抽象类来阻止用户实例化是行不通的,因为这会引导用户去继承这些类。所以,正确的做法是私有构造器;
第五条:避免创建必须要的对象
书中的观点是在能重用的对象的情况下,就没必要重新消耗性能去创建一个新对象。
例如书中的第一个例子:
//错误方式,每次都会产生一个demo字符串对象
String s=new String("demo");
//正确方式,只会产生一个demo字符串对象
String d="demo"
对于同时提供静态工厂方法和构造器的不可变类,通常使用静态工厂方法来创建类,例如Boolean.valueOf(""),大家可以去看看这段代码的源代码,很简单,并且也能给你带点感悟。
除了重用不可变的对象之外,也可以重用那些已知不会被修改的可变对象。然后书中也举了一个时间的例子,很简单,我就不列举代码了,大概讲讲就行了。反例代码是一个方法,用来检验这个人是否是在一个时间区间类出生。因为区间是确定的,但是这个反例代码每次调用的时候都会生成区间的两个Date对象和一个Calendar对象。正因为时间区间是确定的,所以两个Date对象和一个Calendar对象都可以复用,正确的做法应该是把这两个对象改为静态的成员变量以达到复用的效果。
书中还阐述了Map中复用代码的情形,HashMap的keySet()方法,源码如下(很明显的复用对象):
public Set<K> keySet() {
Set<K> ks = keySet;
if (ks == null) {
ks = new KeySet();
keySet = ks;
}
return ks;
}
接着书中举了个自动装箱,就因为for循环里面把Long写成了long,不经意间创建了大量的中间对象:
Long sum=0L; // Long.valueOf(0L)
for(long i=0;i<Integer.MAX_VALUE;i++){
sum += i; //sum=Long.valueOf(sum.longValue()+i)
}
注释后面已经写出来了实际的执行代码;Long.valueOf()方法在除开缓存起来的-128到127之外的值都会产生一个Long对象,所以说要当心无意识的自动装箱,源码如下,:
public static Long valueOf(long l) {
final int offset = 128;
if (l >= -128 && l <= 127) { // will cache
return LongCache.cache[(int)l + offset];
}
return new Long(l);
}
书中最后说了本小节阐述的并非是尽可能的避免创建新对象,而是要通过具体情况自己分析,例如数据库连接池,创建对象是重量级的。总而言之,具体情况具体分析。
第六条:消除过期对象的引用
书中的例子:
public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
return elements[--size];
}
size是个成员变量,每当调用pop()方法的时候,就会弹出对象并指向前一个对象。但是,弹出的对象引用并没有被消除。所以,正确的做法应该是:
public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
Object result = elements[--size];
elements[size] = null; //消除对象引用
return result;
}
书中称在支持垃圾回收机制的语言中,内存泄露被称作是“无事实的对象保持”。并且清空对象应用应该是一种例外,而不是一种规范。消除过期引用最好的方法是让包含引用的变量结束其生命周期。书中还阐述了内存泄露的几个常见来源,第一个来源是自己管理的内存,第二个来源是缓存,第三个来源是监听器和其他回调。
第七条:避免使用终结方法
这个问题在关于JAVA方面的书籍当中大量存在。因为终结方法有个致命的缺点,JAVA语言规范不仅不能保证终结方法会被及时执行,而且根本不能保证他们会被执行。所以终结方法通常是不可预测的,也是很危险的,一般情况下是不必要的。
书中也对终结方法做了总结性的描述:对于终结方法,一般代码中并不会使用,如果要使用一定要考虑上面两种用途是否值得去做,万万不应该依赖终结方法来更新重要的持久状态。
上一篇: Nginx配置以及域名转发