Effective Java——创建和销毁对象
2015-02-01 13:05
232 查看
目录
一、考虑用静态工厂方法代替构造器
二、遇到多个构造参数时要考虑用构建器(Builder模式)
三、用私有构造器或者枚举类型强化Singleton属性
四、通过私有构造器强化不可实例化的能力
五、避免创建不必要的对象
六、消除过期的对象引用
七、避免使用终结方法
一、考虑用静态工厂方法代替构造器
为了让客户端获取类的一个实例,除了使用公有的构造器,类还可以提供一个公有的静态工厂方法,下面这个方法将boolean基本类型值转换成Boolean对象引用:
静态构造方法与构造器不同有以下主要优势:
1.有意义的名称。当一个类需要多个带有相同签名的构造器时,用静态工厂方法代替构造器,并慎重地选择名称以便突出他们之间的区别。
2.不必在每次调用他们的时候都创建一个新对象。可以使得不可变类使用预先构建好的实例,或将构建好的实例缓存起来,进行重复利用,从而避免创建不必要的重复对象。
3.可以返回原返回类型的任何子类型的对象。在选择返回对象的类时有了更大的灵活性。在Java Collections Framework的集合接口中,提供了大量的静态方法返回集合接口类型的实现类型,如Collections.subList()、Collections.unmodifiableList()等。返回的接口是明确的,然而针对具体的实现类,函数的使用者也无需知晓。
4.在创建参数化类型实例的时候,它们使代码变得更加简洁。调用参数化类的构造器时,要指明类型参数:
使用静态工厂方法编译器可以替你找到类型参数,称作类型推导。假设HashMap提供了这个静态工厂方法:
可以用下面这句简洁的代码代替:
静态工厂方法的缺点:
1.类如果不含有公有的或者受保护的构造器,就不能被继承
2.与其他静态方法没有任何区别,Javadoc工具不会注意到静态工厂方法
静态工厂方法的一些惯用名称:valueOf、of、getInstance、newInstance、getType、newType
二、遇到多个构造参数时要考虑用构建器(Builder模式)
如果类的构造器或者静态工厂中具有多个参数,设计这种类时,一种选择是使用重叠构造器(第一个构造器只有必要参数,第二个构造器有一个可选参数,第三个有两个可选参数,以此类推),一种选择是JavaBeans模式(调用无参构造器来创建对象,然后调用setter方法来设置参数),另一种较好的方法是使用Builder模式:不直接生成想要的对象,而是让客户端利用所有必要的参数构造器得到一个builder对象,客户端在builder对象上调用类似于setter的方法来设置可选参数。
三、用私有构造器或者枚举类型强化Singleton属性
Java中主要有三种单例的创建方式:
1.将构造函数私有化,直接通过静态公有的final域字段获取单实例对象:
这样的方式主要优势在于简洁高效,使用者很快就能判定当前类为单实例类,在调用时直接操作Elivs.INSTANCE即可。但是若Elvis的使用代码被迁移到多线程的应用环境下了,系统希望能够做到每个线程使用同一个Elvis实例,不同线程之间则使用不同的对象实例,那么这种创建方式将无法实现该需求。
2. 通过公有域成员的方式返回单实例对象:
这种方法很好的弥补了第一种方式的缺陷,如果今后需要适应多线程环境的对象创建逻辑,仅需要修改Elvis的getInstance()方法内部即可,对用调用者而言则是不变的,这样便极大的缩小了影响的范围。
3.编写一个包含单个元素的枚举类型:
主函数可直接使用Elvis.INSTANCE.leaveTheBuilding()调用单例中的函数。这种方法简洁清晰,线程安全,是实现Singleton的最佳方法。
四、通过私有构造器强化不可实例化的能力
对于有些工具类如java.lang.Math、java.util.Arrays等,其中只是包含了静态方法和静态域字段,因此对这样的类,实例化就显得没有任何意义了。然而在实际的使用中,如果不加任何特殊的处理,这样的类是可以像其他类一样被实例化的。而企图通过将类做成抽象类来强制该类不可被实例化,是行不通的。因为该类还可以被继承,其子类也可以被实例化。解决方法是,将构造函数设置为private,这样类的外部将无法实例化该类,与此同时,在这个私有的构造函数的实现中直接抛出异常,从而也避免了类的内部方法调用该构造函数。
这样定义之后,该类将不会再被外部实例化了,否则会产生编译错误。然而这样的定义带来的最直接的负面影响是该类将不能再被子类化。
五、避免创建不必要的对象
比较下面的例子,并测试他们在运行时的效率差异:
前者通过静态工厂方法保证了每次返回的对象都是同一个true或false,即valueOf将只会返回Boolean.TRUE或Boolean.FALSE两个静态域字段之一。而后面的Boolean构造方式,每次都会构造出一个新的Boolean实例对象。这样在多次调用后,第一种静态工厂方法将会避免大量不必要的Boolean对象被创建,从而提高了程序的运行效率,也降低了垃圾回收的负担。
继续比较下面的代码:
改进后的Person类只是在初始化的时候创建Calender、TimeZone和Date实例一次,而不是在每次调用isBabyBoomer方法时都创建一次他们。如果该方法会被频繁调用,效率的提升将会极为显著。
六、消除过期的对象引用
Java中也有内存泄露:
在以上代码中,在正常的使用中不会产生任何逻辑问题,然而随着程序运行时间不断加长,内存泄露造成的副作用将会慢慢的显现出来。当我们调用pop方法时,该方法将返回当前栈顶的elements,同时将该栈的活动区间(size)减一,然而此时被弹出的Object仍然保持至少两处引用,一个是返回的对象,另一个则是该返回对象在elements数组中原有栈顶位置的引用。这样即便外部对象在使用之后不再引用该Object,那么它仍然不会被垃圾收集器释放,久而久之导致了更多类似对象的内存泄露。修改方式如下:
推荐在以下3中情形下需要考虑资源手工处理问题:
1.类是自己管理内存,如例子中的Stack类。
2.使用对象缓存机制时,需要考虑被从缓存中换出的对象,或是长期不会被访问到的对象。
3.事件监听器和相关回调。用户经常会在需要时显示的注册,然而却经常会忘记在不用的时候注销这些回调接口实现类。
七、避免使用终结方法
在Java的实际开发中,并非所有的资源都是可以被垃圾回收器自动释放的,如FileInputStream、Graphic2D等类中使用的底层操作系统资源句柄,并不会随着对象实例被GC回收而被释放。在Java中完成这样的工作主要是依靠try-finally机制来协助完成的。然而Java中还提供了另外一种被称为finalizer的机制,使用者仅仅需要重载Object对象提供的finalize方法,这样当JVM的在进行垃圾回收时,就可以自动调用该方法。但是由于对象何时被垃圾收集的不确定性,以及finalizer给GC带来的性能上的影响,因此并不推荐使用者依靠该方法来达到关键资源释放的目的。
Java的语言规范中并没有保证该方法会被及时的执行,甚至都没有保证一定会被执行。即便开发者在code中手工调用了System.gc和System.runFinalization这两个方法,这仅仅是提高了finalizer被执行的几率而已。还有一点需要注意的是,被重载的finalize()方法中如果抛出异常,其栈帧轨迹是不会被打印出来的。在Java中被推荐的资源释放方法为,提供显式的具有良好命名的接口方法,如FileInputStream.close()和Graphic2D.dispose()等。然后使用者在finally区块中调用该方法,见如下代码:
总之,除非是作为安全网,或者是为了终止非关键的本地资源,否则不要使用终结方法。如果使用了终结方法,即在类中覆盖了finalize方法,请记住一定要在finally子句中调用super.finalize()。
一、考虑用静态工厂方法代替构造器
二、遇到多个构造参数时要考虑用构建器(Builder模式)
三、用私有构造器或者枚举类型强化Singleton属性
四、通过私有构造器强化不可实例化的能力
五、避免创建不必要的对象
六、消除过期的对象引用
七、避免使用终结方法
一、考虑用静态工厂方法代替构造器
为了让客户端获取类的一个实例,除了使用公有的构造器,类还可以提供一个公有的静态工厂方法,下面这个方法将boolean基本类型值转换成Boolean对象引用:
public static Boolean valueOf(boolean b) { return b ? Boolean.TRUE : Boolean.FALSE; }
静态构造方法与构造器不同有以下主要优势:
1.有意义的名称。当一个类需要多个带有相同签名的构造器时,用静态工厂方法代替构造器,并慎重地选择名称以便突出他们之间的区别。
2.不必在每次调用他们的时候都创建一个新对象。可以使得不可变类使用预先构建好的实例,或将构建好的实例缓存起来,进行重复利用,从而避免创建不必要的重复对象。
3.可以返回原返回类型的任何子类型的对象。在选择返回对象的类时有了更大的灵活性。在Java Collections Framework的集合接口中,提供了大量的静态方法返回集合接口类型的实现类型,如Collections.subList()、Collections.unmodifiableList()等。返回的接口是明确的,然而针对具体的实现类,函数的使用者也无需知晓。
4.在创建参数化类型实例的时候,它们使代码变得更加简洁。调用参数化类的构造器时,要指明类型参数:
Map<String, String> m = new HashMap<String, String>();
使用静态工厂方法编译器可以替你找到类型参数,称作类型推导。假设HashMap提供了这个静态工厂方法:
public static <K,V> HashMap<K,V> newInstance() { return new HashMap<K,V>(); }
可以用下面这句简洁的代码代替:
Map<String,String> m = MyHashMap.newInstance();
静态工厂方法的缺点:
1.类如果不含有公有的或者受保护的构造器,就不能被继承
2.与其他静态方法没有任何区别,Javadoc工具不会注意到静态工厂方法
静态工厂方法的一些惯用名称:valueOf、of、getInstance、newInstance、getType、newType
二、遇到多个构造参数时要考虑用构建器(Builder模式)
如果类的构造器或者静态工厂中具有多个参数,设计这种类时,一种选择是使用重叠构造器(第一个构造器只有必要参数,第二个构造器有一个可选参数,第三个有两个可选参数,以此类推),一种选择是JavaBeans模式(调用无参构造器来创建对象,然后调用setter方法来设置参数),另一种较好的方法是使用Builder模式:不直接生成想要的对象,而是让客户端利用所有必要的参数构造器得到一个builder对象,客户端在builder对象上调用类似于setter的方法来设置可选参数。
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 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; } } //使用方式 public static void main(String[] args) { NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8) .calories(100).sodium(35).carbohydrate(27).build(); System.out.println(cocaCola); }
三、用私有构造器或者枚举类型强化Singleton属性
Java中主要有三种单例的创建方式:
1.将构造函数私有化,直接通过静态公有的final域字段获取单实例对象:
public class Elvis { public static final Elvis INSTANCE = new Elvis(); private Elivs() { ... } public void leaveTheBuilding() { ... } }
这样的方式主要优势在于简洁高效,使用者很快就能判定当前类为单实例类,在调用时直接操作Elivs.INSTANCE即可。但是若Elvis的使用代码被迁移到多线程的应用环境下了,系统希望能够做到每个线程使用同一个Elvis实例,不同线程之间则使用不同的对象实例,那么这种创建方式将无法实现该需求。
2. 通过公有域成员的方式返回单实例对象:
public class Elvis { public static final Elvis INSTANCE = new Elvis(); private Elivs() { ... } public static Elvis getInstance() { return INSTANCE; } public void leaveTheBuilding() { ... } }
这种方法很好的弥补了第一种方式的缺陷,如果今后需要适应多线程环境的对象创建逻辑,仅需要修改Elvis的getInstance()方法内部即可,对用调用者而言则是不变的,这样便极大的缩小了影响的范围。
3.编写一个包含单个元素的枚举类型:
public enum Elvis { INSTANCE; public void leaveTheBuilding() { ... } }
主函数可直接使用Elvis.INSTANCE.leaveTheBuilding()调用单例中的函数。这种方法简洁清晰,线程安全,是实现Singleton的最佳方法。
四、通过私有构造器强化不可实例化的能力
对于有些工具类如java.lang.Math、java.util.Arrays等,其中只是包含了静态方法和静态域字段,因此对这样的类,实例化就显得没有任何意义了。然而在实际的使用中,如果不加任何特殊的处理,这样的类是可以像其他类一样被实例化的。而企图通过将类做成抽象类来强制该类不可被实例化,是行不通的。因为该类还可以被继承,其子类也可以被实例化。解决方法是,将构造函数设置为private,这样类的外部将无法实例化该类,与此同时,在这个私有的构造函数的实现中直接抛出异常,从而也避免了类的内部方法调用该构造函数。
public class UtilityClass { //Suppress default constructor for noninstantiability. private UtilityClass() { throw new AssertionError(); } }
这样定义之后,该类将不会再被外部实例化了,否则会产生编译错误。然而这样的定义带来的最直接的负面影响是该类将不能再被子类化。
五、避免创建不必要的对象
比较下面的例子,并测试他们在运行时的效率差异:
Boolean b = Boolean.valueOf("true"); Boolean b = new Boolean("true");
前者通过静态工厂方法保证了每次返回的对象都是同一个true或false,即valueOf将只会返回Boolean.TRUE或Boolean.FALSE两个静态域字段之一。而后面的Boolean构造方式,每次都会构造出一个新的Boolean实例对象。这样在多次调用后,第一种静态工厂方法将会避免大量不必要的Boolean对象被创建,从而提高了程序的运行效率,也降低了垃圾回收的负担。
继续比较下面的代码:
public class Person { private final Date birthDate; //判断该婴儿是否是在生育高峰期出生的。 public boolean isBabyBoomer { Calender c = Calendar.getInstance(TimeZone.getTimeZone("GMT")); c.set(1946,Calendar.JANUARY,1,0,0,0); Date dstart = c.getTime(); c.set(1965,Calendar.JANUARY,1,0,0,0); Date dend = c.getTime(); return birthDate.compareTo(dstart) >= 0 && birthDate.compareTo(dend) < 0; } } public class Person { private static final Date BOOM_START; private static final Date BOOM_END; static { Calender c = Calendar.getInstance(TimeZone.getTimeZone("GMT")); c.set(1946,Calendar.JANUARY,1,0,0,0); BOOM_START = c.getTime(); c.set(1965,Calendar.JANUARY,1,0,0,0); BOOM_END = c.getTime(); } public boolean isBabyBoomer() { return birthDate.compareTo(BOOM_START) >= 0 && birthDate.compareTo(BOOM_END) < 0; } }
改进后的Person类只是在初始化的时候创建Calender、TimeZone和Date实例一次,而不是在每次调用isBabyBoomer方法时都创建一次他们。如果该方法会被频繁调用,效率的提升将会极为显著。
六、消除过期的对象引用
Java中也有内存泄露:
public class Stack { private Object[] elements; private int size = 0; private static final int DEFAULT_INITIAL_CAPACITY = 16; public Stack() { elements = new Object[DEFAULT_INITIAL_CAPACITY]; } public void push(Object e) { ensureCapacity(); elements[size++] = e; } public Object pop() { if (size == 0) throw new EmptyStackException(); return elements[--size]; } private void ensureCapacity() { if (elements.length == size) elements = Arrays.copys(elements,2*size+1); } }
在以上代码中,在正常的使用中不会产生任何逻辑问题,然而随着程序运行时间不断加长,内存泄露造成的副作用将会慢慢的显现出来。当我们调用pop方法时,该方法将返回当前栈顶的elements,同时将该栈的活动区间(size)减一,然而此时被弹出的Object仍然保持至少两处引用,一个是返回的对象,另一个则是该返回对象在elements数组中原有栈顶位置的引用。这样即便外部对象在使用之后不再引用该Object,那么它仍然不会被垃圾收集器释放,久而久之导致了更多类似对象的内存泄露。修改方式如下:
public Object pop() { if (size == 0) throw new EmptyStackException(); Object result = elements[--size]; elements[size] = null; //手工将数组中的该对象置空 return result; }
推荐在以下3中情形下需要考虑资源手工处理问题:
1.类是自己管理内存,如例子中的Stack类。
2.使用对象缓存机制时,需要考虑被从缓存中换出的对象,或是长期不会被访问到的对象。
3.事件监听器和相关回调。用户经常会在需要时显示的注册,然而却经常会忘记在不用的时候注销这些回调接口实现类。
七、避免使用终结方法
在Java的实际开发中,并非所有的资源都是可以被垃圾回收器自动释放的,如FileInputStream、Graphic2D等类中使用的底层操作系统资源句柄,并不会随着对象实例被GC回收而被释放。在Java中完成这样的工作主要是依靠try-finally机制来协助完成的。然而Java中还提供了另外一种被称为finalizer的机制,使用者仅仅需要重载Object对象提供的finalize方法,这样当JVM的在进行垃圾回收时,就可以自动调用该方法。但是由于对象何时被垃圾收集的不确定性,以及finalizer给GC带来的性能上的影响,因此并不推荐使用者依靠该方法来达到关键资源释放的目的。
Java的语言规范中并没有保证该方法会被及时的执行,甚至都没有保证一定会被执行。即便开发者在code中手工调用了System.gc和System.runFinalization这两个方法,这仅仅是提高了finalizer被执行的几率而已。还有一点需要注意的是,被重载的finalize()方法中如果抛出异常,其栈帧轨迹是不会被打印出来的。在Java中被推荐的资源释放方法为,提供显式的具有良好命名的接口方法,如FileInputStream.close()和Graphic2D.dispose()等。然后使用者在finally区块中调用该方法,见如下代码:
public void test() { FileInputStream fin = null; try { fin = new FileInputStream(filename); //do something. } finally { fin.close(); } }
总之,除非是作为安全网,或者是为了终止非关键的本地资源,否则不要使用终结方法。如果使用了终结方法,即在类中覆盖了finalize方法,请记住一定要在finally子句中调用super.finalize()。
相关文章推荐
- Effective Java:创建和销毁对象
- 学习effective java-4创建和销毁对象之利用私用构造方法强化类的非实例化
- effective java :创建和销毁对象
- effective Java chapter 2创建和销毁对象
- Effective Java 进阶之路第一章(创建和销毁对象) 第一条(静态工厂方法)
- Effective Java 1:创建和销毁对象
- Effective Java笔记一 创建和销毁对象
- Effective Java - 创建和销毁对象 - 用私有构造器或者枚举类型强化 Singleton 属性
- Effective java笔记2--创建于销毁对象
- 学习effective java-1创建和销毁对象之静态工厂方法
- Effective Java——(一)创建和销毁对象
- 学习effective java-6创建和销毁对象之消除过时的对象引用
- 如何创建和销毁对象(Effective Java 第二章)
- effective java:创建和销毁对象
- 学习effective java-3创建和销毁对象之利用私有构造方法或枚举类型来强化单例的属性
- effective java之创建与销毁对象
- EffectiveJava 第1条 :创建和销毁对象--考虑用静态方法代替构造器
- EffectiveJava笔记(一) 创建和销毁对象
- Effective Java学习笔记---创建和销毁对象
- effective java 笔记之创建和销毁对象