程序员人生 网站导航

Effective Java (创建和销毁对象)

栏目:php教程时间:2017-02-07 09:05:04

学习Java的同学注意了!!! 
学习进程中遇到甚么问题或想获得学习资源的话,欢迎加入Java学习交换群,群号码:183993990  我们1起学Java!


1、斟酌用静态工厂方法代替构造器:
    
      构造器是创建1个对象实例最基本也最通用的方法,大部份开发者在使用某个class的时候,首先需要斟酌的就是如何构造和初始化1个对象示例,而构造的方式首先斟酌到的就是通过构造函数来完成,因此在看javadoc中的文档时首先关注的函数也是构造器。但是在有些时候构造器并不是我们唯1的选择,通过反射也是可以轻松到达的。我们这里主要提到的方式是通过静态类工厂的方式来创建class的实例,如:

1 public static Boolean valueOf(boolean b) { 2 return b ? Boolean.TRUE : Boolean.FALSE; 3 }

      静态工厂方法和构造器不同有以下主要优势:
      1.    成心义的名称。
      在框架设计中,针对某些工具类通常会斟酌dummy对象或空对象以辨别该对象是不是已被初始化,如我曾在我的C++基础库中实现了String类型,见以下代码:

复制代码
 1 void showExample() {  2 String strEmpty = String::empty();  3 String strEmpty2 = "";  4 String strData = String::prellocate(1024);  5 if (strEmpty.isEmpty()) {  6 //TODO: do something  7 }  8 }  9 static String String::emptyString; 10 String& String::empty() { 11 return emptyString; 12 } 13 14 bool String::isEmpty() { 15 if (this->_internal == &emptyString->_internal) 16 return true; 17 //TODO: do other justice to verify whether it is empty. 18 }
复制代码

      在上面的代码中,提供了两个静态工厂方法empty和preallocate用于分别创建1个空对象和1个带有指定分配空间的String对象。从使用方式来看,这些静态方法确切提供了成心义的名称,使用者很容易就能够判断出它们的作用和利用场景,而没必要在1组重载的构造器中去搜索每个构造函数及其参数列表,以找出合适当前场景的构造函数。从效力方面来说,由于提供了唯1的静态空对象,当判读对象实例是不是为空时(isEmpty),直接使用预制静态空对象(emptyString)的地址与当前对象进行比较,如果是同1地址,便可确认当前实例为空对象了。对preallocate函数,顾名思义,该函数预分配了指定大小的内存空间,后面在使用该String实例时,没必要担心赋值或追加的字符过量而致使频繁的realloc等操作。    
      2.    没必要在每次调用它们的时候创建1个新的对象。
      还是基于上面的代码实例,由于所有的空对象都同享同1个静态空对象,这样也节省了更多的内存开消,如果是strEmpty2方式构造出的空对象,在履行比较等操作时会带来更多的效力开消。事实上,Java在String对象的实现中,使用了常量资源池也是基于了一样的优化策略。该优势一样适用于单实例模式。  
      3.    可以返回原返回类型的任何子类型。
      在Java Collections Framework的集合接口中,提供了大量的静态方法返回集合接口类型的实现类型,如Collections.subList()、Collections.unmodifiableList()等。返回的接口是明确的,但是针对具体的实现类,函数的使用者其实不也无需知晓。这样不但极大的减少了导出类的数量,而且在今后如果发现某个子类的实现效力较低或发现更好的数据结构和算法来替换当前实现子类时,对集合接口的使用者来讲,不会带来任何的影响。本书在例子中提到EnumSet是通过静态工厂方法返回对象实例的,没有提供任何构造函数,其内部在返回实现类时做了1个优化,即如果枚举的数量小于64,该工厂方法将返回1个经过特殊优化的实现类实例(RegularEnumSet),其内部使用long(64bits在Java中) 中的不同位来表示不同的枚举值。如果枚举的数量大于64,将使用long的数组作为底层支持。但是这些内部实现类的优化对使用者来讲是透明的。 
      4.    在创建参数化类型实例的时候,它们使代码变得更加简洁。
      Map m = new HashMap();
      由于Java在构造函数的调用中没法进行类型的推演,因此也就没法通过构造器的参数类型来实例化指定类型参数的实例化对象。但是通过静态工厂方法则可以利用参数类型推演的优势,避免了类型参数在1次声明中被屡次重写所带来的烦忧,见以下代码:
      public static HashMap newInstance() {
          return new HashMap();
      }
      Map m = MyHashMap.newInstance();

    
2、遇到多个构造参数时要斟酌用构建器(Builder模式):
    
      如果1个class在构造初始化的时候存在非常多的参数,将会致使构造函数或静态工厂函数带有大量的、类型相同的函数参数,特别是当1部份参数只是可选参数的时候,class的使用者不能不为这些可选参数也传入缺省值,有的时候会发现使用者传入的缺省值多是成心义的,而并不是class内部实现所认可的缺省值,比如某个整型可选参数,通常使用者会传入0,然后class内部的实现恰恰认为0是1种重要的状态,而该状态其实不是该调用者关心的,但是该状态却间接致使其他状态的改变,因此带来了1些潜伏的状态不1致问题。与此同时,过量的函数参数也给使用者的学习和使用带来很多没必要要的麻烦,我相信任何使用者都希望看到class的接口是简单易用、函数功能清晰可见的。在Effective C++中针对接口的设计有这样的1句话:"接口要完满而最小化"。针对该类问题通常会斟酌的方法是将所有的参数归结到1个JavaBean对象中,实例化这个Bean对象,然后再将实例化的结果传给这个class的构造函数,这类方法依然没有避免缺省值的问题。该条目推荐了Builder模式来创建这个带有很多可选参数的实例对象。

复制代码
 1 class NutritionFacts {  2 private final int servingSize;  3 private final int servings;  4 private final int calories;  5 private final int fat;  6 private final int sodium;  7 private final int carbohydrate;  8 public static class Builder {  9 //对象的必选参数 10 private final int servingSize; 11 private final int servings; 12 //对象的可选参数的缺省值初始化 13 private int calories = 0; 14 private int fat = 0; 15 private int carbohydrate = 0; 16 private int sodium = 0; 17 //只用少数的必选参数作为构造器的函数参数 18 public Builder(int servingSize,int servings) { 19 this.servingSize = servingSize; 20 this.servings = servings; 21 } 22 public Builder calories(int val) { 23 calories = val; 24 return this; 25 } 26 public Builder fat(int val) { 27 fat = val; 28 return this; 29 } 30 public Builder carbohydrate(int val) { 31 carbohydrate = val; 32 return this; 33 } 34 public Builder sodium(int val) { 35 sodium = val; 36 return this; 37 } 38 public NutritionFacts build() { 39 return new NutritionFacts(this); 40 } 41 } 42 private NutritionFacts(Builder builder) { 43 servingSize = builder.servingSize; 44 servings = builder.servings; 45 calories = builder.calories; 46 fat = builder.fat; 47 sodium = builder.sodium; 48 carbohydrate = builder.carbohydrate; 49 } 50 } 51 //使用方式 52 public static void main(String[] args) { 53 NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).calories(100) 54 .sodium(35).carbohydrate(27).build(); 55 System.out.println(cocaCola); 56 }
复制代码

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

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

      对单实例模式,相信很多开发者其实不陌生,但是如何更好更安全的创建单实例对象还是需要1些斟酌和考虑的,在Java中主要的创建方式有以下3种,我们分别作出解释和适当的比较。
      1.    将构造函数私有化,直接通过静态公有的final域字段获得单实例对象:

1 public class Elvis { 2 public static final Elvis INSTANCE = new Elvis(); 3 private Elivs() { ... } 4 public void leaveTheBuilding() { ... } 5 }

      这样的方式主要优势在于简洁高效,使用者很快就可以判定当前类为单实例类,在调用时直接操作Elivs.INSTANCE便可,由于没有函数的调用,因此效力也非常高效。但是事物是具有1定的双面性的,这类设计方式在1个方向上走的过于极端了,因此他的缺点也会是非常明显的。如果今后Elvis的使用代码被迁移到多线程的利用环境下了,系统希望能够做到每一个线程使用同1个Elvis实例,不同线程之间则使用不同的对象实例。那末这类创建方式将没法实现该需求,因此需要修改接口和接口的调用者代码,这样就带来了更高的修改本钱。
      2.    通过公有域成员的方式返回单实例对象:

1 public class Elvis { 2 public static final Elvis INSTANCE = new Elvis(); 3 private Elivs() { ... } 4 public static Elvis getInstance() { return INSTANCE; } 5 public void leaveTheBuilding() { ... } 6 }

      这类方法很好的弥补了第1种方式的缺点,如果今后需要适应多线程环境的对象创建逻辑,仅需要修改Elvis的getInstance()方法内部便可,对用调用者而言则是不变的,这样便极大的缩小了影响的范围。至于效力问题,当今的JVM针对该种函数都做了很好的内联优化,因此不会产生因函数频繁调用而带来的开消。
      3.    使用枚举的方式(Java SE5):

1 public enum Elvis { 2 INSTANCE; 3 public void leaveTheBuilding() { ... } 4 }

      就目前而言,这类方法在功能上和公有域方式相近,但是他更加简洁更加清晰,扩大性更强也更加安全。
      我在设计自己的表达式解析器时,曾将所有的操作符设计为enum中不同的枚举元素,同时提供了带有参数的构造函数,传入他们的优先级、操作符名称等信息。

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

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

1 public class UtilityClass { 2 //Suppress default constructor for noninstantiability. 3 private UtilityClass() { 4 throw new AssertionError(); 5 } 6 }

      这样定义以后,该类将不会再被外部实例化了,否则会产生编译毛病。但是这样的定义带来的最直接的负面影响是该类将不能再被子类化。
    
5、避免创建没必要要的对象:

      试比较以下两行代码在被屡次反复履行时的效力差异:
      String s = new String("stringette");
      String s = "stringette";

      由于String被实现为不可变对象,JVM底层将其实现为常量池,既所有值等于"stringette" 的String对象实例同享同1对象地址,而且还可以保证,对所有在同1JVM中运行的代码,只要他们包括相同的字符串字面常量,该对象就会被重用。
    
      我们继续比较下面的例子,并测试他们在运行时的效力差异:
      Boolean b = Boolean.valueOf("true");
      Boolean b = new Boolean("true");
      前者通过静态工厂方法保证了每次返回的对象,如果他们都是true或false,那末他们将返回相同的对象。换句话说,valueOf将只会返回Boolean.TRUE或Boolean.FALSE两个静态域字段之1。而后面的Boolean构造方式,每次都会构造出1个新的Boolean实例对象。这样在屡次调用后,第1种静态工厂方法将会避免大量没必要要的Boolean对象被创建,从而提高了程序的运行效力,也下降了垃圾回收的负担。  
      继续比较下面的代码:

复制代码
 1 public class Person {  2 private final Date birthDate;  3 //判断该婴儿是不是是在生育高峰期诞生的。  4 public boolean isBabyBoomer {  5 Calender c = Calendar.getInstance(TimeZone.getTimeZone("GMT"));  6 c.set(1946,Calendar.JANUARY,1,0,0,0);  7 Date dstart = c.getTime();  8 c.set(1965,Calendar.JANUARY,1,0,0,0);  9 Date dend = c.getTime(); 10 return birthDate.compareTo(dstart) >= 0 && birthDate.compareTo(dend) < 0; 11 } 12 } 13 14 public class Person { 15 private static final Date BOOM_START; 16 private static final Date BOOM_END; 17 18 static { 19 Calender c = Calendar.getInstance(TimeZone.getTimeZone("GMT")); 20 c.set(1946,Calendar.JANUARY,1,0,0,0); 21 BOOM_START = c.getTime(); 22 c.set(1965,Calendar.JANUARY,1,0,0,0); 23 BOOM_END = c.getTime(); 24 } 25 public boolean isBabyBoomer() { 26 return birthDate.compareTo(BOOM_START) >= 0 && birthDate.compareTo(BOOM_END) < 0; 27 } 28 }
复制代码

      改进后的Person类只是在初始化的时候创建Calender、TimeZone和Date实例1次,而不是在每次调用isBabyBoomer方法时都创建1次他们。如果该方法会被频繁调用,效力的提升将会极其显著。
      集合框架中的Map接口提供keySet方法,该方法每次都将返回底层原始Map对象键数据的视图,而其实不会为该操作创建1个Set对象并填充底层Map所有键的对象拷贝。因此当屡次调用该方法并返回不同的Set对象实例时,事实上他们底层指向的将是同1段数据的援用。
      在该条目中还提到了自动装箱行动给程序运行带来的性能冲击,如果可以通过原始类型完成的操作应当尽可能避免使用装箱类型和他们之间的交互使用。见下例:

复制代码
1 public static void main(String[] args) { 2 Long sum = 0L; 3 for (long i = 0; i < Integer.MAX_VALUE; ++i) { 4 sum += i; 5 } 6 System.out.println(sum); 7 }
复制代码

      本例中由于错把long sum定义成Long sum,其效力下降了近10倍,这其中的主要缘由便是该毛病致使了2的31次方个临时Long对象被创建了。

6、消除过期的对象援用:

      虽然Java不像C/C++那样需要手工管理内存资源,而是通过更加方便、更加智能的垃圾回收机制来帮助开发者清算过期的资源。即使如此,内存泄漏问题依然会产生在你的程序中,只是和C/C++相比,Java中内存泄漏更加藏匿,更加难以发现,见以下代码:

复制代码
 1 public class Stack {  2 private Object[] elements;  3 private int size = 0;  4 private static final int DEFAULT_INITIAL_CAPACITY = 16;  5 public Stack() {  6 elements = new Object[DEFAULT_INITIAL_CAPACITY];  7 }  8 public void push(Object e) {  9 ensureCapacity(); 10 elements[size++] = e; 11 } 12 public Object pop() { 13 if (size == 0) 14 throw new EmptyStackException(); 15 return elements[--size]; 16 } 17 private void ensureCapacity() { 18 if (elements.length == size) 19 elements = Arrays.copys(elements,2*size+1); 20 } 21 }
复制代码

      以上示例代码,在正常的使用中不会产生任何逻辑问题,但是随着程序运行时间不断加长,内存泄漏酿成的副作用将会渐渐的显现出来,如磁盘页交换、OutOfMemoryError等。那末内存泄漏隐藏在程序中的甚么地方呢?当我们调用pop方法是,该方法将返回当前栈顶的elements,同时将该栈的活动区间(size)减1,但是此时被弹出的Object依然保持最少两处援用,1个是返回的对象,另外一个则是该返回对象在elements数组中原有栈顶位置的援用。这样即使外部对象在使用以后不再援用该Object,那末它依然不会被垃圾搜集器释放,长此以往致使了更多类似对象的内存泄漏。修改方式以下:

复制代码
1 public Object pop() { 2 if (size == 0) 3 throw new EmptyStackException(); 4 Object result = elements[--size]; 5 elements[size] = null; //手工将数组中的该对象置空 6 return result; 7 }
复制代码

      由于现有的Java垃圾搜集器已足够只能和强大,因此没有必要对所有不在需要的对象履行obj = null的显示置空操作,这样反而会给程序代码的浏览带来没必要要的麻烦,该条目只是推荐在以下3中情形下需要斟酌资源手工处理问题:
      1)    类是自己管理内存,如例子中的Stack类。
      2)    使用对象缓存机制时,需要斟酌被从缓存中换出的对象,或是长时间不会被访问到的对象。
      3)    事件监听器和相干回调。用户常常会在需要时显示的注册,但是却常常会忘记在不用的时候注销这些回调接口实现类。
    
7、避免使用终结方法:

      任何事情都存在其1定的双面性或多面性,对C++的开发者,内存资源是需要手工分配和释放的,而对Java和C#这类资源托管的开发语言,更多的工作可以交给虚拟机的垃圾回收器来完成,由此C++程序得到了运行效力,却失去了安全。在Java的实际开发中,并不是所有的资源都是可以被垃圾回收器自动释放的,如FileInputStream、Graphic2D等class中使用的底层操作系统资源句柄,其实不会随着对象实例被GC回收而被释放,但是这些资源对全部操作系统而言,都是非常重要的稀缺资源,更多的资源句柄泄漏将会致使全部操作系统及其运行的各种服务程序的运行效力直线降落。那末如何保证系统资源不会被泄漏了?在C++中,由于其资源完全交由开发者自行管理,因此在决定资源什么时候释放的问题上有着很优雅的支持,C++中的析构函数可以说是完成这1工作的天然候选者。任何在栈上声明的C++对象,当栈退出或当前对象离开其作用域时,该对象实例的析构函数都会被自动调用,因此当函数中有任何异常(Exception)产生时,在栈被烧毁之前,所有栈对象的析构函数均会被自动调用。但是对Java的开发者而言,从语言本身视角来看,Java本身并未提供析构函数这样的机制,固然这也是和其资源被JVM托管有1定关系的。
      在Java中完成这样的工作主要是依托try-finally机制来协助完成的。但是Java中还提供了另外1种被称为finalizer的机制,使用者仅仅需要重载Object对象提供的finalize方法,这样当JVM的在进行垃圾回收时,就能够自动调用该方法。但是由于对象什么时候被垃圾搜集的不肯定性,和finalizer给GC带来的性能上的影响,因此其实不推荐使用者依托该方法来到达关键资源释放的目的。比如,有数千个图形句柄都在等待被终结和回收,惋惜的是履行终结方法的线程优先级要低于普通的工作者线程,这样就会有大量的图形句柄资源停留在finalizer的队列中而不能被及时的释放,终究致使了系统运行效力的降落,乃至还会引发JVM报出OutOfMemoryError的毛病。
      Java的语言规范中并没有保证该方法会被及时的履行,乃至都没有保证1定会被履行。即使开发者在code中手工调用了System.gc和System.runFinalization这两个方法,这仅仅是提高了finalizer被履行的概率而已。还有1点需要注意的是,被重载的finalize()方法中如果抛出异常,其栈帧轨迹是不会被打印出来的。在Java中被推荐的资源释放方法为,提供显式的具有良好命名的接口方法,如FileInputStream.close()和Graphic2D.dispose()等。然后使用者在finally区块中调用该方法,见以下代码:

复制代码
1 public void test() { 2 FileInputStream fin = null; 3 try { 4 fin = new FileInputStream(filename); 5 //do something. 6 } finally { 7 fin.close(); 8 } 9 }
复制代码

      那末在实际的开发中,利用finalizer又能给我们带来甚么样的帮助呢?见下例:

复制代码
 1 public class FinalizeTest {  2 //@Override  3 protected void finalize() throws Throwable {  4 try {  5 //在调试进程中通过该方法,打印对象在被搜集前的各种状态,  6 //如判断是不是仍有资源未被释放,或是不是有状态不1致的现象存在。  7 //推荐将该finalize方法设计成仅在debug状态下可用,而在release  8 //下该方法其实不存在,以免其对运行时效力的影响。  9 System.out.println("The current status: " + _myStatus); 10 } finally { 11 //在finally中对超类finalize方法的调用是必须的,这样可以保证全部class继承 12 //体系中的finalize链都被履行。 13 super.finalize(); 14 } 15 } 16 }
复制代码





------分隔线----------------------------
------分隔线----------------------------

最新技术推荐