首页 > 学院 > 开发设计 > 正文

Effective Java读书笔记三:创建和销毁对象(1-7)

2019-11-14 10:16:25
字体:
来源:转载
供稿:网友

第1条:考虑用静态工厂方法代替构造器

对于类而言,为了让客服端获得它的一个实例最常用的的一个方法就是提供一个公有的构造器。还有一种方法,类可以提供一个公有的静态工厂方法(static factory method),它只是一个返回类实例的静态方法。

通过静态工厂方法构造对象的优势:

静态工厂方法与构造器不同的第一大优势在于,它们有名称,使客服端代码更加容易被阅读。 不必在每次调用的它们的时候都创建一个新的对象(这个完全取决于具体的实现)。 它们可以返回原返回类型的任何子类型的对象。 这种灵活性的一种应用:API可以返回对象,同时又不会使对象的类变成公有的。公有的静态方法所返回的对象的类不仅可以是非公有的,而且该类还可以随着每次调用而发生变化着取决于静态工厂方法的参数值,只要是已声明返回类型的子类型,都是允许的。在创建参数化类型(也就是泛型,jdk1.5新特性)实例的时候,它们是的代码变得更加简洁。/**普通创建****/ Map<String,List<String>> m=new HashMap<String,List<String>>; /**有了静态方法过后***/ Map<String,List<String>> m=HashMap.newInstance(); //前提HashMap提供了这个静态工厂方法 public static <k,v> HashMap<k,v> newInstance(){ return new HashMap<K,V>(); }

静态工厂方法的主要缺点在于:

类如果不含有他的公有或者受保护的构造器,就不能被子类化(即被继承)。它们与其他静态方法实际上没有任何区别。

常用的静态工厂名称:valueOf,of,getInstance,newInstance,getType,newType.

第2条:遇到多个构造参数时要考虑用构建器(Builder模式)

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); }

对于Builder方式,可选参数的缺省值问题也将不再困扰着所有的使用者。这种方式还带来了一个间接的好处是,不可变对象的初始化以及参数合法性的验证等工作在构造函数中原子性的完成了。

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

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()方法内部即可,对用调用者而言则是不变的,这样便极大的缩小了影响的范围。至于效率问题,现今的JVM针对该种函数都做了很好的内联优化,因此不会产生因函数频繁调用而带来的开销。

3、使用枚举的方式(java SE5):

public enum Elvis { INSTANCE; public void leaveTheBuilding() { ... } }

就目前而言,这种方法在功能上和公有域方式相近,但是他更加简洁更加清晰,扩展性更强也更加安全。

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

对于有些工具类如java.lang.Math、java.util.Arrays等,其中只是包含了静态方法和静态域字段,因此对这样的class实例化就显得没有任何意义了。然而在实际的使用中,如果不加任何特殊的处理,这样的classes是可以像其他classes一样被实例化的。这里介绍了一种方式,既将缺省构造函数设置为private,这样类的外部将无法实例化该类,与此同时,在这个私有的构造函数的实现中直接抛出异常,从而也避免了类的内部方法调用该构造函数。

public class UtilityClass { //Suppress default constructor for noninstantiability. private UtilityClass() { throw new AssertionError(); } }

这样定义之后,该类将不会再被外部实例化了,否则会产生编译错误。然而这样的定义带来的最直接的负面影响是该类将不能再被子类化。

第5条:避免创建不必要的对象

一般来说,最好能重用对象而不是在每次需要的时候就创建一个相同功能的新对象。如果对象是不可变的,它就始终可以被重用。 反例:

String s = new String(“stringette”);

该语句每次被执行的时候都创建一个新的String实例,但是这些创建对象的动作全都是不必要的,传递给String构造器的参数(“stringette”)本身就是一个String实例,功能方面等同于构造器创建的对象。如果这种用法是在一个循环中,或者是在一个被频繁调用的方法中,就会创建出很多不必要的String实例。 改进:

String s = “stringette”;

改进后,只用一个String实例,而不是每次执行的时候都创建一个新的实例,而且,它可以保证,对于所有在同一虚拟机中运行的代码,只要它们包含相同的字符串字面常量,该对象就会被重用。

对于同时提供了静态工厂方法和构造器的不可变类,通常可以使用静态工厂方法而不是构造器,以避免创建不必要的对象。 例如,静态工厂方法Boolean.valueOf(String)几乎总是比构造器Boolean(String)好,构造器在每次被调用的时候都会创建一个新的对象,而静态工厂方法则从来不要求这样做,实际上也不会这样做。

要优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱

public static void main(String[] args) { Long sum = 0L; for (long i = 0; i < Integer.MAX_VALUE; i++) { sum += i; } System.out.println(sum);}

这段程序算出的答案是正确的,但是比实际情况要更慢一些,只因为打错了一个字符。变量sum被声明成Long而不是long,这就意味着程序构造了大约2^31个多的Long实例。

Java共有9中基本类型,同别的语言有重要区别的是这9中类型所占存储空间大小与机器硬件架构无关,这使得Java程序有很强的可移植性,如下图:

这里写图片描述

不要错误地认为“创建对象的代价非常昂贵,我们应该要尽可能地避免创建对象,而不是不创建对象”,相反,由于小对象的构造器只做很少量的工作,所以,小对象的创建和回收动作是非常廉价的,特别是在现代的JVM实现上更是如此。通过创建附加的对象,提升程序的清晰性、简洁性和功能性,这通常也是件好事。

反之,通过维护自己的对象池来创建对象并不是一种好的做法,除非池中的对象是非常重量级的。真正正确使用对象池的典型对象示例就是数据库连接池。建立数据库连接的代价是非常昂贵的,因此重用这些对象非常有意义。

另外,在1.5版本里,对基本类型的整形包装类型使用时,要使用形如 Byte.valueOf来创建包装类型,因为-128~127的数会缓存起来,所以我们要从缓冲池中取,Short、Integer、Long也是这样。

第6条:消除过期的对象引用

尽管Java不像C/C++那样需要手工管理内存资源,而是通过更为方便、更为智能的垃圾回收机制来帮助开发者清理过期的资源。即便如此,内存泄露问题仍然会发生在你的程序中,只是和C/C++相比,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); } }

这段程序有一个“内存泄漏”问题,如果一个栈先是增长,然后再收缩,那么,从栈中弹出来的对象不会被当做垃圾回收,即使使用栈的程序不再引用这些对象,它们也不会被回收。这是因为,栈内部维护这对这些对象的过期使用(obsolete reference),过期引用指永远也不会被解除的引用。 修复的方法很简单:一旦对象引用已经过期,只需要清空这些引用即可。对于上述例子中的Stack类而言,只要一个单元弹出栈,指向它的引用就过期了,就可以将它清空。

修改方式如下:

public Object pop() { if (size == 0) throw new EmptyStackException(); Object result = elements[--size]; elements[size] = null; //手工将数组中的该对象置空 return result; }

Stock为什么会有内存泄漏问题呢? 问题在于,Stock类自己管理内存。存储池中包含了elements数组(对象引用单元,而不是对象本身)的元素。数组活动区域的元素是已分配的,而数组其余部分的元素是自由的。但是垃圾回收器并不知道这一点,就需要手动清空这些数组元素。 一般而言,只要类是自己管理内存,就应该警惕内存泄漏问题。一旦元素被释放掉,则该元素中包含的任何对象引用都应该被清空。

由于现有的Java垃圾收集器已经足够只能和强大,因此没有必要对所有不在需要的对象执行obj = null的显示置空操作,这样反而会给程序代码的阅读带来不必要的麻烦,该条目只是推荐在以下3中情形下需要考虑资源手工处理问题:

类是自己管理内存,如例子中的Stack类。使用对象缓存机制时,需要考虑被从缓存中换出的对象,或是长期不会被访问到的对象。事件监听器和相关回调。用户经常会在需要时显示的注册,然而却经常会忘记在不用的时候注销这些回调接口实现类。

第7条:避免使用终结方法

Java的语言规范中并没有保证终结方法会被及时的执行,甚至都没有保证一定会被执行。即便开发者在code中手工调用了System.gc和System.runFinalization这两个方法,这仅仅是提高了finalizer被执行的几率而已。还有一点需要注意的是,被重载的finalize()方法中如果抛出异常,其栈帧轨迹是不会被打印出来的。

《Effective Java中文版 第2版》PDF版下载: http://download.csdn.net/detail/xunzaosiyecao/9745699

作者:jiankunking 出处:http://blog.csdn.net/jiankunking


发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表