java经典问题总结

一、考虑用静态工厂方法代替构造器:

    

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

public static Boolean valueOf(boolean b) {

         return b ? Boolean.TRUE : Boolean.FALSE;

     }

 静态工厂方法和构造器不同有以下主要优势:

      1.    有意义的名称。

      在框架设计中,针对某些工具类通常会考虑dummy对象或者空对象以辨别该对象是否已经被初始化,如我曾在我的C++基础库中实现了String类型,见如下代码:

void showExample() {

         String strEmpty = String::empty();

         String strEmpty2 = "";

         String strData = String::prellocate(1024);

         if (strEmpty.isEmpty()) {

             //TODO: do something

         }

     }

     static String String::emptyString;

     String& String::empty() {

         return emptyString;

     }

      

     bool String::isEmpty() {

         if (this->_internal == &emptyString->_internal)

             return true;

         //TODO: do other justice to verify whether it is empty.

     }

在上面的代码中,提供了两个静态工厂方法empty和preallocate用于分别创建一个空对象和一个带有指定分配空间的String对象。从使用方式来看,这些静态方法确实提供了有意义的名称,使用者很容易就可以判断出它们的作用和应用场景,而不必在一组重载的构造器中去搜寻每一个构造函数及其参数列表,以找出适合当前场景的构造函数。从效率方面来讲,由于提供了唯一的静态空对象,当判读对象实例是否为空时(isEmpty),直接使用预制静态空对象(emptyString)的地址与当前对象进行比较,如果是同一地址,即可确认当前实例为空对象了。对于preallocate函数,顾名思义,该函数预分配了指定大小的内存空间,后面在使用该String实例时,不必担心赋值或追加的字符过多而导致频繁的realloc等操作。    

      2.    不必在每次调用它们的时候创建一个新的对象。

      还是基于上面的代码实例,由于所有的空对象都共享同一个静态空对象,这样也节省了更多的内存开销,如果是strEmpty2方式构造出的空对象,在执行比较等操作时会带来更多的效率开销。事实上,Java在String对象的实现中,使用了常量资源池也是基于了同样的优化策略。该优势同样适用于单实例模式。  

      3.    可以返回原返回类型的任何子类型。

      在Java Collections Framework的集合接口中,提供了大量的静态方法返回集合接口类型的实现类型,如Collections.subList()、Collections.unmodifiableList()等。返回的接口是明确的,然而针对具体的实现类,函数的使用者并不也无需知晓。这样不仅极大的减少了导出类的数量,而且在今后如果发现某个子类的实现效率较低或者发现更好的数据结构和算法来替换当前实现子类时,对于集合接口的使用者来说,不会带来任何的影响。本书在例子中提到EnumSet是通过静态工厂方法返回对象实例的,没有提供任何构造函数,其内部在返回实现类时做了一个优化,即如果枚举的数量小于64,该工厂方法将返回一个经过特殊优化的实现类实例(RegularEnumSet),其内部使用long(64bits在Java中) 中的不同位来表示不同的枚举值。如果枚举的数量大于64,将使用long的数组作为底层支撑。然而这些内部实现类的优化对于使用者来说是透明的。

      4.    在创建参数化类型实例的时候,它们使代码变得更加简洁。

      Map<String,String> m = new HashMap<String,String>();

      由于Java在构造函数的调用中无法进行类型的推演,因此也就无法通过构造器的参数类型来实例化指定类型参数的实例化对象。然而通过静态工厂方法则可以利用参数类型推演的优势,避免了类型参数在一次声明中被多次重写所带来的烦忧,见如下代码:

      public static <K,V> HashMap<K,V> newInstance() {

          return new HashMap<K,V>();

      }
二、遇到多个构造参数时要考虑用构建器(Builder模式):

    

      如果一个class在构造初始化的时候存在非常多的参数,将会导致构造函数或者静态工厂函数带有大量的、类型相同的函数参数,特别是当一部分参数只是可选参数的时候,class的使用者不得不为这些可选参数也传入缺省值,有的时候会发现使用者传入的缺省值可能是有意义的,而并非class内部实现所认可的缺省值,比如某个整型可选参数,通常使用者会传入0,然后class内部的实现恰恰认为0是一种重要的状态,而该状态并不是该调用者关心的,但是该状态却间接导致其他状态的改变,因而带来了一些潜在的状态不一致问题。与此同时,过多的函数参数也给使用者的学习和使用带来很多不必要的麻烦,我相信任何使用者都希望看到class的接口是简单易用、函数功能清晰可见的。在Effective C++中针对接口的设计有这样的一句话:"接口要完满而最小化"。针对该类问题通常会考虑的方法是将所有的参数归结到一个JavaBean对象中,实例化这个Bean对象,然后再将实例化的结果传给这个class的构造函数,这种方法仍然没有避免缺省值的问题。该条目推荐了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方式,可选参数的缺省值问题也将不再困扰着所有的使用者。这种方式还带来了一个间接的好处是,不可变对象的初始化以及参数合法性的验证等工作在构造函数中原子性的完成了。

 

      Map<String,String> m = MyHashMap.newInstance();
三、用私有构造器或者枚举类型强化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()方法内部即可,对用调用者而言则是不变的,这样便极大的缩小了影响的范围。至于效率问题,现今的JVM针对该种函数都做了很好的内联优化,因此不会产生因函数频繁调用而带来的开销。

      3.    使用枚举的方式(Java SE5):

public enum Elvis {

         INSTANCE;

         public void leaveTheBuilding() { ... }

     }

 就目前而言,这种方法在功能上和公有域方式相近,但是他更加简洁更加清晰,扩展性更强也更加安全。
四、通过私有构造器强化不可实例化的能力:

      我在设计自己的表达式解析器时,曾将所有的操作符设计为enum中不同的枚举元素,同时提供了带有参数的构造函数,传入他们的优先级、操作符名称等信息。

 

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

public class UtilityClass {

         //Suppress default constructor for noninstantiability.

         private UtilityClass() {

             throw new AssertionError();

         }

     }

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

    
五、避免创建不必要的对象:

 

      试比较以下两行代码在被多次反复执行时的效率差异:

      String s = new String("stringette");

      String s = "stringette";

      由于String被实现为不可变对象,JVM底层将其实现为常量池,既所有值等于"stringette" 的String对象实例共享同一对象地址,而且还可以保证,对于所有在同一JVM中运行的代码,只要他们包含相同的字符串字面常量,该对象就会被重用。

    

      我们继续比较下面的例子,并测试他们在运行时的效率差异:

      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方法时都创建一次他们。如果该方法会被频繁调用,效率的提升将会极为显著。

      集合框架中的Map接口提供keySet方法,该方法每次都将返回底层原始Map对象键数据的视图,而并不会为该操作创建一个Set对象并填充底层Map所有键的对象拷贝。因此当多次调用该方法并返回不同的Set对象实例时,事实上他们底层指向的将是同一段数据的引用。

      在该条目中还提到了自动装箱行为给程序运行带来的性能冲击,如果可以通过原始类型完成的操作应该尽量避免使用装箱类型以及他们之间的交互使用。见下例:

public static void main(String[] args) {

         Long sum = 0L;

         for (long i = 0; i < Integer.MAX_VALUE; ++i) {

             sum += i;

         }

         System.out.println(sum);

     }

  本例中由于错把long sum定义成Long sum,其效率降低了近10倍,这其中的主要原因便是该错误导致了2的31次方个临时Long对象被创建了。
六、消除过期的对象引用:

      尽管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);

         }

     }

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

public Object pop() {

         if (size == 0)

             throw new EmptyStackException();

         Object result = elements[--size];

         elements[size] = null; //手工将数组中的该对象置空

         return result;

     }

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

      1)    类是自己管理内存,如例子中的Stack类。

      2)    使用对象缓存机制时,需要考虑被从缓存中换出的对象,或是长期不会被访问到的对象。

      3)    事件监听器和相关回调。用户经常会在需要时显示的注册,然而却经常会忘记在不用的时候注销这些回调接口实现类。

    
七、避免使用终结方法:

 

      任何事情都存在其一定的双面性或者多面性,对于C++的开发者,内存资源是需要手工分配和释放的,而对于Java和C#这种资源托管的开发语言,更多的工作可以交给虚拟机的垃圾回收器来完成,由此C++程序得到了运行效率,却失去了安全。在Java的实际开发中,并非所有的资源都是可以被垃圾回收器自动释放的,如FileInputStream、Graphic2D等class中使用的底层操作系统资源句柄,并不会随着对象实例被GC回收而被释放,然而这些资源对于整个操作系统而言,都是非常重要的稀缺资源,更多的资源句柄泄露将会导致整个操作系统及其运行的各种服务程序的运行效率直线下降。那么如何保证系统资源不会被泄露了?在C++中,由于其资源完全交由开发者自行管理,因此在决定资源何时释放的问题上有着很优雅的支持,C++中的析构函数可以说是完成这一工作的天然候选者。任何在栈上声明的C++对象,当栈退出或者当前对象离开其作用域时,该对象实例的析构函数都会被自动调用,因此当函数中有任何异常(Exception)发生时,在栈被销毁之前,所有栈对象的析构函数均会被自动调用。然而对于Java的开发者而言,从语言自身视角来看,Java本身并未提供析构函数这样的机制,当然这也是和其资源被JVM托管有一定关系的。

      在Java中完成这样的工作主要是依靠try-finally机制来协助完成的。然而Java中还提供了另外一种被称为finalizer的机制,使用者仅仅需要重载Object对象提供的finalize方法,这样当JVM的在进行垃圾回收时,就可以自动调用该方法。但是由于对象何时被垃圾收集的不确定性,以及finalizer给GC带来的性能上的影响,因此并不推荐使用者依靠该方法来达到关键资源释放的目的。比如,有数千个图形句柄都在等待被终结和回收,可惜的是执行终结方法的线程优先级要低于普通的工作者线程,这样就会有大量的图形句柄资源停留在finalizer的队列中而不能被及时的释放,最终导致了系统运行效率的下降,甚至还会引发JVM报出OutOfMemoryError的错误。

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

         }

     }

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

public class FinalizeTest {

         //@Override

         protected void finalize() throws Throwable {

             try {

                 //在调试过程中通过该方法,打印对象在被收集前的各种状态,

 //如判断是否仍有资源未被释放,或者是否有状态不一致的现象存在。

 //推荐将该finalize方法设计成仅在debug状态下可用,而在release

 //下该方法并不存在,以避免其对运行时效率的影响。

                 System.out.println("The current status: " + _myStatus);

             } finally {

                 //在finally中对超类finalize方法的调用是必须的,这样可以保证整个class继承

 //体系中的finalize链都被执行。

                 super.finalize();

             }

         }

     }
八、覆盖equals时请遵守通用约定:

  对于Object类中提供的equals方法在必要的时候是必要重载的,然而如果违背了一些通用的重载准则,将会给程序带来一些潜在的运行时错误。如果自定义的class没有重载该方法,那么该类实例之间的相等性的比较将是基于两个对象是否指向同一地址来判定的。因此对于以下几种情况可以考虑不重载该方法:

      1.    类的每一个实例本质上都是唯一的。

      不同于值对象,需要根据其内容作出一定的判定,然而该类型的类,其实例的自身便具备了一定的唯一性,如Thread、Timer等,他本身并不具备更多逻辑比较的必要性。

      2.    不关心类是否提供了“逻辑相等”的测试功能。

      如Random类,开发者在使用过程中并不关心两个Random对象是否可以生成同样随机数的值,对于一些工具类亦是如此,如NumberFormat和DateFormat等。

      3.    超类已经覆盖了equals,从超类继承过来的行为对于子类也是合适的。

      如Set实现都从AbstractSet中继承了equals实现,因此其子类将不在需要重新定义该方法,当然这也是充分利用了继承的一个优势。

      4.    类是私有的或是包级别私有的,可以确定它的equals方法永远不会被调用。

    

      那么什么时候应该覆盖Object.equals呢?如果类具有自己特有的“逻辑相等”概念,而且超类中没有覆盖equals以实现期望的行为,这是我们就需要覆盖equals方法,如各种值对象,或者像Integer和Date这种表示某个值的对象。在重载之后,当对象插入Map和Set等容器中时,可以得到预期的行为。枚举也可以被视为值对象,然而却是这种情形的一个例外,对于枚举是没有必要重载equals方法,直接比较对象地址即可,而且效率也更高。

      在覆盖equals是,该条目给出了通用的重载原则:

      1.    自反性:对于非null的引用值x,x.equals(x)返回true。

      如果违反了该原则,当x对象实例被存入集合之后,下次希望从该集合中取出该对象时,集合的contains方法将直接无法找到之前存入的对象实例。

      2.    对称性:对于任何非null的引用值x和y,如果y.equals(x)为true,那么x.equals(y)也为true。

public final class CaseInsensitiveString {

         private final String s;

         public CaseInsensitiveString(String s) {

             this.s = s;

         }

         @Override public boolean equals(Object o) {

             if (o instanceof CaseInsensitiveString)

                 return s.equalsIgnoreCase((CaseInsensitiveString)o).s);

             if (o instanceof String) //One-way interoperability

                 return s.equalsIgnoreCase((String)o);

             return false;

         }

     }

     public static void main(String[] args) {

         CaseInsensitiveString cis = new CaseInsensitiveString("Polish");

         String s = "polish";

         List<CaseInsensitiveString> l = new ArrayList<CaseInsensitiveString>();

         l.add(cis);

         if (l.contains(s))

             System.out.println("s can be found in the List");

     }

 对于上例,如果执行cis.equals(s)将会返回true,因为在该class的equals方法中对参数o的类型针对String作了特殊的判断和特殊的处理,因此如果equals中传入的参数类型为String时,可以进一步完成大小写不敏感的比较。然而在String的equals中,并没有针对CaseInsensitiveString类型做任何处理,因此s.equals(cis)将一定返回false。针对该示例代码,由于无法确定List.contains的实现是基于cis.equals(s)还是基于s.equals(cis),对于实现逻辑两者都是可以接受的,既然如此,外部的使用者在调用该方法时也应该同样保证并不依赖于底层的具体实现逻辑。由此可见,equals方法的对称性是非常必要的。以上的equals实现可以做如下修改:

@Override public boolean equals(Object o) {

         if (o instanceof CaseInsensitiveString)

             return s.equalsIgnoreCase((CaseInsensitiveString)o).s);

         return false;

     }

 这样修改之后,cis.equals(s)和s.equals(cis)都将返回false。    

      3.    传递性:对于任何非null的引用值x、y和z,如果x.equals(y)返回true,同时y.equals(z)也返回true,那么x.equals(z)也必须返回true。

public class Point {

         private final int x;

         private final int y;

         public Point(int x,int y) {

             this.x = x;

             this.y = y;

         }

         @Override public boolean equals(Object o) {

             if (!(o instanceof Point))

                 return false;

             Point p = (Point)o;

             return p.x == x && p.y == y;

         }

     }

   对于该类的equals重载是没有任何问题了,该逻辑可以保证传递性,然而在我们试图给Point类添加新的子类时,会是什么样呢?

public class ColorPoint extends Point {

         private final Color c;

         public ColorPoint(int x,int y,Color c) {

             super(x,y);

             this.c = c;

         }

         @Override public boolean equals(Object o) {

             if (!(o instanceof ColorPoint))

                 return false;

             return super.equals(o) && ((ColorPoint)o).c == c;

         }

     }

  如果在ColorPoint中没有重载自己的equals方法而是直接继承自超类,这样的相等性比较逻辑将会给使用者带来极大的迷惑,毕竟Color域字段对于ColorPoint而言确实是非常有意义的比较性字段,因此该类重载了自己的equals方法。然而这样的重载方式确实带来了一些潜在的问题,见如下代码:

public void test() {

         Point p = new Point(1,2);

         ColorPoint cp = new ColorPoint(1,2,Color.RED);

         if (p.equals(cp))

             System.out.println("p.equals(cp) is true");

         if (!cp.equals(p))

             System.out.println("cp.equals(p) is false");

     }

   从输出结果来看,ColorPoint.equals方法破坏了相等性规则中的对称性,因此需要做如下修改:@Override public boolean equals(Object o) {

         if (!(o instanceof Point))

             return false;

         if (!(o instanceof ColorPoint))

             return o.equals(this);

         return super.equals(o) && ((ColorPoint)o).c == c;

     }

   经过这样的修改,对称性确实得到了保证,但是却牺牲了传递性,见如下代码:

public void test() {

         ColorPoint p1 = new ColorPoint(1,2,Color.RED);

         Point p2 = new Point(1,2);

         ColorPoint p1 = new ColorPoint(1,2,Color.BLUE);

         if (p1.equals(p2) && p2.equals(p3))

             System.out.println("p1.equals(p2) && p2.equals(p3) is true");

         if (!(p1.equals(p3))

             System.out.println("p1.equals(p3) is false");

     }

   再次看输出结果,传递性确实被打破了。如果我们在Point.equals中不使用instanceof而是直接使用getClass呢?

@Override public boolean equals(Object o) {

         if (o == null || o.getClass() == getClass())

             return false;

         Point p = (Point)o;

         return p.x == x && p.y == y;

     }

  这样的Point.equals确实保证了对象相等性的这几条规则,然而在实际应用中又是什么样子呢?

class MyTest {

         private static final Set<Point> unitCircle;

         static {

             unitCircle = new HashSet<Point>();

             unitCircle.add(new Point(1,0));

             unitCircle.add(new Point(0,1));

             unitCircle.add(new Point(-1,0));

             unitCircle.add(new Point(0,-1));

         }

         public static boolean onUnitCircle(Point p) {

             return unitCircle.contains(p);

         }

     }

  如果此时我们测试的不是Point类本身,而是ColorPoint,那么按照目前Point.equals(getClass方式)的实现逻辑,ColorPoint对象在被传入onUnitCircle方法后,将永远不会返回true,这样的行为违反了"里氏替换原则"(敏捷软件开发一书中给出了很多的解释),既一个类型的任何重要属性也将适用于它的子类型。因此该类型编写的任何方法,在它的子类型上也应该同样运行的很好。

      如何解决这个问题,该条目给出了一个折中的方案,既复合优先于继承,见如下代码:

public class ColorPoint {

         //包含了Point的代理类

         private final Point p;

         private final Color c;

         public ColorPoint(int x,int y,Color c) {

             if (c == null)

                 throw new NullPointerException();

             p = new Point(x,y);

             this.c = c;

         }

         //提供一个视图方法返回内部的Point对象实例。这里Point实例为final对象非常重要,

 //可以避免使用者的误改动。视图方法在Java的集合框架中有着大量的应用。

         public Point asPoint() {

             return p;

         }

         @Override public boolean equals(Object o) {

             if (!(o instanceof ColorPoint))

                 return false;

             ColorPoint cp = (ColorPoint)o;

             return cp.p.equals(p) && cp.c.equals(c);

         }

     }

  4.    一致性:对于任何非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有被改变,多次调用x.equals(y)就会一致的返回true,或者一致返回false。

      在实际的编码中,尽量不要让类的equals方法依赖一些不确定性较强的域字段,如path。由于path有多种表示方式可以指向相同的目录,特别是当path中包含主机名称或ip地址等信息时,更增加了它的不确定性。再有就是path还存在一定的平台依赖性。

      5.    非空性:很难想象会存在o.equals(null)返回true的正常逻辑。作为JDK框架中极为重要的方法之一,equals方法被JDK中的基础类广泛的使用,因此作为一种通用的约定,像equals、toString、hashCode和compareTo等重要的通用方法,开发者在重载时不应该让自己的实现抛出异常,否则会引起很多潜在的Bug。如在Map集合中查找指定的键,由于查找过程中的键相等性的比较就是利用键对象的equals方法,如果此时重载后的equals方法抛出NullPointerException异常,而Map的get方法并未捕获该异常,从而导致系统的运行时崩溃错误,然而事实上,这样的问题是完全可以通过正常的校验手段来避免的。综上所述,很多对象在重载equals方法时都会首先对输入的参数进行是否为null的判断,见如下代码:

@Override public boolean equals(Object o) {

         if (o == null)

             return false;

         if (!(o instanceof MyType))

             return false;

         ...

     }

 注意以上代码中的instanceof判断,由于在后面的实现中需要将参数o进行类型强转,如果类型不匹配则会抛出ClassCastException,导致equals方法提前退出。在此需要指出的是instanceof还有一个潜在的规则,如果其左值为null,instanceof操作符将始终返回false,因此上面的代码可以优化为:

@Override public boolean equals(Object o) {

         if (!(o instanceof MyType))

             return false;

         ...

     }

  鉴于之上所述,该条目中给出了重载equals方法的最佳逻辑:

      1.    使用==操作符检查"参数是否为这个对象的引用",如果是则返回true。由于==操作符是基于对象地址的比较,因此特别针对拥有复杂比较逻辑的对象而言,这是一种性能优化的方式。

      2.    使用instanceof操作符检查"参数是否为正确的类型",如果不是则返回false。

      3.    把参数转换成为正确的类型。由于已经通过instanceof的测试,因此不会抛出ClassCastException异常。

      4.    对于该类中的每个"关键"域字段,检查参数中的域是否与该对象中对应的域相匹配。

      如果以上测试均全部成功返回true,否则false。见如下示例代码:

@Override public boolean equals(Object o) {

         if (o == this)

             return true;

         

         if (!(o instanceof MyType))

             return false;

             

         MyType myType = (MyType)o;

         return objField.equals(o.objField) && intField == o.intField

             && Double.compare(doubleField,o.doubleField) == 0

             && Arrays.equals(arrayField,o.arrayField);

     }

从上面的示例中可以看出,如果域字段为Object对象,则使用equals方法进行两者之间的相等性比较,如果为int等整型基本类型,可以直接比较,如果为浮点型基本类型,考虑到精度和Double.NaN和Float.NaN等问题,推荐使用其对应包装类的compare方法,如果是数组,可以使用JDK 1.5中新增的Arrays.equals方法。众所周知,&&操作符是有短路原则的,因此应该将最有可能不相同和比较开销更低的域比较放在最前面。

      最后需要提起注意的是Object.equals的参数类型为Object,如果要重载该方法,必须保持参数列表的一致性,如果我们将子类的equals方法写成:public boolean equals(MyType o);Java的编译器将会视其为Object.equals的过载(Overload)方法,因此推荐在声明该重载方法时,在方法名的前面加上@Override注释标签,一旦当前声明的方法因为各种原因并没有重载超类中的方法,该标签的存在将会导致编译错误,从而提醒开发者此方法的声明存在语法问题。

    
九、覆盖equals时总要覆盖hashCode:

 

      一个通用的约定,如果类覆盖了equals方法,那么hashCode方法也需要被覆盖。如果将会导致该类无法和基于散列的集合一起正常的工作,如HashMap、HashSet。来自JavaSE6的约定如下:

      1.    在应用程序执行期间,只要对象的equals方法的比较操作所用到的信息没有被修改,那么对这同一个对象多次调用,hashCode方法都必须始终如一地返回同一个整数。在同一个应用程序的多次执行过程中,每次执行所返回的整数可以不一致。

      2.    如果两个对象根据equals(Object)方法比较是相等的,那么调用这两个对象中任意一个对象的hashCode方法都必须产生同样的整数结果。

      3.    如果两个对象根据equals(Object)方法比较是不相等的,那么调用这两个对象中任意一个对象的hashCode方法,则不一定要产生不同的整数结果。但是程序员应该知道,给不相等的对象产生截然不同的整数结果,有可能提高散列表的性能。

      如果类没有覆盖hashCode方法,那么Object中缺省的hashCode实现是基于对象地址的,就像equals在Object中的缺省实现一样。如果我们覆盖了equals方法,那么对象之间的相等性比较将会产生新的逻辑,而此逻辑也应该同样适用于hashCode中散列码的计算,既参与equals比较的域字段也同样要参与hashCode散列码的计算。见下面的示例代码:

public final class PhoneNumber {

         private final short areaCode;

         private final short prefix;

         private final short lineNumber;

         public PhoneNumber(int areaCode,int prefix,int lineNumber) {

             //做一些基于参数范围的检验。

             this.areaCode = areaCode;

             this.prefix = prefix;

             this.lineNumber = lineNumber;

         }

         @Override public boolean equals(Object o) {

             if (o == this)

                 return true;

             if (!(o instanceof PhoneNumber))

                 return false;

             PhoneNumber pn = (PhoneNumber)o;

             return pn.lineNumber = lineNumber && pn.prefix == prefix && pn.areaCode = areaCode;

         }

     }

     public static void main(String[] args) {

         Map<PhoneNumber,String> m = new HashMap<PhoneNumber,String>();

         PhoneNumber pn1 = new PhoneNumber(707,867,5309);

         m.put(pn1,"Jenny");

         PhoneNumber pn2 = new PhoneNumber(707,867,5309);

         if (m.get(pn) == null)

             System.out.println("Object can't be found in the Map");

     }

 从以上示例的输出结果可以看出,新new出来的pn2对象并没有在Map中找到,尽管pn2和pn1的相等性比较将返回true。这样的结果很显然是有悖我们的初衷的。如果想从Map中基于pn2找到pn1,那么我们就需要在PhoneNumber类中覆盖缺省的hashCode方法,见如下代码:

@Override public int hashCode() {

         int result = 17;

         result = 31 * result + areaCode;

         result = 31 * result + prefix;

         result = 31 * result + lineNumber;

         return result;

     }

 在上面的代码中,可以看到参与hashCode计算的域字段也同样参与了PhoneNumber的相等性(equals)比较。对于生成的散列码,推荐不同的对象能够尽可能生成不同的散列,这样可以保证在存入HashMap或HashSet中时,这些对象被分散到不同的散列桶中,从而提高容器的存取效率。对于有些不可变对象,如果需要被频繁的存取于哈希集合,为了提高效率,可以在对象构造的时候就已经计算出其hashCode值,hashCode()方法直接返回该值即可,如:

public final class PhoneNumber {

         private final short areaCode;

         private final short prefix;

         private final short lineNumber;

         private final int myHashCode;

         public PhoneNumber(int areaCode,int prefix,int lineNumber) {

             //做一些基于参数范围的检验。

             this.areaCode = areaCode;

             this.prefix = prefix;

             this.lineNumber = lineNumber;

             myHashCode = 17;

             myHashCode = 31 * myHashCode + areaCode;

             myHashCode = 31 * myHashCode + prefix;

             myHashCode = 31 * myHashCode + lineNumber;

         }

         @Override public boolean equals(Object o) {

             if (o == this)

                 return true;

             if (!(o instanceof PhoneNumber))

                 return false;

             PhoneNumber pn = (PhoneNumber)o;

             return pn.lineNumber = lineNumber && pn.prefix == prefix && pn.areaCode = areaCode;

         }

         @Override public int hashCode() {

             return myHashCode;

         }

     }

 另外,该条目还建议不要仅仅利用某一域字段的部分信息来计算hashCode,如早期版本的String,为了提高计算哈希值的效率,只是挑选其中16个字符参与hashCode的计算,这样将会导致大量的String对象具有重复的hashCode,从而极大的降低了哈希集合的存取效率。

    
十、始终要覆盖toString:

 

      与equals和hashCode不同的是,该条目推荐应该始终覆盖该方法,以便在输出时可以得到更明确、更有意义的文字信息和表达格式。这样在我们输出调试信息和日志信息时,能够更快速的定位出现的异常或错误。如上一个条目中PhoneNumber的例子,如果不覆盖该方法,就会输出PhoneNumber@163b91 这样的不可读信息,因此也不会给我们诊断问题带来更多的帮助。以下代码重载了该方法,那么在我们调用toString或者println时,将会得到"(408)867-5309"。

@Override String toString() {

         return String.format("(%03d) %03d-%04d",areaCode,prefix,lineNumber);

     }

  对于toString返回字符串中包含的域字段,如本例中的areaCode、prefix和lineNumber,应该在该类(PhoneNumber)的声明中提供这些字段的getter方法,以避免toString的使用者为了获取其中的信息而不得不手工解析该字符串。这样不仅带来不必要的效率损失,而且在今后修改toString的格式时,也会给使用者的代码带来负面影响。提到toString返回字符串的格式,有两个建议,其一是尽量不要固定格式,这样会给今后添加新的字段信息带来一定的束缚,因为必须要考虑到格式的兼容性问题,再者就是推荐可以利用toString返回的字符串作为该类的构造函数参数来实例化该类的对象,如BigDecimal和BigInteger等装箱类。

      这里还有一点建议是和hashCode、equals相关的,如果类的实现者已经覆盖了toString的方法,那么完全可以利用toString返回的字符串来生成hashCode,以及作为equals比较对象相等性的基础。这样的好处是可以充分的保证toString、hashCode和equals的一致性,也降低了在对类进行修订时造成的一些潜在问题。尽管这不是刚性要求的,却也不失为一个好的实现方式。该建议并不是源于该条目,而是去年在看effective C#中了解到的。

    
十二、考虑实现Comparable接口:

 

      和之前提到的通用方法equals、hashCode和toString不同的是compareTo方法属于Comparable接口,该接口为其实现类提供了排序比较的规则,实现类仅需基于内部的逻辑,为compareTo返回不同的值,既A.compareTo(B) > 0可视为A > B,反之则A < B,如果A.compareTo(B) == 0,可视为A == B。在C++中由于提供了操作符重载的功能,因此可以直接通过重载操作符的方式进行对象间的比较,事实上C++的标准库中提供的缺省规则即为此,如bool operator>(OneObject o)。在Java中,如果对象实现了Comparable接口,即可充分利用JDK集合框架中提供的各种泛型算法,如:Arrays.sort(a); 即可完成a对象数组的排序。事实上,JDK中的所有值类均实现了该接口,如Integer、String等。

      Object.equals方法的通用实现准则也同样适用于Comparable.compareTo方法,如对称性、传递性和一致性等,这里就不做过多的赘述了。然而两个方法之间有一点重要的差异还是需要在这里提及的,既equals方法不应该抛出异常,而compareTo方法则不同,由于在该方法中不推荐跨类比较,如果当前类和参数对象的类型不同,可以抛出ClassCastException异常。在JDK 1.5 之后我们实现的Comparable<T>接口多为该泛型接口,不在推荐直接继承1.5 之前的非泛型接口Comparable了,新的compareTo方法的参数也由Object替换为接口的类型参数,因此在正常调用的情况下,如果参数类型不正确,将会直接导致编译错误,这样有助于开发者在coding期间修正这种由类型不匹配而引发的异常。

      在该条目中针对compareTo的相等性比较给出了一个强烈的建议,而不是真正的规则。推荐compareTo方法施加的等同性测试,在通常情况下应该返回和equals方法同样的结果,考虑如下情况:

public static void main(String[] args) {

         HashSet<BigDecimal> hs = new HashSet<BigDecimal>();

         BigDecimal bd1 = new BigDecimal("1.0");

         BigDecimal bd2 = new BigDecimal("1.00");

         hs.add(bd1);

         hs.add(bd2);

         System.out.println("The count of the HashSet is " + hs.size());

         

         TreeSet<BigDecimal> ts = new TreeSet<BigDecimal>();

         ts.add(bd1);

         ts.add(bd2);

         System.out.println("The count of the TreeSet is " + ts.size());

     }

     /*    输出结果如下:

         The count of the HashSet is 2

         The count of the TreeSet is 1

 */

由以上代码的输出结果可以看出,TreeSet和HashSet中包含元素的数量是不同的,这其中的主要原因是TreeSet是基于BigDecimal的compareTo方法是否返回0来判断对象的相等性,而在该例中compareTo方法将这两个对象视为相同的对象,因此第二个对象并未实际添加到TreeSet中。和TreeSet不同的是HashSet是通过equals方法来判断对象的相同性,而恰恰巧合的是BigDecimal的equals方法并不将这个两个对象视为相同的对象,这也是为什么第二个对象可以正常添加到HashSet的原因。这样的差异确实给我们的编程带来了一定的负面影响,由于HashSet和TreeSet均实现了Set<E>接口,倘若我们的集合是以Set<E>的参数形式传递到当前添加BigDecimal的函数中,函数的实现者并不清楚参数Set的具体实现类,在这种情况下不同的实现类将会导致不同的结果发生,这种现象极大的破坏了面向对象中的"里氏替换原则"。

      在重载compareTo方法时,应该将最重要的域字段比较方法比较的最前端,如果重要性相同,则将比较效率更高的域字段放在前面,以提高效率,如以下代码:

public int compareTo(PhoneNumer pn) {

         if (areaCode < pn.areaCode)

             return -1;

         if (areaCode > pn.areaCode)

             return 1;

             

         if (prefix < pn.prefix)

             return -1;

         if (prefix > pn.prefix)

             return 1;

             

         if (lineNumber < pn.lineNumer)

             return -1;

         if (lineNumber > pn.lineNumber)

             return 1;

         return 0;

     }

  上例给出了一个标准的compareTo方法实现方式,由于使用compareTo方法排序的对象并不关心返回的具体值,只是判断其值是否大于0,小于0或是等于0,因此以上方法可做进一步优化,然而需要注意的是,下面的优化方式会导致数值类型的作用域溢出问题。

public int compareTo(PhoneNumer pn) {

         int areaCodeDiff = areaCode - pn.areaCode;

         if (areaCodeDiff != 0)

             return areaCodeDiff;

         int prefixDiff = prefix - pn.prefix;

         if (prefixDiff != 0)

             return prefixDiff;

     

         int lineNumberDiff = lineNumber - pn.lineNumber;

         if (lineNumberDiff != 0)

             return lineNumberDiff;

         return 0;

     }
十三、使类和成员的可访问性最小化:

 

      信息隐藏是软件程序设计的基本原则之一,面向对象又为这一设计原则提供了有力的支持和保障。这里我们简要列出几项受益于该原则的优势:

      1.    更好的解除各个模块之间的耦合关系:

      由于模块间的相互调用是基于接口契约的,每个模块只是负责完成自己内部既定的功能目标和单元测试,一旦今后出现性能优化或需求变更时,我们首先需要做的便是定位需要变动的单个模块或一组模块,然后再针对各个模块提出各自的解决方案,分别予以改动和内部测试。这样便大大降低了因代码无规则交叉而带来的潜在风险,同时也缩减了开发周期。

      2.    最大化并行开发:

      由于各个模块之间保持着较好的独立性,因此可以分配更多的开发人员同时实现更多的模块,由于每个人都是将精力完全集中在自己负责和擅长的专一领域,这样不仅提高了软件的质量,也大大加快了开发的进度。

      3.    性能优化和后期维护:

      一般来说,局部优化的难度和可行性总是要好于来自整体的优化,事虽如此,然而我们首先需要做的却是如何定位需要优化的局部,在设计良好的系统中,完成这样的工作并非难事,我们只需针对每个涉及的模块做性能和压力测试,之后再针对测试的结果进行分析并拿到相对合理的解决方案。

      4.    代码的高可复用性:

      在软件开发的世界中,提出了众多的设计理论,设计原则和设计模式,之所以这样,一个非常现实的目标之一就是消除重复代码,记得《重构》中有这样的一句话:“重复代码,万恶之源”。可见提高可用代码的复用性不仅对编程效率和产品质量有着非常重要的意义,对日后产品的升级和维护也是至关重要的。说一句比较现实的话,一个设计良好的产品,即使因为某些原因导致失败,那么产品中应用到的一个个独立、可用和高效的模块也为今后的东山再起提供了一个很好的技术基础。

      让我们重新回到主题,Java通过访问控制的方式来完成信息隐藏,而我们的原则是尽可能的使每个类的域成员不被外界访问。对于包内的类而言,则尽可能少的定义公有类,遵循这样的原则可以极大的降低因包内设计或实现的改变而给该包的使用者带来的影响。当然达到这个目标的一个重要前提是定义的接口足以完成调用者的需求。

      该条目给出了一个比较重要的建议,既不要提供直接访问或通过函数返回可变域对象的实例,见下例:

      public final Thing[] values = { ... };

      即便Thing数组对象本身是final的,不能再被赋值给其他对象,然而数组内的元素是可以改变的,这样便给外部提供了一个机会来修改内部数据的状态,从而在主类未知的情况下破坏了对象内部的状态或数据的一致性。其修订方式如下:

private static final Thing[] PRIVATE_VALUES = { ... };

     public static final Thing[] values() {

         return PRIVATE_VALUES.clone();

     }

 总而言之,你应该尽可能地降低可访问性。你在仔细地设计了一个最小的公有API之后,应该防止把任何散乱的类、接口和成员变成API的一部分。除了公有静态final域的特殊情形之外,公有类都不应该包含公有域。并且要确保公有静态final域所引用的对象都是不可变的。
十四、在公有类中使用访问方法而非公有域:

 

      这个条目简短的标题已经非常清晰的表达了他的含义,我们这里将只是列出几点说明:

      1.    对于公有类而言,由于存在大量的使用者,因此修改API接口将会给使用者带来极大的不便,他们的代码也需要随之改变。如果公有类直接暴露了域字段,一旦今后需要针对该域字段添加必要的约束逻辑时,唯一的方法就是为该字段添加访问器接口,而已有的使用者也将不得不更新其代码,以避免破坏该类的内部逻辑。

      2.    对于包级类和嵌套类,公有的域方法由于只能在包内可以被访问,因而修改接口不会给包的使用者带来任何影响。

      3.    对于公有类中的final域字段,提供直接访问方法也会带来负面的影响,只是和非final对象相比可能会稍微好些,如final的数组对象,即便数组对象本身不能被修改,但是他所包含的数组成员还是可以被外部改动的,针对该情况建议提供API接口,在该接口中可以添加必要的验证逻辑,以避免非法数据的插入,如:

public <T> boolean setXxx(int index, T value) {

         if (index > myArray.length)

             return false;

         if (!(value instanceof LegalClass))

             return false;

         ...

         return true;

     }
十五、使可变性最小化:

 

      只在类构造的时候做初始化,构造之后类的外部没有任何方法可以修改类成员的状态,该对象在整个生命周期内都会保持固定不变的状态,如String、Integer等。不可变类比可变类更加易于设计、实现和使用,而且线程安全。

      使类成为不可变类应遵循以下五条原则:

      1.    不要提供任何会修改对象状态的方法;

      2.    保证类不会被扩展,既声明为final类,或将构造函数定义为私有;

      3.    使所有的域都是final的;

      4.    使所有的域都成为私有的;

      5.    确保在返回任何可变域时,返回该域的deep copy。

      见如下Complex类:

final class Complex {

         private final double re;

         private final double im;

         public Complex(double re,double im) {

             this.re = re;

             this.im = im;

         }

         public double realPart() {

             return re;

         }

         public double imaginaryPart() {

             return im;

         }

         public Complex add(Complex c) {

             return new Complex(re + c.re,im + c.im);

         }

         public Complex substract(Complex c) {

             return new Complex(re - c.re, im - c.im);

         }

         ... ...

     }

     不可变对象还有一个对象重用的优势,这样可以避免创建多余的新对象,这样也能减轻垃圾收集器的压力,如:

      public static final Complex ZERO = new Complex(0,0);

      public static final Complex ONE = new Complex(1,0);

      这样使用者可以重复使用上面定义的两个静态final类,而不需要在每次使用时都创建新的对象。

      从Complex.add和Complex.substract两个方法可以看出,每次调用他们的时候都会有新的对象被创建,这样势必会带来一定的性能影响,特别是对于copy开销比较大的对象,如包含几万Bits的BigInteger。如果我们所作的操作仅仅是修改其中的某个Bit,如bigInteger.flipBit(0),该操作只是修改了第0位的状态,而BigInteger却为此copy了整个对象并返回。鉴于此,该条目推荐为不可变对象提供一个功能相仿的可变类,如java.util.BitSet之于java.math.BigInteger。如果我们在实际开发中确实遇到刚刚提及的场景,那么使用BitSet或许是更好的选择。

      对于不可变对象还有比较重要的优化技巧,既某些关键值的计算,如hashCode,可以在对象构造时或留待某特定方法(Lazy Initialization)第一次调用时进行计算并缓存到私有域字段中,之后再获取该值时,可以直接从该域字段获取,避免每次都重新计算。这样的优化主要是依赖于不可变对象的域字段在构造后即保持不变的特征。

    
十六、复合优先于继承:

 

      由于继承需要透露一部分实现细节,因此不仅需要超类本身提供良好的继承机制,同时也需要提供更好的说明文档,以便子类在覆盖超类方法时,不会引起未知破坏行为的发生。需要特别指出的是对于跨越包边界的继承,很可能超类和子类的实现者并非同一开发人员或同一开发团队,因此对于某些依赖实现细节的覆盖方法极有可能会导致预料之外的结果,还需要指出的是,这些细节对于超类的普通用户来说往往是不看见的,因此在未来的升级中,该实现细节仍然存在变化的可能,这样对于子类的实现者而言,在该细节变化时,子类的相关实现也需要做出必要的调整,见如下代码:

//这里我们需要扩展HashSet类,提供新的功能用于统计当前集合中元素的数量,

 //实现方法是新增一个私有域变量用于保存元素数量,并每次添加新元素的方法中

 //更新该值,再提供一个公有的方法返回该值。

     public class InstrumentedHashSet<E> extends HashSet<E> {

         private int addCount = 0;

         public InstrumentedHashSet() {}

         public InstrumentedHashSet(int initCap,float loadFactor) {

             super(initCap,loadFactor);

         }

         @Override public boolean add(E e) {

             ++addCount;

             return super.add(e);

         }

         @Override public boolean addAll(Collection<? extends E> c) {

             addCount += c.size();

             return super.addAll(c);

         }

         public int getAddCount() {

             return addCount;

         }

     }

   该子类覆盖了HashSet中的两个方法add和addAll,而且从表面上看也非常合理,然而他却不能正常的工作,见下面的测试代码:

public static void main(String[] args) {

         InstrumentedHashSet<String> s = new InstrumentedHashSet<String>();

         s.addAll(Arrays.asList("Snap","Crackle","Pop"));

         System.out.println("The count of InstrumentedHashSet is " + s.getAddCount());

     }

     //The count of InstrumentedHashSet is 6

从输出结果中可以非常清楚的看出,我们得到的结果并不是我们期望的3,而是6。这是什么原因所致呢?在HashSet的内部,addAll方法是基于add方法来实现的,而HashSet的文档中也并未列出这样的细节说明。了解了原因之后,我们应该取消addAll方法的覆盖,以保证得到正确的结果。然而仍然需要指出的是,这样的细节既然未在API文档中予以说明,那么也就间接的表示这种未承诺的实现逻辑是不可依赖的,因为在未来的某个版本中他们有可能会发生悄无声息的发生变化,而我们也无法通过API文档获悉这些。还有一种情况是超类在未来的版本中新增了添加新元素的接口方法,因此我们在子类中也必须覆盖这些方法,同时也要注意一些新的超类实现细节。由此可见,类似的继承是非常脆弱的,那么该如何修订我们的设计呢?答案很简单,复合优先于继承,见如下代码:

//转发类

     class ForwardingSet<E> implements Set<E> {

         private final Set<E> s;

         public ForwardingSet(Set<E> s) {

             this.s = s;

         }

         @Override public int size() {

             return s.size();

         }

         @Override public void clear() {

             s.clear();

         }

         @Override public boolean add(E e) {

             return s.add(e);

         }

         @Override public boolean addAll(Collection<? extends E> c) {

             return s.addAll(c);

         }

         ... ...

     }

     //包装类

     class InstrumentedHashSet<E> extends ForwardingSet<E> {

         private int addCount = 0;

         public InstrumentedHashSet(int initCap,float loadFactor) {

             super(initCap,loadFactor);

         }

         @Override public boolean add(E e) {

             ++addCount;

             return super.add(e);

         }

         @Override public boolean addAll(Collection<? extends E> c) {

             addCount += c.size();

             return super.addAll(c);

         }

         public int getAddCount() {

             return addCount;

         }

     }

    由上面的代码可以看出,这种设计最大的问题就是比较琐碎,需要将接口中的方法基于委托类重新实现。

      在决定使用继承而不是复合之间,还应该问自己最后一组问题。对于你试图扩展的类,它的API中有没有缺陷呢?如果有,你是否愿意把这些缺陷传播到类的API中?继承机制会把超类API中的所有缺陷传播到子类中,而复合则允许设计新的API来隐藏这些缺陷。

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

 

      上一条目针对继承将会引发的潜在问题给出了很好的解释,本条目将继续深化这一个设计理念,并提出一些好的建议,以便在确实需要基于继承来设计时,避免这些潜在问题的发生。

      1)    为公有方法提供更为详细的说明文档,这其中不仅包扩必要的功能说明和参数描述,还要包含关键的实现细节说明,比如对其他公有方法的依赖和调用。

      在上一条目的代码示例中,子类同时覆盖了HashSet的addAll和add方法,由于二者之间存在内部的调用关系,而API文档中并没有给出详细的说明,因而子类的覆盖方法并没有得到期望的结果。

      2)    在超类中尽可能避免公有方法之间的相互调用。

      HashSet.addAll和HashSet.add给我们提供了一个很好的案例,然而这并不表示HashSet的设计和实现是有问题的,我们只能说HashSet不是为了继承而设计的类。在实际的开发中,如果确实有这样的需要又该如何呢?很简单,将公用的代码提取(extract)到一个私有的帮助方法中,再在其他的公有方法中调用该帮助方法。

      3)    可以采用设计模式中模板模式的设计技巧,在超类中将需要被覆盖的方法设定为protected级别。

      在采用这种方式设计超类时,还需要额外考虑的是哪些域字段也同时需要被设定为protected级别,以保证子类在覆盖protected方法时,可以得到必要的状态信息。

      4)    不要在超类的构造函数中调用可能被子类覆盖的方法,如public和protected级别的域方法。

      由于超类的初始化早于子类的初始化,如果此时调用的方法被子类覆盖,而覆盖的方法中又引用了子类中的域字段,这将很容易导致NullPointerException异常被抛出,见下例:

public class SuperClass {

         public SuperClass() {

             overrideMe();

         }

         public void overrideMe() {}

     }

     public final class SubClass extends SuperClass {

         private final Date d;

         SubClass() {

             d = new Date();

         }

         @Override public void overrideMe() {

             System.out.println(dd.getDay());

         }

     }

     public static void main(String[] args) {

         SubClass sub = new SubClass();

         sub.overrideMe();

     }

     5)    如果超类实现了Cloneable和Serializable接口,由于clone和readObject也有构造的能力,因此在实现这两个接口方法时也需要注意,不能调用子类的覆盖方法。

 
十八、接口优先于抽象类:

 

      众所周知,Java是不支持多重继承但是可以实现多个接口的,而这也恰恰成为了接口优于抽象类的一个重要因素。现将他们的主要差异列举如下:

      1)    现有的类可以很容易被更新,以实现新的接口。

      如果现存的类并不具备某些功能,如比较和序列化,那么我们可以直接修改该类的定义分别实现Comparable和Serializable接口。倘若Comparable和Serializable不是接口而是抽象类,那么同时继承两个抽象类是Java语法规则所不允许的,如果当前类已经继承自某个超类了,那么他将无法再扩展任何新的超类。

      2)    接口是定义mixin(混合类型)的理想选择。

      Comparable是一个典型的mixin接口,他允许类表明他的实例可以与其他的可相互比较的对象进行排序。这样的接口之所以被称为mixin,是因为他允许任选的功能可被混合到类型的主要功能中。抽象类不能被用于定义mixin,同样也是因为他们不能被更新到现有的类中:类不可能有一个以上的超类,类层次结构中也没有适当的地方来插入mixin。

      3)    接口允许我们构造非层次结构的类型框架。

      由于我们可以为任何已有类添加新的接口,而无需考虑他当前所在框架中的类层次关系,这样便给功能的扩展带来了极大的灵活性,也减少了对已有类层次的冲击。如:

public interface Singer {  //歌唱家

         AudioClip sing(Song s);

     }

     public interface SongWriter {  //作曲家

         Song compose(boolean hit);

     }

  在现实生活中,有些歌唱家本身也是作曲家。因为我们这里是通过接口来定义这两个角色的,所有同时实现他们是完全可能的。甚至可以再提供一个接口扩展自这两个接口,并提供新的方法,如:

public interface SingerWriter extends Singer, SongWriter {

         AudioClip strum();

         void actSensitive();

     }

  试想一下,如果将Singer和SongWriter定义为抽象类,那么完成这一扩展就会是非常浩大的工程,甚至可能造成"组合爆炸"的现象。

      我们已经列举出了一些接口和抽象类之间的重要差异,下面我们还可以了解一下如何组合使用接口和抽象类,以便他们能为我们设计的框架带来更好的扩展性和层级结构。在Java的Collections Framework中存在一组被称为"骨架实现"(skeletal implementation)的抽象类,如AbstractCollection、AbstractSet和AbstractList等。如果设计得当,骨架实现可以使程序员很容易的提供他们自己的接口实现。这种组合还可以让我们在设计自己的类时,根据实际情况选择是直接实现接口,还是扩展该抽象类。和接口相比,骨架实现类还存在一个非常明显的优势,既如果今后为该骨架实现类提供新的方法,并提供了默认的实现,那么他的所有子类均不会受到影响,而接口则不同,由于接口不能提供任何方法实现,因此他所有的实现类必须进行修改,为接口中新增的方法提供自己的实现,否则将无法通过编译。

      简而言之,接口通常是定义允许多个实现的类型的最佳途径。这条规则有个例外,即当演变的容易性比灵活性更为重要的时候。在这种情况下,应该使用抽象类来定义类型,但前提是必须理解并且可以接受这些局限性。如果你导出了一个重要的接口,就应该坚决考虑同时提供骨架实现类。
十九、接口只用于定义类型:

      当类实现接口时,接口就充当可以引用这个类的实例的类型。因此,类实现了接口,就表明客户端可以对这个类的实例实施某些动作。为了任何其他目的定义接口是不恰当的。如实现Comparable接口的类,表明他可以存放在排序的集合中,之后再从集合中将存入的对象有序的读出,而实现Serializable接口的类,表明该类的对象具有序列化的能力。类似的接口在JDK中大量存在。
二十、类层次优于标签类:

 

      这里先给出标签类的示例代码:

class Figure {

         enum Shape { RECT,CIRCLE };

         final Shape s;  //标签域字段,标识当前Figure对象的实际类型RECT或CIRCLE。

         double length;  //length和width均为RECT形状的专有域字段

         double width;

         double radius;    //radius是CIRCLE的专有域字段

         Figure(double radius) {                    //专为生成CIRCLE对象的构造函数

             s = Shape.CIRCLE;

             this.radius = radius;

         }

         Figure(double length,double width) {    //专为生成RECT对象的构造函数

             s = Shape.RECT;

             this.length = length;

             this.width = width;

         }

         double area() {

             switch (s) {                        //存在大量的case判断来确定实际的对象类型。

             case RECT:

                 return length * width;

             case CIRCLE:

                 return Math.PI * (radius * radius);

             default:

                 throw new AssertionError();

             }

         }

     }

   像Figure这样的类通常被我们定义为标签类,他实际包含多个不同类的逻辑,其中每个类都有自己专有的域字段和类型标识,然而他们又都同属于一个标签类,因此被混乱的定义在一起。在执行真正的功能逻辑时,如area(),他们又不得不通过case语句再重新进行划分。现在我们总结一下标签类将会给我们的程序带来哪些负面影响。

      1.    不同类型实例要求的域字段被定义在同一个类中,不仅显得混乱,而且在构造新对象实例时,也会加大内存的开销。

      2.    初始化不统一,从上面的代码中已经可以看出,在专为创建CIRCLE对象的构造函数中,并没有提供length和width的初始化功能,而是借助了JVM的缺省初始化。这样会给程序今后的运行带来潜在的失败风险。

      3.    由于没有在构造函数中初始化所有的域字段,因此不能将所有的域字段定义为final的,这样该类将有可能成为可变类。

      4.    大量的swtich--case语句,在今后添加新类型的时候,不得不修改area方法,这样便会引发因误修改而造成错误的风险。顺便说一下,这一点可以被看做《敏捷软件开发》中OCP原则的反面典型。

      那么我们需要通过什么方法来解决这样的问题呢?该条目给出了明确的答案:利用Java语句提供的继承功能。见下面的代码:

abstract class Figure {

         abstract double area();

     }

     class Circle extends Figure {

         final double radius;

         Circle(double radius) {

             this.radius = radius;

         }

         double area() {

             return Math.PI * (radius * radius);

         }

     }

     class Rectangle extends Figure {

         final double length;

         final double width;

         Rectangle(double length,double width) {

             this.length = length;

             this.width = width;

         }

         double area() {

             return length * width;

         }

     }

  现在我们为每种标签类型都定义了不同的子类,可以明显看出,这种基于类层次的设计规避了标签类的所有问题,同时也大大提供了程序的可读性和可扩展性,如:

class Square extends Rectangle {

         Square(double side) {

             super(side,side);

         }

     }

 现在我们新增了正方形类,而我们所需要做的仅仅是继承Rectangle类。

      简而言之,标签类很少有适用的场景。当你想要编写一个包含显式标签域的类时,应该考虑一下,这个标签是否可以被取消,这个类是否可以用类层次来代替。当你遇到一个包含标签域的现有类时,就要考虑将它重构到一个层次结构中去。
二十一、用函数对象表示策略:

      函数对象可以简单的理解为C语言中的回调函数,但是我想他更加类似于C++中的仿函数对象。仿函数对象在C++的标准库中(STL)有着广泛的应用,如std::less等。在Java中并未提供这样的语法规则,因此他们在实现技巧上确实存在一定的差异,然而设计理念却是完全一致的。下面是该条目中对函数对象的描述:

      Java没有提供函数指针,但是可以用对象引用实现统一的功能。调用对象上的方法通常是执行该对象(that Object)上的某项操作。然而,我们也可能定义这样一种对象,它的方法执行其他对象(other Objects)上的操作。如果一个类仅仅导出这样的一个方法,它的实例实际上就等同于一个指向该方法的指针。这样的实例被称为函数对象(Function Object),如JDK中Comparator,我们可以将该对象看做是实现两个对象之间进行比较的"具体策略对象",如:

class StringLengthComparator {

         public int compare(String s1,String s2) {

             return s1.length() - s2.length();

         }

     }

  这种对象自身并不包含任何域字段,其所有实例在功能上都是等价的,因此可以看作为无状态的对象。这样为了提供系统的性能,避免不必要的对象创建开销,我们可以将该类定义为Singleton对象,如:

class StringLengthComparator {

         private StringLengthComparator() {}    //禁止外部实例化该类

         public static final StringLengthComparator INSTANCE = new StringLengthComparator();

         public int compare(String s1,String s2) {

             return s1.length() - s2.length();

         }

     }

  StringLengthComparator类的定义极大的限制了参数的类型,这样客户端也无法再传递任何其他的比较策略。为了修正这一问题,我们需要让该类成为Comparator<T>接口的实现类,由于Comparator<T>是泛型类,因此我们可以随时替换策略对象的参数类型,如:

class StringLengthComparator implements Comparator<String> {

         public int compare(String s1,String s2) {

             return s1.length() - s2.length();

         }

     }

   简而言之,函数指针的主要用途就是实现策略模式。为了在Java中实现这种模式,要声明一个接口来表示策略,并且为每个具体策略声明一个实现了该接口的类。当一个具体策略只被使用一次时,可以考虑使用匿名类来声明和实例化这个具体的策略类。当一个具体策略是设计用来重复使用的时候,他的类通常就要被实现为私有的静态成员类,并通过公有的静态final域被导出,其类型为该策略接口。

二十二、优先考虑静态成员类:

      在Java中嵌套类主要分为四种类型,下面给出这四种类型的应用场景。

      1.    静态成员类:        

      静态成员类可以看做外部类的公有辅助类,仅当与它的外部类一起使用时才有意义。例如,考虑一个枚举,它描述了计算器支持的各种操作。Operation枚举应该是Calculator类的公有静态成员类,然后,Calculator类的客户端就可以用诸如Calculator.Operation.PLUS和Calculator.Operation.MINUS这样的名称来引用这些操作。

      2.    非静态成员类:

      一种常见的用法是定义一个Adapter,它允许外部类的实例被看做是另一个不相关的类的实例。如Map接口的实现往往使用非静态成员类来实现它们的集合视图,这些集合视图是由Map的keySet、entrySet和Values方法返回的。

      从语法上讲,静态成员类和非静态成员类之间唯一的区别是,静态成员类的声明中包含了static修饰符,尽管语法相似,但实际应用却是大相径庭。每个非静态成员类的实例中都隐含一个外部类的对象实例,在非静态成员类的实例方法内部,可以调用外围实例的方法。如果嵌套类的实例可以在它的外围类的实例之外独立存在,这个嵌套类就必须是静态成员类。由于静态成员类中并不包含外部类实例的对象引用,因此在创建时减少了内存开销。

      3.    匿名类:

      匿名类没有自己的类名称,也不是外围类的一个成员。匿名类可以出现在代码中任何允许存在表达式的地方。然而匿名类的适用性受到诸多限制,如不能执行instanceof测试,或者任何需要类名称的其他事情。我们也无法让匿名类实现多个接口,当然也不能直接访问其任何成员。最后需要说的是,建议匿名类的代码尽量短小,否则会影响程序的可读性。

      匿名类在很多时候可以用作函数对象。

      4.    局部类:

      是四种嵌套类中最少使用的类,在任何"可以声明局部变量"的地方,都可以声明局部类,并且局部类也遵守同样的作用域规则。
二十三、请不要在新代码中使用原生态类型:

      先简单介绍一下泛型的概念和声明形式。声明中具有一个或者多个类型参数的类或者接口,就是泛型类或接口,如List<E>,这其中E表示List集合中元素的类型。在Java中,相对于每个泛型类都有一个原生类与之对应,即不带任何实际类型参数的泛型名称,如List<E>的原生类型List。他们之间最为明显的区别在于List<E>包含的元素必须是E(泛型)类型,如List<String>,那么他的元素一定是String,否则将产生编译错误。和泛型不同的是,原生类型List可以包含任何类型的元素,因此在向集合插入元素时,即使插入了不同类型的元素也不会引起编译期错误。那么在运行,当List的使用从List中取出元素时,将不得不针对类型作出判断,以保证在进行元素类型转换时不会抛出ClassCastException异常。由此可以看出,泛型集合List<E>不仅可以在编译期发现该类错误,而且在取出元素时不需要再进行类型判断,从而提高了程序的运行时效率。

//原生类型的使用方式

     class TestRawType {

         private final List stamps = new List();

         public static void main(String[] args) {

             stamps.add(new Coin(...));

         }

     }

     class MyRunnable implements Runnable {

         @Override

         void run() {

             for (Iterator i = stamps.iterator(); i.hasNext(); ) {

                 Stamp s = (Stamp)i.next();  //这里将抛出类型转换异常

                 //TODO: do something.

             }

         }

     }

 以上仅为简化后的示例代码,当run()方法中抛出异常时,可以很快发现是在main()中添加了非Stamp类型的元素。如果给stamps对象添加元素的操作是在多个函数或线程中完成的,那么迅速定位到底是哪个或哪几个函数添加了非Stamp类型的元素,将会需要更多的时间去调试。

//泛型类型的使用方式

     class TestGenericType {

         private final List<Stamp> stamps = new List<Stamp>();

         public static void main(String[] args) {

             stamps.add(new Coin(...)); //该行将直接导致编译错误。

         }

     }

     class MyRunnable implements Runnable {

         @Override

         void run() {

             for (Stamp s : stamps) { //这里不再需要类型转换了。

                 //TODO: do something

             }

         }

     }

   通过以上两个例子可以看出泛型类型相对于原生类型还是有着非常明显的优势的。一般而言,原生类型的使用都是为了保持一定的兼容性,毕竟泛型是在Java 1.5中才推出的。如原有的代码中(Java 1.5之前)包含一个函数,其参数为原生类型,如void func(List l); 在之后的升级代码中,如果给该函数传入泛型类型的List<E>对象将是合法的,不会产生编译错误。同时Java的泛型对象在运行时也会被擦除类型,即List<E>擦除类型后将会变成List,Java之所以这样实现也就是为了保持向后的兼容性。

      现在我们比较一下List和List<Object>这两个类型之间的主要区别,尽管这两个集合可以包含任何类型的对象元素,但是前者是类型不安全的,而后者则明确告诉使用者可以存放任意类型的对象元素。另一个区别是,如果void func(List l)改为void func(List<Object> l),List<String>类型的对象将不能传递给func函数,因为Java将这两个泛型类型视为完全不同的两个类型。

      在新代码中不要使用原生类型,这条规则有两个例外,两者都源于“泛型信息可以在运行时被擦除”这一事实。在Class对象中必须要使用原生类型。JLS不允许使用Class的参数化类型。换句话说,List.class, String[].class和int.class都是合法的,但是List<String>.class和List<?>.class则是不合法。这条规则的第二个例外与instanceof操作符相关。由于泛型信息可以在运行时被擦除,因此在泛型类型上使用instanceof操作符是非法的。如:

private void test(Set o) {

         if (o instanceof Set) {

             Set<?> m = (Set<?>)o;

         }

     }
二十四、消除非受检警告:

      在进行泛型编程时,经常会遇到编译器报出的非受检警告(unchecked cast warnings),如:Set<Lark> exaltation = new HashSet(); 对于这样的警告要尽可能在编译期予以消除。对于一些比较难以消除的非受检警告,可以通过@SuppressWarnings("unchecked")注解来禁止该警告,前提是你已经对该条语句进行了认真地分析,确认运行期的类型转换不会抛出ClassCastException异常。同时要在尽可能小的范围了应用该注解(SuppressWarnings),如果可以应用于变量,就不要应用于函数。尽可能不要将该注解应用于Class,这样极其容易掩盖一些可能引发异常的转换。见如下代码:

public <T> T[] toArray(T[] a) {

         if (a.length < size)

             return (T[])Arrays.copyOf(elements,size,a.getClass());

         System.arraycopy(elements,0,a,0,size);

         if (a.length > size)

             a[size] = null;

         return a;

     }

    编译该代码片段时,编译器会针对(T[])Arrays.copyOf(elements,size,a.getClass())语句产生一条非受检警告,现在我们需要做的就是添加一个新的变量,并在定义该变量时加入@SuppressWarnings注解,见如下修订代码:

public <T> T[] toArray(T[] a) {

         if (a.length < size) {

             //TODO: 加入更多的注释,以便后面的维护者可以非常清楚该转换是安全的。

             @SuppressWarnings("unchecked") T[] result =

                 (T[])Arrays.copyOf(elements,size,a.getClass());

             return result;

         }

         System.arraycopy(elements,0,a,0,size);

         if (a.length > size)

             a[size] = null;

         return a;

     }

  这个方法可以正确的编译,禁止非受检警告的范围也减少到了最小。

      为什么要消除非受检警告,还有一个比较重要的原因。在开始的时候,如果工程中存在大量的未消除非受检警告,开发者认真分析了每一处警告并确认不会产生任何运行时错误,然而所差的是在分析之后没有消除这些警告。那么在之后的开发中,一旦有新的警告发生,极有可能淹没在原有的警告中,而没有被开发者及时发现,最终成为问题的隐患。如果恰恰相反,在分析之后消除了所有的警告,那么当有新警告出现时将会立即引起开发者的注意。
二十五、列表优先于数组:

      数组和泛型相比,有两个重要的不同点。首先就是数组是协变的,如:Object[] objArray = new Long[10]是合法的,因为Long是Object的子类,与之相反,泛型是不可协变的,如List<Object> objList = new List<Long>()是非法的,将无法通过编译。因此泛型可以保证更为严格的类型安全性,一旦出现插入元素和容器声明时不匹配的现象是,将会在编译期报错。二者的另一个区别是数组是具体化的,因此数组会在运行时才知道并检查它们的元素类型约束。如将一个String对象存储在Long的数组中时,就会得到一个ArrayStoreException异常。相比之下,泛型则是通过擦除来实现的。因此泛型只是在编译时强化类型信息,并在运行时丢弃它们的元素类型信息。擦除就是使泛型可以与没有使用泛型的代码随意进行交互。由此可以得出混合使用泛型和数组是比较危险的,因为Java的编译器禁止了这样的使用方法,一旦使用,将会报编译错误。见如下用例:

public void test() {

         //这里我们先假设该语句可以通过编译

         List<String>[] stringLists = new List<String>[1];

         //该语句是正常的,intList中将仅包含值为42的一个整型元素

         List<Integer> intList = Arrays.asList(42);

         //该语句也是合法的,因为数组支持协变

         Object[] objects = stringLists;

         //由于泛型对象在运行时是擦除对象类型信息的,擦除后intList将变为List类型

         //而objects是Object类型的数组,List本身也是Object的子类,因此下面的语句合法。

         objects[0] = intList;

         //下面的语句将会抛出ClassCastException异常。很显然stringLists[0]是List<Integer>对象。

         String s = stringLists[0].get(0);

     }

 从以上示例得出,当你得到泛型数组创建错误时,最好的解决办法通常是优先使用集合类型List<E>,而不是数组类型E[]。这样可能会损失一些性能或简洁性,但是换回的却是更高的类型安全性和互用性。见如下示例代码:

static Object reduce(List l, Function f, Object initVal) {

         Object[] snapshot = l.toArray();

         Object result = initVal;

         for (Object o : snapshot) {

             return = f.apply(result,o);

         }

         return result;

     }

     interface Function {

         Object apply(Object arg1,Object arg2);

     }

  事实上,从以上函数和接口的定义可以看出,如果他们被定义成泛型函数和泛型接口,将会得到更好的类型安全,同时也没有对他们的功能造成任何影响,见如下修改为泛型的示例代码:

static <E> E reduce(List<E> l,Function<E> f,E initVal) {

         E[] snapshot = l.toArray();

         E result = initVal;

         for (E e : snapshot) {

             result = f.apply(result,e);

         }

         return result;

     }

     interface Function<E> {

         E apply(E arg1,E arg2);

     }

这样的写法回提示一个编译错误,即E[] snapshot = l.toArray();是无法直接转换并赋值的。修改方式也很简单,直接强转就可以了,如E[] snapshot = (E[])l.toArray();在强转之后,仍然会收到编译器给出的一条警告信息,即无法在运行时检查转换的安全性。尽管结果证明这样的修改之后是可以正常运行的,但是这样的写法确实也是不安全的,更好的办法是通过List<E>替换E[],见如下修改后的代码:

static <E> E reduce(List<E> l,Function<E> f,E initVal) {

         E[] snapshot = new ArrayList<E>(l);

         E result = initVal;

         for (E e : snapshot) {

             result = f.apply(result,e);

         }

         return result;

     }
二十六、优先考虑泛型:

      如下代码定义了一个非泛型集合类:

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

             Object result = elements[--size];

             elements[size] = null;

             return result;

         }

         public boolean isEmpty() {

             return size == 0;

         }

         private void ensureCapacity() {

             if (elements.length == size)

                 elements = Arrays.copyOf(elements,2 * size + 1);

         }

     }

     在看与之相对于的泛型集合实现方式:

public class Stack<E> {

         private E[] elements;

         private int size = 0;

         private static final int DEFAULT_INITIAL_CAPACITY = 16;

         public Stack() {

             elements = new E[DEFAULT_INITIAL_CAPACITY];

         }

         public void push(E e) {

             ensureCapacity();

             elements[size++] = e;

         }

         public E pop() {

             if (size == 0)

                 throw new EmptyStackException();

             E result = elements[--size];

             elements[size] = null;

             return result;

         }

         public boolean isEmpty() {

             return size == 0;

         }

         private void ensureCapacity() {

             if (elements.length == size)

                 elements = Arrays.copyOf(elements,2 * size + 1);

         }

     }

  上面的泛型集合类Stack<E>在编译时会引发一个编译错误,即elements = new E[DEFAULT_INITIAL_CAPACITY]语句不能直接实例化泛型该类型的对象。修改方式如下:elements = (E[])new Object[DEFAULT_INITIAL_CAPACITY],只要我们保证所有push到该数组中的对象均为该类型的对象即可,剩下需要做的就是添加注解以消除该警告:

@SuppressWarning("unchecked")

     public Stack() {

         elements = (E[])new Object[DEFAULT_INITIAL_CAPACITY];

     }

  总而言之,使用泛型比使用需要在客户端代码中进行转换的类型来的更加安全,也更加容易。在设计新类型的时候,要确保它们不需要这种转换就可以使用。这通常意味着要把类做成是泛型的。
二十七、优先考虑泛型方法:

      和优先选用泛型类一样,我们也应该优先选用泛型方法。特别是静态工具方法尤其适合于范兴华。如Collections.sort()和Collections.binarySearch()等静态方法。见如下非泛型方法:

public static Set union(Set s1, Set s2) {

         Set result = new HashSet(s1);

         result.addAll(s2);

         return result;

     }

 这个方法在编译时会有警告报出。为了修正这些警告,最好的方法就是使该方法变为类型安全的,要将方法声明修改为声明一个类型参数,表示这三个集合的元素类型,并在方法中使用类型参数,见如下修改后的泛型方法代码:

public static <E> Set<E> union(Set<E> s1,Set<E> s2) {

         Set<E> result = new HashSet<E>(s1);

         result.addAll(s2);

         return result;

     }

和调用泛型对象构造函数来创建泛型对象不同的是,在调用泛型函数时无须指定函数的参数类型,而是通过Java编译器的类型推演来填充该类型信息,见如下泛型对象的构造:

      Map<String,List<String>> anagrams = new HashMap<String,List<String>>();

      很明显,以上代码在等号的两边都显示的给出了类型参数,并且必须是一致的。为了消除这种重复,可以编写一个泛型静态工厂方法,与想要使用的每个构造器相对应,如:

public static <K,V> HashMap<K,V> newHashMap() {

         return new HashMap<K,V>();

     }

 我们的调用方式也可以改为:Map<String,List<String>> anagrams = newHashMap();

      除了在以上的情形下使用泛型函数之外,我们还可以在泛型单例工厂的模式中应用泛型函数,这些函数通常为无状态的,且不直接操作泛型对象的方法,见如下示例:

public interface UnaryFunction<T> {

         T apply(T arg);

     }

     private static UnaryFunction<Object> IDENTITY_FUNCTION

         = new UnaryFunction<Object>() {

             public Object apply(Object arg) {

                 return arg;

             }

         };

     @SuppressWarning("unchecked")

     public static <T> UnaryFunction<T> identityFunction() {

         return (UnaryFunction<T>)IDENTITY_FUNCTION;

     }

调用方式如下:

public static void main(String[] args) {

         String[] strings = {"jute","hemp","nylon"};

         UnaryFunction<String> sameString = identityFunction();

         for (String s : strings)

             System.out.println(sameString.apply(s));

         

         Number[] numbers = {1,2.0,3L};

         UnaryFunction<Number> sameNumber = identityFunction();

         for (Number n : numbers)

             System.out.println(sameNumber.apply(n));

     }

 对于该静态函数,如果我们为类型参数添加更多的限制条件,如参数类型必须是Comparable<T>的实现类,这样我们的函数对象便可以基于该接口做更多的操作,而不仅仅是像上例中只是简单的返回参数对象,见如下代码:

public static <T extends Comparable<T>> T max(List<T> l) {

         Iterator<T> i = l.iterator();

         T result = i.next();

         while (i.hasNext()) {

             T t = i.next();

             if (t.compareTo(result) > 0)

                 result = T;

         }

         return result;

     }

总而言之,泛型方法就想泛型对象一样,提供了更为安全的使用方式。

    
二十八、利用有限制通配符来提升API的灵活性:

 

      前面的条目已经解释为什么泛型不支持协变,而在我们的实际应用中可能确实需要一种针对类型参数的特化,幸运的是,Java提供了一种特殊的参数化类型,称为有限制的通配符类型(bounded wildcard type),来处理类似的情况。见如下代码:

public class Stack<E> {

         public Stack();

         public void push(E e);

         public E pop();

         public boolean isEmpty();

     }

 现在我们需要增加一个方法:

public void pushAll(Iterable<E> src) {

         for (E e : src)

             push(e);

     }

 如果我们的E类型为Number,而我们却喜欢将Integer对象也插入到该容器中,现在的写法将会导致编译错误,因为即使Integer是Number的子类,由于类型参数是不可变的,因此这样的写法也是错误的。需要进行如下的修改:

public void pushAll(Iterable<? extends E> src) {

         for (E e : src)

             push(e);

     }

 修改之后该方法便可以顺利通过编译了。因为参数中Iterable的类型参数被限制为E(Number)的子类型即可。

      既然有了pushAll方法,我们可能也需要新增一个popAll的方法与之对应,见如下代码:

public void popAll(Collection<E> dst) {

         while (!isEmpty())

             dst.add(pop());

     }

    popAll方法将当前容器中的元素全部弹出,并以此添加到参数集合中。如果Collections中的类型参数和Stack完全一致,这样的写法不会有任何问题,然而在实际的应用中,我们通常会将Collection中的元素视为更通用的对象类型,如Object,见如下应用代码:

      Stack<Number> numberStack = new Stack<Number>();

      Collection<Object> objs = createNewObjectCollection();

      numberStack.popAll(objs);

      这样的应用方法将会导致编译错误,因为Object和Stack中Number参数类型是不匹配的,而我们对目标容器中对象是否为Number并不关心,Object就已经满足我们的需求了。为了到达这种更高的抽象,我们需要对popAll做如下的修改:

public void popAll(Collection<? super E> dst) {

         while (!isEmpty())

             dst.add(pop());

     }

 修改之后,之前的使用方式就可以顺利通过编译了。因为参数集合的类型参数已经被修改为E(Number)的超类即可。

      这里给出了一个助记方式,便于我们记住需要使用哪种通配符类型:

      PECS(producer-extends, consumer-super)

      解释一下,如果参数化类型表示一个T生产者,就使用<? extends T>,如果它表示一个T消费者,就使用<? super T>。在我们上面的例子中,pushAll的src参数产生E实例供Stack使用,因此src相应的类型为Iterable<? extends E>;popAll的dst参数通过Stack消费E实例,因此dst相应的类型为Collection<? super E>。PECS这个助记符突出了使用通配符类型的基本原则。

      在上一个条目中给出了下面的泛型示例函数:

      public static <E> Set<E> union(Set<E> s1, Set<E> s2);

      这里的s1和s2都是生产者,根据PECS原则,它们的声明可以改为:

      public static <E> Set<E> union(Set<? extends E> s1,Set<? extends E> s2);

      由于泛型函数在调用时,其参数类型是可以通过函数参数的类型推演出来的,如果上面的函数被如下方式调用时,将会导致Java的编译器无法推演出泛型参数的实际类型,因此引发了编译错误。

      Set<Integer> integers = new Set<Integer>();

      Set<Double> doubles = new Set<Double>();

      Set<Number> numbers = union(integers,doubles);

      如果想顺利通过编译并得到正确的执行结果,我们只能通过显示的方式指定该函数类型参数的实际类型,从而避免了编译器的类型参数自动推演,见修改后的代码:

      Set<Number> numbers = Union.<Number>union(integers,doubles);

    

      现在我们再来看一下前面也给出过的max方法,其初始声明为:

      public static <T extends Comparable<T>> T max<List<T> srcList);

      下面是修改过的使用通配符类的声明:

      public static <T extends Comparable<? super T>> T max(List<? extends T> srcList);

      下面将逐一给出新声明的解释:

      1.    函数参数srcList产生了T实例,因此将类型从List<T>改为List<? extends T>;

      2.    最初T被指定为扩展Comparable<T>,然而Comparable又是T的消费者,用于比较两个T之间的顺序关系。因此参数化类型Comparable<T>被替换为Comparable<? super T>。

      注:Comparator和Comparable一样,他们始终都是消费者,因此Comparable<? super T>优先于Comparable<T>。

 
二十九、优先考虑类型安全的异构容器:

 

      泛型通常用于集合,如Set和Map等。这样的用法也就限制了每个容器只能有固定数目的类型参数,一般来说,这也确实是我们想要的。然而有的时候我们需要更多的灵活性,如数据库可以用任意多的Column,如果能以类型安全的方式访问所有Columns就好了,幸运的是有一种方法可以很容易的做到这一点,就是将key进行参数化,而不是将容器参数化,见以下代码:

public class Favorites {

         public <T> void putFavorite(Class<T> type,T instance);

         public <T> T getFavorite(Class<T> type);

     }

下面是该类的使用示例:

public static void main(String[] args) {

         Favorites f = new Favorites();

         f.putFavorite(String.class,"Java");

         f.putFavorite(Integer.class,0xcafebabe);

         f.putFavorite(Class.class,Favorites.class);

         String favoriteString = f.getFavorite(String.class);

         int favoriteInteger = f.getFavorite(Integer.class);

         Class<?> favoriteClass = f.getFavorite(Class.class);

         System.out.printf("%s %x %s\n",favoriteString

             ,favoriteInteger,favoriteClass.getName());

     }

     //Java cafebabe Favorites

  这里Favorites实例是类型安全的:当你请求String的时候,它是不会给你Integer的。同时它也是异构的容器,不像普通的Map,他的所有键都是不同类型的。下面就是Favorites的具体实现:

public class Favorites {

         private Map<Class<?>,Object> favorites =

             new HashMap<Class<?>,Object>();

         public <T> void putFavorite(Class<T> type,T instance) {

             if (type == null)

                 throw new NullPointerException("Type is null");

             favorites.put(type,type.cast(instance));

         }

         public <T> T getFavorite(Class<T> type) {

             return type.cast(favorites.get(type));

         }

     }

可以看出每个Favorites实例都得到一个Map<Class<?>,Object>容器的支持。由于该容器的值类型为Object,为了进一步确实类型的安全性,我们在put的时候通过Class.cast()方法将Object参数尝试转换为Class所表示的类型,如果类型不匹配,将会抛出ClassCastException异常。以此同时,在从Map中取出值对象的时候,由于该对象当前的类型是Object,因此我们需要再次利用Class.cast()函数将其转换为我们的目标类型。

      对于Favorites类的put/get方法,有一个非常明显的限制,即我们无法将“不可具体化”类型存入到该异构容器中,如List<String>、List<Integer>等泛型类型。这样的限制主要源于Java中泛型类型在运行时的类型擦出机制,即List<String>.class和List<Integer>.class是等同的对象,均为List.class。如果Java编译器通过了这样的调用代码,那么List<String>.class和List<Integer>.class将会返回相同的对象引用,从而破坏Favorites的内部结构。
三十、用enum代替int常量:

 

      枚举类型是指由一组固定的常量组成合法值的类型,该特征是在Java 1.5 中开始被支持的,之前的Java代码都是通过“公有静态常量域字段”的方法来简单模拟枚举的,如:

      public static final int APPLE_FUJI = 0;

      public static final int APPLE_PIPPIN = 1;

      public static final int APPLE_GRANNY_SMITH = 2;

      ... ...

      public static final int ORANGE_NAVEL = 0;

      public static final int ORANGE_TEMPLE = 1;

      public static final int ORANGE_BLOOD = 2;

      这样的写法是比较脆弱的。首先是没有提供相应的类型安全性,如两个逻辑上不相关的常量值之间可以进行比较或运算(APPLE_FUJI - ORANGE_TEMPLE),再有就是常量int是编译时常量,被直接编译到使用他们的客户端中。如果与该常量关联的int发生了变化,客户端就必须重新编译。如果没有重新编译,程序还是可以执行,但是他们的行为将不确定。

      下面我们来看一下Java 1.5 中提供的枚举的声明方式:

      public enum Apple { FUJI, PIPPIN, GRANNY_SMITH }

      public enum Orange { NAVEL, TEMPLE, BLOOD }

      和“公有静态常量域字段”不同的是,如果函数的参数是枚举类型,如Apple,那么他的实际值只能来自于该枚举所声明的枚举值,即FUJI, PIPPIN, GRANNY_SMITH。如果试图将Apple和Orange中的枚举值进行比较,将会导致编译错误。

      和C/C++中提供的枚举不同的是,Java中允许在枚举中添加任意的方法和域,并实现任意的接口。下面先给出一个带有域方法和域字段的枚举声明:

public enum Planet {

         MERCURY(3.302e+23,2.439e6),

         VENUS(4.869e+24,6.052e6),

         EARTH(5.975e+24,6.378e6),

         MARS(6.419e+23,3.393e6),

         JUPITER(1.899e+27,7.149e7),

         SATURN(5.685e+26,6.027e7),

         URANUS(8.683e+25,2.556e7),

         NEPTUNE(1.024e+26,2.477e7);

         private final double mass;   //千克

         private final double radius; //米

         private final double surfaceGravity;

         private static final double G = 6.67300E-11;

         Planet(double mass,double radius) {

             this.mass = mass;

             this.radius = radius;

             surfaceGravity = G * mass / (radius * radius);

         }

         public double mass() {

             return mass;

         }

         public double radius() {

             return radius;

         }

         public double surfaceGravity() {

             return surfaceGravity;

         }

         public double surfaceWeight(double mass) {

             return mass * surfaceGravity;

         }

     }

   在上面的枚举示例代码中,已经将数据和枚举常量关联起来了,因此需要声明实例域字段,同时编写一个带有数据并将数据保存在域中的构造器。枚举天生就是不可变的,因此所有的域字段都应该为final的。下面看一下该枚举的应用示例:

public class WeightTable {

         public static void main(String[] args) {

             double earthWeight = Double.parseDouble(args[0]);

             double mass = earthWeight/Planet.EARTH.surfaceGravity();

             for (Planet p : Planet.values())

                 System.out.printf("Weight on %s is %f%n",p,p.surfaceWeight(mass));

         }

     }

     // Weight on MERCURY is 66.133672

     // Weight on VENUS is 158.383926

     // Weight on EARTH is 175.000000

     // Weight on MARS is 66.430699

     // Weight on JUPITER is 442.693902

     // Weight on SATURN is 186.464970

     // Weight on URANUS is 158.349709

     // Weight on NEPTUNE is 198.846116

    枚举的静态方法values()将按照声明顺序返回他的值数组。枚举的toString方法返回每个枚举值的声明名称。

      在实际的编程中,我们常常需要针对不同的枚举常量提供不同的数据操作行为,见如下代码:

public enum Operation {

         PLUS,MINUS,TIMES,DIVIDE;

         double apply(double x,double y) {

             switch (this) {

                 case PLUS: return x + y;

                 case MINUS: return x - y;

                 case TIMES: return x * y;

                 case DIVIDE: return x / y;

             }

             throw new AssertionError("Unknown op: " + this);

         }

     }

上面的代码已经表达出这种根据不同的枚举值,执行不同的操作。但是上面的代码在设计方面确实存在一定的缺陷,或者说漏洞,如果我们新增枚举值的时候,所有和apply类似的域函数,都需要进行相应的修改,如有遗漏将会导致异常的抛出。幸运的是,Java的枚举提供了一种更好的方法可以将不同的行为与每个枚举常量关联起来:在枚举类型中声明一个抽象的apply方法,并在特定于常量的类主体中,用具体的方法覆盖每个常量的抽象apply方法,如:

public enum Operation {

         PLUS { double apply(double x,double y) { return x + y;} },

         MINUS { double apply(double x,double y) { return x - y;} },

         TIMES { double apply(double x,double y) { return x * y;} },

         DIVIDE { double apply(double x,double y) { return x / y;} };

         abstract double apply(double x, double y);

     }

  这样在添加新枚举常量时就不会轻易忘记提供相应的apply方法了。我们在进一步看一下如何将枚举常量和特定的数据进行关联,见如下代码:

public enum Operation {

         PLUS("+") { double apply(double x,double y) { return x + y;} },

         MINUS("-") { double apply(double x,double y) { return x - y;} },

         TIMES("*") { double apply(double x,double y) { return x * y;} },

         DIVIDE("/") { double apply(double x,double y) { return x / y;} };

         private final String symbol;

         Operation(String symbol) {

             this.symbol = symbol;

         }

         @Override public String toString() {

             return symbol;

         }

         abstract double apply(double x, double y);

     }

下面给出以上代码的应用示例:

public static void main(String[] args) {

         double x = Double.parseDouble(args[0]);

         double y = Double.parseDouble(args[1]);

         for (Operation op : Operation.values())

             System.out.printf("%f %s %f = %f%n",x,op,y,op.apply(x,y));

         }

     }

     // 2.000000 + 4.000000 = 6.000000

     // 2.000000 - 4.000000 = -2.000000

     // 2.000000 * 4.000000 = 8.000000

     // 2.000000 / 4.000000 = 0.500000

没有类型有一个自动产生的valueOf(String)方法,他将常量的名字转变为枚举常量本身,如果在枚举中覆盖了toString方法(如上例),就需要考虑编写一个fromString方法,将定制的字符串表示法变回相应的枚举,见如下代码:

public enum Operation {

         PLUS("+") { double apply(double x,double y) { return x + y;} },

         MINUS("-") { double apply(double x,double y) { return x - y;} },

         TIMES("*") { double apply(double x,double y) { return x * y;} },

         DIVIDE("/") { double apply(double x,double y) { return x / y;} };

         private final String symbol;

         Operation(String symbol) {

             this.symbol = symbol;

         }

         @Override public String toString() {

             return symbol;

         }

         abstract double apply(double x, double y);

         //新增代码

         private static final Map<String,Operation> stringToEnum = new HashMap<String,Operation>();

         static {

             for (Operation op : values())

                 stringToEnum.put(op.toString(),op);

         }

         public static Operation fromString(String symbol) {

             return stringToEnum.get(symbol);

         }

     }

   需要注意的是,我们无法在枚举常量构造的时候将自身放入到Map中,这样会导致编译错误。与此同时,枚举构造器不可以访问枚举的静态域,除了编译时的常量域之外。

    
三十一、用实例域代替序数:

 

      Java中的枚举提供了ordinal()方法,他返回每个枚举常量在类型中的数字位置,如:

public enum Color {

         WHITE,RED,GREEN,BLUE,ORANGE,BLACK;

         public int indexOfColor() {

             return ordinal() + 1;

         }

     }

  上面的枚举中提供了一个获取颜色索引的方法(indexOfColor),该方法将返回颜色值在枚举类型中的声明位置,如果我们的外部程序依赖了该顺序值,那么这将会是非常危险和脆弱的,因为一旦这些枚举值的位置出现变化,或者在已有枚举值的中间加入新的枚举值时,都将导致该索引值的变化。该条目推荐使用实例域的方式来代替枚举提供的序数值,见如下修改后的代码:

public enum Color {

         WHITE(1),RED(2),GREEN(3),ORANGE(4),BLACK(5);

         private final int indexOfColor;

         Color(int index) {

             this.indexOfColor = index;

         }

         public int indexOfColor() {

             return indexOfColor;

         }

     }

     Enum规范中谈到ordinal时这么写道:“大多数程序员都不需要这个方法。它是设计成用于像EnumSet和EnumMap这种基于枚举的通用数据结构的。”除非你在编写的是这种数据结构,否则最好避免使用ordinal()方法。

    
三十二、用EnumSet代替位域:

 

      下面的代码给出了位域的实现方式:

public class Text {

         public static final int STYLE_BOLD = 1 << 0;

         public static final int STYLE_ITALIC = 1 << 1;

         public static final int STYLE_UNDERLINE = 1 << 2;

         public static final int STYLE_STRIKETHROUGH = 1 << 3;

         public void applyStyles(int styles) { ... }

     }

 这种表示法让你用OR位运算将几个常量合并到一个集合中,使用方式如下:

      text.applyStyles(Text.STYLE_BOLD | Text.STYLE_ITALIC);

      Java中提供了EnumSet类,该类继承自Set接口,同时也提供了丰富的功能,类型安全性,以及可以从任何其他Set实现中得到的互用性。但是在内部具体实现上,没有EnumSet内容都表示为位矢量。如果底层的枚举类型有64个或者更少的元素,整个EnumSet就用单个long来表示,因此他的性能也是可以比肩位域的。与此同时,他提供了大量的操作方法,其实现也是基于位操作的,但是相比于手工位操作,由于EnumSet替我们承担了这部分的开发,从而也避免了一些容易出现的低级错误,代码的美观程度也会有所提升,见如下修改的代码:

public class Text {

         public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }

         public void applyStyles(Set<Style> styles) { ... }

     }

  新的使用方式如下:

      text.applyStyles(EnumSet.of(Style.BOLD,Style.ITALIC));

      需要说明的是,EnumSet提供了丰富的静态工厂来轻松创建集合。

 
三十三、用EnumMap代替序数索引:

 

      前面的条目已经给出了尽量不要直接使用枚举的ordinal()方法的原因,这里就不在做过多的赘述了。在这个条目中,只是再一次给出了ordinal()的典型用法,与此同时也再一次提供了一个更为合理的解决方案用于替换ordinal()方法,从而进一步证明我们在编码过程中应该尽可能减少对枚举中ordinal()函数的依赖。见如下代码:

public class Herb {

         public enum Type { ANNUAL, PERENNIAL, BIENNIAL }

         private final String name;

         private final Type type;

         Herb(String name, Type type) {

             this.name = name;

             this.type = type;

         }

         @Override public String toString() {

             return name;

         }

     }

     public static void main(String[] args) {

         Herb[] garden = getAllHerbsFromGarden();

         Set<Herb> herbsByType = (Set<Herb>[])new Set[Herb.Type.values().length];

         for (int i = 0; i < herbsByType.length; ++i) {

             herbsByType[i] = new HashSet<Herb>();

         }

         for (Herb h : garden) {

             herbsByType[h.type.ordinal()].add(h);

         }

         for (int i = 0; i < herbsByType.length; ++i) {

             System.out.printf("%s: %s%n",Herb.Type.values()[i],herbByType[i]);

         }

     }

这里我需要简单描述一下上面代码的应用场景:在一个花园里面有很多的植物,它们被分成3类,分别是一年生(ANNUAL)、多年生(PERENNIAL)和两年生(BIENNIAL),正好对应着Herb.Type中的枚举值。现在我们需要做的是遍历花园中的每一个植物,并将这些植物分为3类,最后再将分类后的植物分类打印出来。下面将提供另外一种方法,即通过EnumMap来实现和上面代码相同的逻辑:

public static void main(String[] args) {

         Herb[] garden = getAllHerbsFromGarden();

         Map<Herb.Type,Set<Herb>> herbsByType =

             new EnumMap<Herb.Type,Set<Herb>>(Herb.Type.class);

         for (Herb.Type t : Herb.Type.values()) {

             herbssByType.put(t,new HashSet<Herb>());

         }

         for (Herb h : garden) {

             herbsByType.get(h.type).add(h);

         }

         System.out.println(herbsByType);

     }

 和之前的代码相比,这段代码更加清晰,也更加安全,运行效率方面也是可以与使用ordinal()的方式想媲美的。

 
三十四、用接口模拟可伸缩的枚举:

 

      枚举是无法被扩展(extends)的,这是一个无法回避的事实。如果我们的操作中存在一些基础操作,如计算器中的基本运算类型(加减乘除)。然而对于有些用户来讲,他们也可以使用更高级的操作,如求幂和求余等。针对这样的需求,该条目提出了一种非常巧妙的设计方案,即利用枚举可以实现接口这一事实,我们将API的参数定义为该接口,而不是具体的枚举类型,见如下代码:

public interface Operation {

         double apply(double x,double y);

     }

     public enum BasicOperation implements Operation {

         PLUS("+") {

             public double apply(double x,double y) { return x + y; }

         },

         MINUS("-") {

             public double apply(double x,double y) { return x - y; }

         },

         TIMES("*") {

             public double apply(double x,double y) { return x * y; }

         },

         DIVIDE("/") {

             public double apply(double x,double y) { return x / y; }

         };

         private final String symbol;

         BasicOperation(String symbol) {

             this.symbol = symbol;

         }

         @Override public String toString() {

             return symbol;

         }

     }

     public enum ExtendedOperation implements Operation {

         EXP("^") {

             public double apply(double x,double y) {

                 return Math.pow(x,y);

             }

         },

         REMAINDER("%") {

             public double apply(double x,double y) {

                 return x % y;

             }

         };

         private final String symbol;

         ExtendedOperation(String symbol) {

             this.symbol = symbol;

         }

         @Override public String toString() {

             return symbol;

         }

     }

  通过以上的代码可以看出,在任何可以使用BasicOperation的地方,我们也同样可以使用ExtendedOperation,只要我们的API是基于Operation接口的,而非BasicOperation或ExtendedOperation。下面为以上代码的应用示例:

public static void main(String[] args) {

         double x = Double.parseDouble(args[0]);

         double y = Double.parseDouble(args[1]);

         test(ExtendedOperation.class,x,y);

     }

     private static <T extends Enum<T> & Operation> void test(

         Class<T> opSet,double x,double y) {

         for (Operation op : opSet.getEnumConstants()) {

             System.out.printf("%f %s %f = %f%n",x,op,y,op.apply(x,y));

         }

     }

   注意,参数Class<T> opSet将推演出类型参数的实际类型,即上例中的ExtendedOperation。与此同时,test函数的参数类型限定确保了类型参数既是枚举类型又是Operation的实现类,这正是遍历元素和执行每个元素相关联的操作所必须的。
三十五、注解优先于命名模式

目前使用很少,没有什么深刻体会,理解不透

 
三十六、坚持使用Override注解

在你想要覆盖超类声明的每个方法中声明使用Override注解,这样编译器就会帮助你发现是否正确覆盖了一个方法。

例外:在具体的类中,不必标注你确信覆盖了的抽象的方法;实现接口的类中,也不必标注出你想要哪些方法来覆盖接口方法,这两种情况编译器都会帮助你提醒,没有覆盖,抛出错误。当然标注了也没有什么坏处。

 

但是在抽象类和接口覆盖超类或者超接口的时候,坚持使用Override注解

 
三十七、记接口定义类型

不知所云
三十八、检查参数的有效性:

 

      绝大多数方法和构造器对于传递给它们的参数值都会有些限制。比如,索引值必须大于等于0,且不能超过其最大值,对象不能为null等。这样就可以在导致错误的源头将错误捕获,从而避免了该错误被延续到今后的某一时刻再被引发,这样就是加大了错误追查的难度。就如同编译期能够报出的错误总比在运行时才发现要更好一些。事实上,我们不仅仅需要在函数的内部开始出进行这些通用的参数有效性检查,还需要在函数的文档中给予明确的说明,如在参数非法的情况下,会抛出那些异常,或导致函数返回哪些错误值等,见如下代码示例:

/**

      * Returns a BigInteger whose value is(this mod m). This method

      * differs from the remainder method in that it always returns a

      * non-negative BigInteger.

      * @param m the modulus, which must be positive.

      * @return this mod m.

      * @throws ArithmeticException if m is less than or equal to 0.

 */

      public BigInteger mod(BigInteger m) {

          if (m.signum() <= 0)

              throw new ArithmeticException("Modulus <= 0: " + m);

          ... //Do the computation.

      }

   是不是我们为所有的方法均需要做出这样的有效性检查呢?对于未被导出的方法,如包方法等,你可以控制这个方法将在哪些情况下被调用,因此这时可以使用断言来帮助进行参数的有效性检查,如:

private static void sort(long a[],int offset,int length) {

          assert(a != null);

          assert(offset >= 0 && offset <= a.length);

          assert(length >= 0 && length <= a.length - offset);

          ... //Do the computation

      }

  和通用的检查方式不同,断言在其条件为真时,无论外部包得客户端如何使用它。断言都将抛出AssertionError。它们之间的另一个差异在于如果断言没有起到作用,即-ea命令行参数没有传递给java解释器,断言将不会有任何开销,这样我们就可以在调试期间加入该命令行参数,在发布时去掉该命令行选项,而我们的代码则不需要任何改动。

       需要强调的是,对于有些函数的参数,其在当前函数内并不使用,而是留给该类其他函数内部使用的,比较明显的就是类的构造函数,构造函数中的很多参数都不一样用于构造器内,只是在构造的时候进行有些赋值操作,而这些参数的真正使用者是该类的其他函数,对于这种情况,我们就更需要在构造的时候进行参数的有效性检查,否则一旦将该问题释放到域函数的时候,再追查该问题的根源,将不得不付出更大的代价和更多的调试时间。

       对该条目的说法确实存在着一种例外情况,在有些情况下有效性检查工作的开销是非常大的,或者根本不切实际,因为这些检查已经隐含在计算过程中完成了,如Collections.sort(List),容器中对象的所有比较操作均在该函数执行时完成,一旦比较操作失败将会抛出ClassCastException异常。因此对于sort来讲,如果我们提前做出有效性检查将是毫无意义的。

     
三十九、必要时进行保护性拷贝:

 

      如果你的对象没有做很好的隔离,那么对于调用者而言,则有机会破坏该对象的内部约束条件,因此我们需要保护性的设计程序。该破坏行为一般由两种情况引起,首先就是恶心的破坏,再有就是调用者无意识的误用,这两种条件下均有可能给你的类带来一定的破坏性,见如下代码:

public final class Period {

         private final Date start;

         private final Date end;

         public Period(Date start,Date end) {

             if (start.compareTo(end) > 0) {

                 throw new IllegalArgumentException(start + "After " + end);

             this.start = start;

             this.end = end;

         }

         public Date start() {

             return start;

         }

         public Date end() {

             return end;

         }

     }

    从表面上看,该类的实现确实对约束性的条件进行了验证,然而由于Date类本身是可变了,因此很容易违反这个约束,见如下代码:

public void testPeriod() {

         Date start = new Date();

         Date end = new Date();

         Period p = new Period(start,end);

         end.setYear(78);  //该修改将直接影响Period内部的end对象。

     }

 为了避免这样的攻击,我们需要对Period的构造函数进行相应的修改,即对每个可变参数进行保护性拷贝。

public Period(Date start,Date end) {

         this.start = new Date(start.getTime());

         this.end = new Date(end.getTime());

         if (start.compareTo(end) > 0) {

             throw new IllegalArgumentException(start + "After " + end);

     }

 需要说明的是,保护性拷贝是在坚持参数有效性之前进行的,并且有效性检查是针对拷贝之后的对象,而不是针对原始对象的。这主要是为了避免在this.start = new Date(start.getTime())到if (start.compareTo(end) > 0)这个时间窗口内,参数start和end可能会被其他线程修改。

      现在构造函数已经安全了,后面我们需要用同样的方式继续修改另外两个对象访问函数。

public Date start() {

         return new Date(start.getTime());

     }

     public Date end() {

         return new Date(end.getTime());

     }

经过这一番修改之后,Period成为了不可变类,其内部的“周期的起始时间不能落后于结束时间”约束条件也不会再被破坏。

      参数的保护性拷贝并不仅仅针对不可变类。每当编写方法或者构造器时,如果它要允许客户提供的对象进入到内部数据结构中,则有必要考虑一下,客户提供的对象进入到内部数据结构中,则有必要考虑一下,客户提供的对象是否有可能是可变的。如果是,就要考虑你的类是否能够容忍对象进入数据结构之后发生变化。如果答案是否定的,就必须对该对象进行保护性拷贝,并且让拷贝之后的对象而不是原始对象进入到数据结构中。例如,如果你正在考虑使用有客户提供的对象引用作为内部Set实例的元素,或者作为内部Map实例的键(Key),就应该意识到,如果这个对象在插入之后再被修改,Set或者Map的约束条件就会遭到破坏。

    
四十一、谨慎重载:

 

      见下面一个函数重载的例子:

public class CollectionClassfier {

         public static String classify(Set<?> s) {

             return "Set";

         }

         public static String classify(List<?> l) {

             return "List";

         }

         public static String classify(Collection<?> c) {

             return "Unknown collection";

         }

         public static void main(String[] args) {

             Collection<?>[] collections = {

                 new HashSet<String>(),

                 new ArrayList<BigInteger>(),

                 new HashMap<String,String>().values()

             };

             for (Collection<?> c : collections)

                 System.out.println(classify(c));

         }

     }

这里你可能会期望程序打印出

      //Set

      //List

      //Unknown Collection

      然而实际上却不是这样,输出的结果是3个"Unknown Collection"。为什么会是这样呢?因为函数重载后,需要调用哪个函数是在编译期决定的,这不同于多态的运行时动态绑定。针对此种情形,该条目给出了一个修正的方法,如下:

public static String classify(Collection<?> c) {

         return c instanceof Set ? "Set" : c instanceof List

             ? "List" : "Unknown Collection";

     }

 和override不同,重载机制不会像override那样规范,并且每次都能得到期望的结果。因此在使用时需要非常谨慎,否则一旦出了问题,就会需要更多的时间去调试。该条目给出以下几种尽量不要使用重载的情形:

      1.    函数的参数中包含可变参数;

      2.    当函数参数数目相同时,你无法准确的确定哪一个方法该被调用时;

      3.    在Java 1.5 之后,需要对自动装箱机制保持警惕。

      我们先简单说一下第二种情形。比如两个重载函数均有一个参数,其中一个是整型,另一个是Collection<?>,对于这种情况,int和Collection<?>之间没有任何关联,也无法在两者之间做任何的类型转换,否则将会抛出ClassCastException的异常,因此对于这种函数重载,我们是可以准确确定的。反之,如果两个参数分别是int和short,他们之间的差异就不是这么明显。

      对于第三种情形,该条目给出了一个非常典型的用例代码,如下:

public class SetList {

         public static void main(String[] args) {

             Set<Integer> s = new TreeSet<Integer>();

             List<Integer> l = new ArrayList<Integer>();

             for (int i = -3; i < 3; ++i) {

                 s.add(i);

                 l.add(i);

             }

             for (int i = 0; i < 3; ++i) {

                 s.remove(i);

                 l.remove(i);

             }

             System.out.println(s + " " + l);

         }

     }

  在执行该段代码前,我们期望的结果是Set和List集合中大于等于的元素均被移除出容器,然而在执行后却发现事实并非如此,其结果为:

      [-3,-2,-1] [-2,0,2]

      这个结果和我们的期望还是有很大差异的,为什么Set中的元素是正确的,而List则不是,是什么导致了这一结果的发生呢?下面给出具体的解释:

      1. s.remove(i)调用的是Set中的remove(E),这里的E表示Integer,Java的编译器会将i自动装箱到Integer中,因此我们得到了想要的结果。

      2. l.remove(i)实际调用的是List中的remove(int index)重载方法,而该方法的行为是删除集合中指定索引的元素。这里分别对应第0个,第1个和第2个。

      为了解决这个问题,我们需要让List明确的知道,我们需要调用的是remove(E)重载函数,而不是其他的,这样我们就需要对原有代码进行如下的修改:

public class SetList {

         public static void main(String[] args) {

             Set<Integer> s = new TreeSet<Integer>();

             List<Integer> l = new ArrayList<Integer>();

             for (int i = -3; i < 3; ++i) {

                 s.add(i);

                 l.add(i);

             }

             for (int i = 0; i < 3; ++i) {

                 s.remove(i);

                 l.remove((Integer)i); //or remove(Integer.valueOf(i));

             }

             System.out.println(s + " " + l);

         }

     }

 该条目还介绍了一种实现函数重载,同时又尽可能避免上述错误发生的方式。即其中的一个重载函数,在其内部通过一定的转换逻辑转换之后,再通过转换后的参数类型调用其他的重载函数,从而确保即便使用者在使用过程中出现重载误用的情况,也因两者可以得到相同的结果而规避了潜在错误的发生。

 
四十二、慎用可变参数:

 

      可变参数方法接受0个或者多个指定类型的参数。可变参数机制通过先创建一个数组,数组的大小为在调用位置所传递的参数数量,然后将参数值传到数组中,最后将数组传递给方法,如:

static int sum(int...args) {

         int sum = 0;

         for (int arg : args)

             sum += arg;

         retrun sum;

     }

上面的方法可以正常的工作,但是在有的时候,我们可能需要至少一个或者多个某种类型参数的方法,如:

static int min(int...args) {

         if (args.length == 0)

             throw new IllegalArgumentException("Too few arguments.");

         int min = args[0];

         for (int i = 0; i < args.length; ++i) {

             if (args[i] < min)

                 min = args[i];

         }

         return min;

     }

  对于上面的代码主要存在两个问题,一是如果调用者没有传递参数是,该函数将会在运行时抛出异常,而不是在编译期报错。另一个问题是这样的写法也是非常不美观的,函数内部必须做参数的数量验证,不仅如此,这也影响了效率。将编译期可以完成的事情推到了运行期。下面提供了一种较好的修改方式,如下:

static int min(int firstArg,int...remainingArgs) {

         int min = firstArgs;

         for (int arg : remainingArgs) {

             if (arg < min)

                 min = arg;

         }

         return min;

     }

   由此可见,当你真正需要让一个方法带有不定数量的参数时,可变参数就非常有效。

      有的时候在重视性能的情况下,使用可变参数机制要特别小心。可变参数方法的每次调用都会导致进行一次数组分配和初始化。如果确定确实无法承受这一成本,但又需要可变参数的灵活性,还有一种模式可以弥补这一不足。假设确定对某个方法95%的调用会有3个或者更少的参数,就声明该方法的5个重载,每个重载方法带有0个至3个普通参数,当参数的数目超过3个时,就使用一个可变参数方法:

public void foo() {}

     public void foo(int a1) {}

     public void foo(int a1,int a2) {}

     public void foo(int a1,int a2,int a3) {}

     public void foo(int a1,int a2,int a3,int...rest) {}

  所有调用中只有5%参数数量超过3个的调用需要创建数组。就像大多数的性能优化一样,这种方法通常不恰当,但是一旦真正需要它时,还是非常有用处的。

    
四十三、返回零长度的数组或者集合,而不是null:

      见如下代码:

public class CheesesShop {

         private final List<Cheese> cheesesInStock = new List<Cheese>();

         public Cheese[] getCheeses() {

             if (cheesesInStock.size() == 0)

                 return null;

             return cheeseInStock.toArray(null);

         }

     }

     从以上代码可以看出,当没有Cheese的时候,getCheeses()函数返回一种特例情况null。这样做的结果会使所有的调用代码在使用前均需对返回值数组做null的判断,如下:

public void testGetCheeses(CheesesShop shop) {

         Cheese[] cheeses = shop.getCheeses();

         if (cheese != null && Array.asList(cheeses).contains(Cheese.STILTON))

             System.out.println("Jolly good, just the thing.");

     }

 对于一个返回null而不是零长度数组或者集合的方法,几乎每次用到该方法时都需要这种曲折的处理方式。很显然,这样是比较容易出错的。如果我们使getCheeses()函数在没有Cheese的时候不再返回null,而是返回一个零长度的数组,那么我的调用代码将会变得更加简洁,如下:

public void testGetCheeses2(CheesesShop shop) {

         if (Array.asList(shop.getCheeses()).contains(Cheese.STILTON))

             System.out.println("Jolly good, just the thing.");

     }

    相比于数组,集合亦是如此。
四十四、为所有到处的API元素编写文档注释

如果想要一个API真正可用,就必须为其编写文档。javadoc利用特殊格式的稳定注释(documentation comment),根据源代码自动产生API文档。

详细的规范可以参考:Sun的 How to Write Doc Comments

 

为了正确地编写API文档,必须在每个被到处的类,接口,构造器,方法和域声明之前增加一个文档注释。如果类是序列化的,也应该对它的序列化形式编写文档。

 

注意事项:

1. @param、@return、@throws标签后面的短语或者子句都不用句点来结束。

2. 使用html标签会被转换成HTML

3. 使用代码片段放在{@code}中

4. 特殊字符文档,比如小于号,放在{@literal}中

5. 文档第一句话成注释所属元素的概要描述,因此要注意句点的使用

6. 方法和构造器,概要最好是完整的动词短语,而类,接口和域,应该是名词短语

7. 关于泛型,枚举和注解(后两者体验不深 )

1. 为泛型或者方法编写文档,确保说明所以的类型参数

2. 枚举,说明常量

3. 注解,确保说明所有成员已经类型本身

简而言之:要为API编写文档,文档注释是最好的最有效的途径。
四十五、将局部变量的作用域最小化:

 

      将局部变量的作用域最小化,可以增强代码的可读性和可维护性,并降低出错的可能性。在C语言中要求局部变量必须在一个代码块的开头处进行声明,出于习惯,有些开发者延续了这样的做法。这个习惯需要改正,Java提供了你在代码块的任何地方声明变量的语法支持。

      "要使局部变量的作用域最小化,最有力的实践就是在第一次使用它的地方声明"。如果过早的声明,开发者就有可能在真正使用该变量的时候忘记了它的类型或者初始值了,而且也会带来代码块内变量名的名字污染问题,由此引发的Bug,往往是令人极为沮丧的。

      "几乎每个局部变量的声明都应该包含一个初始化表达式"。如果你没有足够的信息来满足对一个变量进行有意义的初始化,就应该推迟这个声明,直到可以初始化为止。这条规则有个例外的情况与try-catch语句有关。如果一个变量被一个方法初始化,而这个方法可能会抛出一个异常,该变量就必须在try块内初始化,如果这个变量的值也必须在try块之外被访问,它就必须在try块之前被声明,但是遗憾的是在try块之前,它还不能被"有意义地初始化"。

      循环中提供了特殊的机会将变量的作用域最小化,它们的作用域正好被限定在需要的范围之内。因此,如果在循环终止之后不再需要变量的内容,for循环就优先于while循环,见如下代码片段:

Iterator<Element> i = c.iterator();

     while (i.hasNext()) {

         doSomething(i.next());

     }

     ... ...

     Iterator<Element> i2 = c2.iterator();

     while (i.hasNext()) {  //BUG!

         doSomethingElse(i2.next());

     }

 可以看到在第二个循环的循环条件判断处有一个非常明显的BUG,这极有可能是copy-paste所致。然而该类错误如果出现在for循环里,将直接引发编译期错误。见如下代码片段:   

for (Iterator<Element> i = c.iterator(); i.hasNext(); ) {

         doSomething(i.next());

     }

     ... ...

     for (Iterator<Element> i2 = c2.iterator(); i.hasNext(); ) {

         doSomethingElse(i2.next());

     }

  而且,如果使用for循环,犯这种copy-paste错误的可能性大大降低,因为通常没有必要在两个循环中使用不同的变量名。循环是完全独立的,所以重用元素(或者迭代器)变量的名称不会有任何危害。实际上,这也是很流行的做法。

    
四十六、for-each循环优先于传统的for循环:

 

      for-each循环是在Java 1.5 发行版本之后才支持的,之前只能使用传统的for循环。相比于普通for循环,for-each大大提高了代码可读性,由此也减少了低级BUG出现的几率。见如下代码片段:

enum Suit { CLUB,DIAMOND,HEART,SPADE }

     enum Rank { ACE,DEUCE,THREE,FOUR,FIVE,SIX,SEVEN,EIGHT,NINE,TEN,JACK,QUEEN,KING }

     ... ...

     Collection<Suit> suits = Arrays.asList(Suit.values());

     Collection<Rank> ranks = Arrays.asList(Rank.values());

     List<Card> deck = new ArrayList<Card>();

     for (Iterator<Suit> i = suits.iterator(); i.hasNext(); } {

         for (Iterator<Rank> j = ranks.iterator(); j.hasNext(); )

             deck.add(new Card(i.next(),j.next()); //BUG, j被多次迭代

     }

   上面代码的BUG是比较隐匿的,很多专家级的程序员也会偶尔犯类似的错误。下面我们来一下修复后的代码片段:

... ...

     for (Iterator<Suit> i = suits.iterator(); i.hasNext(); } {

         Suit suit = i.next();

         for (Iterator<Rank> j = ranks.iterator(); j.hasNext(); )

             deck.add(new Card(suit,j.next()); //BUG, j被多次迭代

     }

 我们下面再来看一下用for-each循环来实现该逻辑的代码片段:   

... ...

     for (Suit suit : suits) {

         for (Rank rank : Ranks)

             deck.add(new Card(suit,rank));

     }

 总之,for-each循环的简洁性和预防Bug方面有着传统for循环无法比拟的优势,并且没有性能损失。应该尽可能地使用for-each循环。遗憾的是,有三种常见的情况无法使用for-each循环:

      1. 过滤:如果需要遍历集合,并删除选定的元素,就需要使用显式的迭代器,以便可以调用它的remove方法。

      2. 转换:如果需要遍历列表或数组,并取代它部分或者全部的元素值,就需要列表迭代器或者数组索引,以便设定元素的值。

      3. 并行迭代:如果需要并行的遍历多个集合,就需要显式的控制迭代器或者索引变量,以便所有迭代器或者索引变量都可以得到同步前移。

    
四十八:如果需要精确的答案,请避免使用float和double:

 

      float和double类型主要是为了科学计算和工程计算而设计的。它们执行二进制浮点运算,这是为了在广泛的数值范围上提供较精确的快速近似计算而精心设计的。然而,它们并没有提供完全精确的结果,所以不应该被用于需要精确结果的场合,如货币计算等。

      该条目给出一个例子,如果你手里有1美元,超市货架上有一排糖果,它们的售价分别为10美分、20美分、30美分,以此类推直到1美元。你打算从标价10美分的开始买,每个糖果买1颗,直到不能支付货架上下一中价格的糖果为止,那么你可以买多少糖果?还会找回多少零头呢?见如下代码:

public static void main(String[] args) {

         double funds = 1.00;

         int itemsBought = 0;

         for (double price = .10; funds >= price; price += .10) {

             funds -= price;

             itemsBought++;

         }

         System.out.println(itemsBought + " items bought.");

         System.out.println("Change: $" + funds);

     }

     // 3 items bought.

 // Change: $0.39999999999999

  很显然,如果我们用手工计算的话是不会得到该结果的,造成这一结果的主要原因就是double类型的精度问题。解决该问题的正确办法是使用BigDecimal、int或者long进行货币计算。下面我们看一下该程序用BigDecimal实现的翻版。

public static void main(String[] args) {

         final BigDecimal TEN_CENTS = new BigDecimal(".10");

         int itemsBought = 0;

         BigDecimal funds = new BigDecimal("1.00");

         for (BigDecimal price = TEN_CENTS; funds.compareTo(price) >= 0;price.add(TEN_CENTS)) {

             itemsBought++;

             funds = funds.substract(price);

         }

         System.out.println(itemsBought + " items bought.");

         System.out.println("Money left over: $" + funds);

     }

     // 4 items bought.

 // Money left over: $0.00

   现在我们得到了正确的结果。然而,使用BigDecimal有两个主要缺点:和使用基本运算类型相比,这样做很不方便,而且效率也低。除了该方法之外我们还可以使用int或者long,至于使用哪种具体类型,需要视所涉及的数值大小而定。现在我们需要将计算单位转换为分,而不再是以元为单位,下面是这个例子的又一次翻版。

public static void main(String[] args) {

         int itemsBougth = 0;

         int funds = 100;

         for (int price = 0; funds >= price; price += 10) {

             itemsBought++;

             fund -= price;

         }

         System.out.println(itemsBought + " items bought.");

         System.out.println("Money left over: $" + funds + " cents.");

     }

     // 4 items bought.

 // Money left over: $0.00 cents.

   使用int和long代替BigDecimal之后,该段代码的执行效率大大提升。需要指出的是,如果数值所涉及的范围没有超过9位十进制数字,就可以使用int,没有超过18位可以使用long,一旦超过,则必须使用BigDecimal。

    
四十九、基本类型优先于基本装箱类型:

 

      Java的类型系统中主要包含两个部分,分别是基本类型,如int、double、long,还有就是引用类型,如String、List等。其中每个基本类型都对应着一种引用类型,被称为装箱基本类型,如分别和int、double、long对应的装箱类型Integer、Double和Long等。

      Java在1.5 中新增了自动装箱的和自动拆箱的功能。这些特性仅仅是模糊了基本类型和装箱类型之间的区别,但是并没有完全消除他们之间的差异,而这些差别往往会给我们的程序带来一些潜在的问题。我们先看一下他们之间的主要区别:

      1. 基本类型只有值,在进行比较时可以直接基于值进行比较,而装箱类型在进行同一性比较时和基本类型相比有着不同的逻辑,毕竟他们是对象,是Object的子类,它们需要遵守Java中类对象比较的默认规则。

      2. 基本类型只有功能完备的值,而每个装箱类型除了它对应基本类型的所有功能之外,还有一个非功能值:null。记住,它毕竟是对象。

      3. 基本类型通常比装箱类型更节省时间和空间。

      见如下代码示例:

public class MyTest {

         private static int compare(Integer first,Integer second) {

             return first < second ? -1 : (first == second ? 0 : 1);

         }

         public static void main(String[] args) {

             Integer first = new Integer(42);

             Integer second = new Integer(42);

             System.out.println("Result of compare first and second is " + compare(first,second));

         }

     }

    这段代码看起来非常简单,它的运行结果也非常容易得出,然而当我们真正运行它的时候却发现,实际输出的结果和我们的期望是完全不同的,这是为什么呢?见如下分析:

      1. compare方法中的第一次比较(first < second)将能够正常工作并得到正确的结果,即first < second为false;

      2. 在进行相等性比较的时候问题出现了,如前所述,Integer毕竟是对象,在进行对象之间的同一性比较时它将遵守对象的同一性比较规则,由于这两个参数对象的地址是不同的,因为我们是通过两次不同的new方法构建出的这两个参数对象。结果可想而知,first == second返回false;

      3. 现在最后的输出结果已经很清楚了:Result of compare first and second is 1

      下面我们看一下如何修正以上代码中存在的错误:

public class MyTest {

         private static int compare(Integer first,Integer second) {

             int f = first;

             int s = second;

             return f < s ? -1 : (f == s ? 0 : 1);

         }

         public static void main(String[] args) {

             Integer first = new Integer(42);

             Integer second = new Integer(42);

             System.out.println("Result of compare first and second is " + compare(first,second));

         }

     }

  我们使用两个临时的基本类型变量来代替装箱类型的参数变量,然后再基于基本类型变量进行之前代码中的比较。在运行这段代码之后,我们发现确实得到了期望的结果。

      现在让我们再看一段代码片段:

public class Unbelievable {

         static Integer i;

         public static void main(String[] args) {

             if (i == 42)

                 System.out.println("Unbelievable");

         }

     }

   程序的运行结果并没有打印出"Unbelievable",而是抛出了空指针异常。这是因为装箱类型的i变量并没有被初始化,即它本身为null,当程序计算表达式(i == 42)时,它会将Integer与int进行比较。几乎在任何一种情况下,当在一项操作中混合使用基本类型和装箱基本类型时,装箱类型就会自动拆箱,这种情况无一例外。如果null对象引用被自动拆箱,就会得到一个NullPointerException。修正这一问题也非常简单,只需将i的类型从Integer变为int即可。

      在看一下最后一个代码示例:

public static void main(String[] args) {

         Long sum = 0L;

         for (long i = 0; i < Integer.MAX_VALUE; ++i) {

             sum += i;

         }

         System.out.println(sum);

     }

    这段代码虽然不像之前的两个示例那样有着明显的Bug,然而在运行时却存在着明显的性能问题。因为在执行for循环时,会有不断的自动装箱和自动拆箱的操作发生。修改该代码也是非常容易的,只需将sum的类型从Long变为long即可。

      该条目的最后介绍了在以下两种情况下我们将需要使用装箱基本类型:

      1. 由于Java泛型中的类型参数不能为基本类型,因此在需要使用基本类型作为类型参数时,我们只能将其替换为与之对应的装箱类型。

      2. 在使用反射进行方法调用时。

    
五十一、当心字符串连接的性能:

 

      字符串连接操作(+)是把多个字符串合并为一个字符串的最为便利的途径。因此如果仅仅是对两个较小字符串进行一次连接并输出连接结果,这样是比较合适的。然而如果是为n个字符串而重复地使用字符串连接操作符,则需要n的平方级的时间。这是由于字符串对象本身是不可变的,在连接两个字符串时,需要copy两个连接字符串的内容并形成新的连接后的字符串。见如下代码:

public String statement() {

         String result = "";

         for (int i = 0; i < numItems(); i++) {

             result += lineForItem(i);

         }

         return result;

     }

 

  此时如果项目数量巨大,这个方法的执行时间将难以估量。为了获得可以接受的性能,请使用StringBuilder替代String,见如下修正后的代码:

public String statement() {

         StringBuilder b = new StringBuilder(numItems * LINE_WIDTH);

         for (int i = 0; i < numItems(); i++)

             b.append(lineForItem(i));

         return b.toString();

     }

上述两种做法在性能上的差异是巨大的,如果numItems()返回100,而lineForItem返回一个固定长度为80的字符串,后者将比前者块85倍。由于第一种做法的开销是随项目数量呈平方级增加,而第二种做法是线性增加的,所以数目越大,差异越大。
五十二、通过接口引用对象:

 

      一般来讲,在函数参数、返回值、域变量等声明中,应该尽量使用接口而不是类作为它们的类型。只有当你利用构造器创建某个对象的时候,才真正需要引用这个对象的类,如:

      List<Subscriber> subscribers = new Vector<Subscriber>();

      而不是像下面这样的声明:

      Vector<Subscriber> subscribers = new Vector<Subscriber>();

      如果你养成了用接口作为类型的习惯,你的程序将更加灵活。对于上面的例子,在今后的改进中,如果不想使用Vector作为实例化对象,我们只需在如下一出进行修改即可:

      List<Subscriber> subscribers = new ArrayList<Subscriber>();

      如果之前该变量的类型不是接口类型,而是它实际类型的本身,那么在做如此修改之前,则需要确认在所有使用该变量的代码行是否用到了Vector的特性,从而导致不行直接进行替换。如果该变量的接口为接口,我们将不受此问题的限制。

      那么在哪些情况下不是使用接口而是使用实际类呢?见如下情况:

      1. 没有合适的接口存在,如String和BigInteger等值对象,通常它们都是final的,也没有提供任何接口。

      2. 对象属于一个框架,而框架的基本类型是类,不是接口。如果对象属于这种基于类的框架,就应使用基类来引用该对象,如TimerTask。

      3. 类实现了接口,但是它提供了接口中不存在的额外方法。如果程序此时依赖于这些额外的方法,这种类就应该只被用来引用他的实例。

      简而言之,如果类实现了接口,就应该尽量使用其接口引用该类的引用对象,这样可以使程序更加灵活,如果不是,则使用类层次结构中提供了必要功能的最基础的类。

    
五十三、接口优先于反射机制:

 

      Java中提供了反射的机制,如给定一个Class实例,你可以获取Constructor、Method和Field等实例,分别代表了该Class实例所表示的类的Constructor(构造器)、Method(方法)和Field(域)。与此同时,这些实例可以使你通过反射机制操作它们的底层对等体。然后这种灵活是需要付出一定代价的,如下:

      1. 丧失了编译时类型检查的好处,包括异常检查和类型检查等。

      2. 执行反射访问所需要的代码往往非常笨拙和冗长,阅读起来也非常困难,通常而言,一个基于普通方式的函数调用大约1,2行,而基于反射方式,则可能需要十几行。

      3. 性能损失,反射方法的调用比普通方法调用慢了许多。

      核心反射机制最初是为了基于组件的应用创建工具而设计的。它们通常需要动态装载类,并且用反射功能找出它们支持哪些方法和构造器,如类浏览器、对象监视器、代码分析工具、解释性的嵌入式系统等。

      在通常情况下,如果只是以非常有限的形式使用反射机制,虽然也要付出少许代价,但是可以获得许多好处。对于有些程序,它们必须用到编译时无法获取的类,但是在编译时却存在适当的接口或超类,通过它们可以引用这个类。如果是这样,可以先通过反射创建实例,然后再通过它们的接口或超类,以正常的方式访问这些实例。见如下代码片段:

public static void main(String[] args) {

         Class<?> cl = null;

         try {

             c1 = Class.forName(args[0]);

         } catch (ClassNotFoundException e) {

             System.err.println("Class not found.");

             System.exit(1);

         }

         Set<String> s = null;

         try {

             s = (Set<String>)c1.newInstance();

         } catch (IllegalAccessException e) {

             System.err.println("Class not accessible");

             System.exit(1);

         } catch (InstantiationException e) {

             System.err.println("Class not instantiation.");

             System.exit(1);

         }

         s.addAll(Arrays.asList(args).subList(1,args.length));

         System.out.println(s);

     }

 

   上面的代码中体现出了反射的两个缺点:

      1. 这个例子有3个运行时异常的错误,如果不使用反射方式实例化,这3个错误都会成为编译时错误。

      2. 根据类名生成它的实例需要20行冗长的代码,而调用构造器可以非常简洁的只使用一行代码。

      简而言之,反射机制是一种功能强大的机制,对于特定的复杂系统编程任务,它是非常必要的。如果你编写的程序必须要与编译时未知的类一起工作,如有可能,就应该仅仅使用反射机制来实例化对象,而访问对象时则使用编译时已知的某个接口或者超类。

    
五十四、谨慎地使用本地方法:

 

      JNI允许Java应用程序可以调用本地方法,所谓本地方法是指用本地程序设计语言,如C/C++来编写的特殊方法。本地方法在本地语言中可以执行任意的计算任务,并最终返回Java程序。它的主要用途就是访问一些本地的资源,如注册表、文件锁等,或者是访问遗留代码中的一些遗留数据。当然通过本地方法在有些应用场景中是可以大大提高提高系统执行效率的。

      随着Java平台的不断成熟,它提供了越来越多以前只有宿主平台才有的特性,如java.util.prefs和java.awt.SystemTray等。与此同时,随着JVM的不断优化,其效率也在不断的提高,因此只有在很少的情况下才会考虑使用JNI。还需要指出的是,JNI中胶合Java和C++的代码部分非常冗长且难以理解。

五十四:谨慎地使用本地方法

1. 指使用本地程序设计语言编写的特殊方法

2. jvm越来越快,本地方法提供性能不值得

3. 本地语言不安全,平台相关,不方便移植

4. 尽可能少使用,必要时候才使用,而且极少数情况会用到,并全面测试
五十五:谨慎第进行优化

不要费力去编写快速的程序,应该努力编写好的程序,速度随之而来
五十六:遵守普遍接受的命名惯例

 

如果长期养成的习惯与此不同,不要盲目遵从这些命名规范,可以运用常识
五十七、只针对异常情况才使用异常:

 

      不知道你否则遇见过下面的代码:

try {

         int i = 0;

         while (true)

             range[i++].climb();

     } catch (ArrayIndexOutOfBoundsException e) {

     }

 这段代码的意图不是很明显,其本意就是遍历变量数组range中的每一个元素,并执行元素的climb方法,当下标超出range的数组长度时,将会直接抛出ArrayIndexOutOfBoundsException异常,catch代码块将会捕获到该异常,但是未作任何处理,只是将该错误视为正常工作流程的一部分来看待。这样的写法确实给人一种匪夷所思的感觉,让我们再来看一下修改后的写法:

for (Mountain m : range) {

         m.climb();

     }

   和之前的写法相比其可读性不言而喻。那么为什么又有人会用第一种写法呢?显然他们是被误导了,他们企图避免for-each循环中JVM对每次数组访问都要进行的越界检查。这无疑是多余的,甚至适得其反,因为将代码放在try-catch块中反而阻止了JVM的某些特定优化,至于数组的边界检查,现在很多JVM实现都会将他们优化掉了。在实际的测试中,我们会发现采用异常的方式其运行效率要比正常的方式慢很多。

      除了刚刚提到的效率和代码可读性问题,第一种写法还会掩盖一些潜在的Bug,假设数组元素的climb方法中也会访问某一数组,并且在访问的过程中出现了数组越界的问题,基于该错误,JVM将会抛出ArrayIndexOutOfBoundsException异常,不幸的是,该异常将会被climb函数之外catch语句捕获,在未做任何处理之后,就按照正常流程继续执行了,这样Bug也就此被隐藏起来。

      这个例子的教训很简单:"异常应该只用于异常的情况下,它们永远不应该用于正常的控制流"。虽然有的时候有人会说这种怪异的写法可以带来性能上的提升,即便如此,随着平台实现的不断改进,这种异常模式的性能优势也不可能一直保持。然而,这种过度聪明的模式带来的微妙的Bug,以及维护的痛苦却依然存在。

      根据这条原则,我们在设计API的时候也是会有所启发的。设计良好的API不应该强迫它的客户端为了正常的控制流而使用异常。如Iterator,JDK在设计时充分考虑到这一点,客户端在执行next方法之前,需要先调用hasNext方法已确认是否还有可读的集合元素,见如下代码:

for (Iterator<Foo> i = collection.iterator(); i.hasNext(); ) {

         Foo f = i.next();

     }

如果Iterator缺少hasNext方法,客户端则将被迫改为下面的写法:

try {

         Iterator<Foo> i = collection.iterator();

         while (true)

             Foo f = i.next();

     } catch (NoSuchElementException e) {

     }

 这应该非常类似于本条目开始时给出的遍历数组的例子。在实际的设计中,还有另外一种方式,即验证可识别的错误返回值,然而该方式并不适合于此例,因为对于next,返回null可能是合法的。那么这两种设计方式在实际应用中有哪些区别呢?

      1. 如果是缺少同步的并发访问,或者可被外界改变状态,使用可识别返回值的方法是非常必要的,因为在测试状态(hasNext)和对应的调用(next)之间存在一个时间窗口,在该窗口中,对象可能会发生状态的变化。因此,在该种情况下应选择返回可识别的错误返回值的方式。

      2. 如果状态测试方法(hasNext)和相应的调用方法(next)使用的是相同的代码,出于性能上的考虑,没有必要重复两次相同的工作,此时应该选择返回可识别的错误返回值的方式。

      3. 对于其他情形则应该尽可能考虑"状态测试"的设计方式,因为它可以带来更好的可读性。

    
五十八、对可恢复的情况使用受检异常,对编程错误使用运行时异常:

 

      Java中提供了三种可抛出结构:受检异常、运行时异常和错误。该条目针对这三种类型适用的场景给出了一般性原则。

      1. 如果期望调用者能够适当地恢复,对于这种情况就应该使用受检异常,如某人打算网上购物,结果余额不足,此时可以抛出自定义的受检异常。通过抛出受检异常,将强迫调用者在catch子句中处理该异常,或继续向上传播。因此,在方法中声明受检异常,是对API用户的一种潜在提示。

      2. 用运行时异常来表明编程错误。大多数的运行时异常都表示"前提违例",即API的使用者没有遵守API设计者建立的使用约定。如数组访问越界等问题。

      3. 对于错误而言,通常是被JVM保留用于表示资源不足、约束失败,或者其他使程序无法继续执行的条件。

      针对自定义的受检异常,该条目还给出一个非常实用的技巧,当调用者捕获到该异常时,可以通过调用该自定义异常提供的接口方法,获取更为具体的错误信息,如当前余额等信息。

五十九、避免不必要的使用受检异常:

    

      受检异常是Java提供的一个很好的特征。与返回值不同,它们强迫程序员必须处理异常的条件,从而大大增强了程序的可靠性。然而,如果过分使用受检异常则会使API在使用时非常不方便,毕竟我们还是需要用一些额外的代码来处理这些抛出的异常,倘若在一个函数中,它所调用的五个API都会抛出异常,那么编写这样的函数代码将会是一项令人沮丧的工作。

      如果正确的使用API不能阻止这种异常条件的产生,并且一旦产生异常,使用API的程序员可以立即采用有用的动作,这种负担就被认为是正当的。除非这两个条件都成立,否则更适合使用未受检异常,见如下测试:

try {

         dosomething();

     } catch (TheCheckedException e) {

         throw new AssertionError();

     }

     

     try {

         donsomething();

     } catch (TheCheckedException e) {

         e.printStackTrace();

         System.exit(1);

     }

   当我们使用受检异常时,如果在catch子句中对异常的处理方式仅仅如以上两个示例,或者还不如它们的话,那么建议你考虑使用未受检异常。原因很简单,它们在catch子句中,没有做出任何用于恢复异常的动作。

    
六十、优先使用标准异常:

 

      使用标准异常,不仅可以更好的复用已有的代码,同时也使你设计的API更加容易学习和使用,因为它和程序员已经熟悉的习惯用法更为一致。另外一个优势是,代码的可读性更好,程序员在阅读时不会出现更多的不熟悉的代码。该条目给出了一些非常常用且容易被复用的异常,见下表:

      异常                                               应用场合

      IllegalArgumentException              非null的参数值不正确。

      IllegalStateException                     对于方法调用而言,对象状态不合适。

      NullPointerException                     在禁止使用null的情况下参数值为null。

      IndexOutOfBoundsException         下标参数值越界

      ConcurrentModificationException   在禁止并发修改的情况下,检测到对象的并发修改。

      UnsupportedOperationException    对象不支持用户请求的方法。

      当然在Java中还存在很多其他的异常,如ArithmeticException、NumberFormatException等,这些异常均有各自的应用场合,然而需要说明的是,这些异常的应用场合在有的时候界限不是非常分明,至于该选择哪个比较合适,则更多的需要依赖上下文环境去判断。

      最后需要强调的是,一定要确保抛出异常的条件和该异常文档中描述的条件保持一致。

    
六十一、抛出与抽象相对应的异常:

 

      如果方法抛出的异常与它所执行的任务没有明显的关系,这种情形将会使人不知所措。特别是当异常从底层开始抛出时,如果在中间层没有做任何处理,这样底层的实现细节将会直接污染高层的API接口。为了解决这样的问题,我们通常会做出如下处理:

try {

         doLowerLeverThings();

     } catch (LowerLevelException e) {

         throw new HigherLevelException(...);

     }

这种处理方式被称为异常转译。事实上,在Java中还提供了一种更为方便的转译形式--异常链。试想一下上面的示例代码,在调试阶段,如果高层应用逻辑可以获悉到底层实际产生异常的原因,那么对找到问题的根源将会是非常有帮助的,见如下代码:

try {

         doLowerLevelThings();

     } catch (LowerLevelException cause) {

         throw new HigherLevelException(cause);

     }

  底层异常作为参数传递给了高层异常,对于大多数标准异常都支持异常链的构造器,如果没有,可以利用Throwable的initCause方法设置原因。异常链不仅让你可以通过接口函数getCause访问原因,它还可以将原因的堆栈轨迹集成到更高层的异常中。

      通过这种异常链的方式,可以非常有效的将底层实现细节与高层应用逻辑彻底分离出来。    

    
六十三、在细节中包含能捕获失败的信息:

 

    当程序由于未被捕获的异常而失败的时候,系统会自动地打印出该异常的堆栈轨迹。在堆栈轨迹中包含该异常的字符串表示法,即toString方法的返回结果。如果我们在此时为该异常提供了详细的出错信息,那么对于错误定位和追根溯源都是极其有意义的。比如,我们将抛出异常的函数的输入参数和函数所在类的域字段值等信息格式化后,再打包传递给待抛出的异常对象。假设我们的高层应用捕捉到IndexOutOfBoundsException异常,如果此时该异常对象能够携带数组的下界和上界,以及当前越界的下标值等信息,在看到这些信息后,我们就能很快做出正确的判断并修订该Bug。

    特别是对于受检异常,如果抛出的异常类型还能提供一些额外的接口方法用于获取导致错误的数据或信息,这对于捕获异常的调用函数进行错误恢复是非常重要的。

    
六十四、努力使失败保持原子性:

 

      这是一个非常重要的建议,因为在实际开发中当你是接口的开发者时,经常会忽视他,认为不保证的话估计也没有问题。相反,如果你是接口的使用者,也同样会忽略他,会认为这个是接口实现者理所应当完成的事情。

      当对象抛出异常之后,通常我们期望这个对象仍然保持在一种定义良好的可用状态之中,即使失败是发生在执行某个操作的过程中间。对于受检异常而言,这尤为重要,因为调用者希望能从这种异常中进行恢复。一般而言,失败的方法调用应该使对象保持在被调用之前的状态。具有这种属性的方法被称为具有"失败原子性"。

      有以下几种途径可以保持这种原子性。

      1. 最简单的方法是设计不可变对象。因为失败的操作只会导致新对象的创建失败,而不会影响已有的对象。

      2. 对于可变对象,一般方法是在操作该对象之前先进行参数的有效性验证,这可以使对象在被修改之前,抛出更为有意义的异常,如:

public Object pop() {

         if (size == 0)

             throw new EmptyStackException();

         Object result = elements[--size];

         elements[size] = null;

         return result;

     }

   如果没有在操作之前验证size,elements的数组也会抛出异常,但是由于size的值已经发生了变化,之后再继续使用该对象时将永远无法恢复到正常状态了。

      3. 预先写好恢复性代码,在出现错误时执行带段代码,由于此方法在代码编写和代码维护的过程中,均会带来很大的维护开销,再加之效率相对较低,因此很少会使用该方法。

      4. 为该对象创建一个临时的copy,一旦操作过程中出现异常,就用该复制对象重新初始化当前的对象的状态。

      虽然在一般情况下都希望实现失败原子性,然而在有些情况下却是难以做到的,如两个线程同时修改一个可变对象,在没有很好同步的情况下,一旦抛出ConcurrentModificationException异常之后,就很难在恢复到原有状态了。

    
六十五、不要忽略异常:

 

      这是一个显而易见的常识,但是经常会被违反,因此该条目重新提出了它,如:

try {

         dosomething();

     } catch (SomeException e) {

     }

 可预见的、可以使用忽略异常的情形是在关闭FileInputStream的时候,因为此时数据已经读取完毕。即便如此,如果在捕获到该异常时输出一条提示信息,这对于挖出一些潜在的问题也是非常有帮助的。否则一些潜在的问题将会一直隐藏下去,直到某一时刻突然爆发,以致造成难以弥补的后果。

      该条目中的建议同样适用于受检异常和未受检的异常。
六十六、同步访问共享的可变数据:

 

      在Java中很多时候都是通过synchronized关键字来实现共享对象之间的同步的。事实上,对象同步并不仅限于当多个线程操作同一可变对象时,仍然能够保证该共享对象的状态始终保持一致。与此同时,他还可以保证进入同步方法或者同步代码块的每个线程,都看到由同一个锁保护的之前所有的修改效果。

      Java的语言规范保证了读写一个变量是原子的,除非这个变量的类型为long或double。换句话说,读取一个非long或double类型的变量,可以保证返回的值是某个线程保存在该变量中的,即时多个线程在没有同步的情况下并发地修改这个变量也是如此。然而需要特别指出的是,这样的做法是非常危险的。即便这样做不会带来数据同步修改的问题,但是他会导致另外一个更为隐匿的错误发生。见如下代码:

public class StopThread {

         private static boolean stopRequested = false;

         public static void main(String[] args) throw InterruptedException {

             Thread bgThread = new Thread(new Runnable() {

                 public void run() {

                     int i = 0;

                     while (!stopRequested)

                         i++;

                 }

             });

             bgThread.start();

             TimeUnit.SECONDS.sleep(1);

             stopRequested = true;

         }

     }

   对于上面的代码片段,有些人会认为在主函数sleep一秒后,工作者线程的循环状态标志(stopRequested)就会被修改,从而致使工作者线程正常退出。然而事实却并非如此,因为Java的规范中并没有保证在非同步状态下,一个线程修改的变量,在另一个线程中就会立即可见。事实上,这也是Java针对内存模型进行优化的一个技巧。为了把事情描述清楚,我们可以将上面代码中run方法的代码模拟为优化后的代码,见如下修改后的run方法:

public void run() {

         int i = 0;

         if (!stopRequested) {

             while (true)

                 i++;

         }

     }

   这种优化被称为提升,正是HotSpot Server VM的工作。

      要解决这个问题并不难,只需在读取和写入stopRequested的时候加入synchronized关键字即可,见如下代码:

public class StopThread {

         private static boolean stopRequested = false;

         private static synchronized void requestStop() {

             stopRequested = true;

         }

         private static synchronized boolean stopRequested() {

             return stopRequested;

         }

         public static void main(String[] args) throw InterruptedException {

             Thread bgThread = new Thread(new Runnable() {

                 public void run() {

                     int i = 0;

                     while (!stopRequested())

                         i++;

                 }

             });

             bgThread.start();

             TimeUnit.SECONDS.sleep(1);

             requestStop();

         }

     }

   在上面的修改代码中,读写该变量的函数均被加以同步。

      事实上,Java中还提供了另外一种方式用于处理该类问题,即volatile关键字。该单词的直译为“易变的”,引申到这里就是告诉cpu该变量是容易被改变的变量,不能每次都从当前线程的内存模型中获取该变量的值,而是必须从主存中获取,这种做法所带来的唯一负面影响就是效率的折损,但是相比于synchronized关键字,其效率优势还是非常明显的。见如下代码:

public class StopThread {

         private static volatile boolean stopRequested = false;

         public static void main(String[] args) throw InterruptedException {

             Thread bgThread = new Thread(new Runnable() {

                 public void run() {

                     int i = 0;

                     while (!stopRequested)

                         i++;

                 }

             });

             bgThread.start();

             TimeUnit.SECONDS.sleep(1);

             stopRequested = true;

         }

     }

  和第一个代码片段相比,这里只是在stopRequested域变量声明之前加上volatile关键字,从而保证该变量为易变变量。然而需要说明的是,该关键字并不能完全取代synchronized同步方式,见如下代码:

public class Test {

         private static volatile int nextID = 0;

         public static int generateNextID() {

             return nextID++;

         }

     }

   generateNextID方法的用意为每次都给调用者生成不同的ID值,遗憾的是,最终结果并不是我们期望的那样,当多个线程调用该方法时,极有可能出现重复的ID值。这是因为++运算符并不是原子操作,而是由两个指令构成,首先是读取该值,加一之后再重新赋值。由此可见,这两个指令之间的时间窗口极有可能造成数据的不一致。如果要修复该问题,我们可以使用JDK(1.5 later)中java.util.concurrent.atomic包提供的AtomicLong类,使用该类性能要明显好于synchronized的同步方式,见如下修复后的代码:

public class Test {

         private static final AtomicLong nextID = new AtomicLong();

         public static long generateNextID() {

             return nextID.getAndIncrement();

         }

     }
六十七、避免过度同步:

 

      过度同步所导致的最明显问题就是性能下降,特别是在如今的多核时代,再有就是可能引发的死锁和一系列不确定性的问题。当同步函数或同步代码块内调用了外来方法,如可被子类覆盖的方法,或外部类的接口方法等。由于这些方法的行为存在一定的未知性,如果在同步块内调用了类似的方法,将极有可能给当前的同步带来未知的破坏性。见如下代码:

public class ObservableSet<E> extends ForwardingSet<E> {

         public ObservableSet(Set<E> set) {

             super(set);

         }

         private final List<SetObserver<E>> observers = new ArrayList<SetObserver<E>>();

         public void addObserver(SetObserver<E> observer) {

             synchronized(observers) {

                 observers.add(observer);

             }

         }

         public boolean removeObserver(SetObserver<E> observer) {

             synchronized(observers) {

                 return observers.remover(observer);

             }

         }

         private void notifyElementAdded(E element) {

             synchronized(observers) {

                 for (SetObserver<E> observer : observers)

                     observer.added(this,element);

             }

         }

         @Override public boolean add(E element) {

             boolean added = super.add(element);

             if (added)

                 notifyElementAdded(element);

             return added;

         }

         @Override public boolean addAll(Collection<? extends E> c) {

             boolean result = false;

             for (E element : c)

                 result |= add(element);

             return result;

         }

     }

   下面的代码片段是回调接口和测试调用:

public interface SetObserver<E> {

         void added(ObservableSet<E> set,E element);

     }

     

     public static void main(String[] args) {

         ObservableSet<Integer> set = new ObservableSet<Integer>(new HashSet<Integer>());

         set.addObserver(new SetObserver<Integer>() {

             public void added(ObservableSet<Integer> s, Integer e) {

                 System.out.println(e);

             }

         });

         for (int i = 0; i < 100; i++)

             set.add(i);

     }

  对于这个测试用例,他完全没有问题,可以保证得到正确的输出,即打印出0-99的数字。

      现在我们换一个观察者接口的实现方式,见如下代码片段:

set.addObserver(new SetObserver<Integer>() {

         public void added(ObservableSet<Integer> s,Integer e) {

             System.out.println(e);

             if (e == 23)

                 s.removeObserver(this);

         }

     });

 对于以上代码,当执行s.removeObserver(this)的时候,将会抛出ConcurrentModificationException异常,因为在notifyElementAdded方法中正在遍历该集合。对于该段代码,我只能说我们是幸运的,错误被及时抛出并迅速定位,这是因为我们的调用是在同一个线程内完成的,而Java中synchronized关键字构成的锁是可重入的,或者说是可递归的,即在同一个线程内可多次调用且不会被阻塞。如果恰恰相反,我们的冲突调用来自于多个线程,那么将会形成死锁。在多线程的应用程序中,死锁是一种比较难以重现和定位的错误。为了解决上述问题,我们需要做的一是将调用外部代码的部分移出同步代码块,再有就是针对该遍历,我们需要提前copy出来一份,并基于该对象进行遍历,从而避免了上面的并发访问冲突,如:

private void notifyElementAdded(E element) {

         List<SetObserver<E>> snapshot = null;

         synchronized(observers) {

             snapshot = new ArrayList<SetObserver<E>>(observers);

         }

         for (SetObserver<E> Observer : snapshot)

             Observer.added(this,element);

     }

    减少不必要的代码同步还可以大大提高程序的并发执行效率,一个非常明显的例子就是StringBuffer,该类在JDK的早期版本中即以出现,是数据操作同步类,即时我们是以单线程方式调用该类的方法,也不得不承受块同步带来的额外开销。Java在1.5中提供了非同步版本的StringBuilder类,这样在单线程应用中可以消除因同步而带来的额外开销,对于多线程程序,可以继续选择StringBuffer,或者在自己认为需要同步的代码部分加同步块。

    
六十八、executor和task优先于线程:

 

      在Java 1.5 中提供了java.util.concurrent包,在这个包中包含了Executor Framework框架,这是一个很灵活的基于接口的任务执行工具。该框架提供了非常方便的调用方式和强大的功能,如:

      ExecutorService executor = Executors.newSingleThreadExecutor();  //创建一个单线程执行器对象。

      executor.execute(runnable);  //提交一个待执行的任务。

      executor.shutdown();  //使执行器优雅的终止。

      事实上,Executors对象还提供了更多的工厂方法,如适用于小型服务器的Executors.newCachedThreadPool()工厂方法,该方法创建的执行器实现类对于小型服务器来说还是比较有优势的,因为在其内部实现中并没有提供任务队列,而是直接将任务提交给当前可用的线程,如果此时没有可用的线程了,则创建一个新线程来执行该任务。因此在任务数量较多的大型服务器上,由于该机制创建了大量的工作者线程,这将会导致系统的整体运行效率下降。对于该种情况,Executors提供了另外一个工厂方法Executors.newFixedThreadPool(),该方法创建的执行器实现类的内部提供了任务队列,用于任务缓冲。

      相比于java.util.Timer,该框架也提供了一个更为高效的执行器实现类,通过工厂方法Executors.ScheduledThreadPool()可以创建该类。它提供了更多的内部执行线程,这样在执行耗时任务是,其定时精度要优于Timer类。

 

 
六十九、并发工具优先于wait和notify:

      java.util.concurrent中更高级的工具分成三类:Executor Framework、并发集合(Concurrent Collection)以及同步器(Synchronizer)。相比于java.util中提供的集合类,java.util.concurrent中提供的并发集合就有更好的并发性,其性能通常数倍于普通集合,如ConcurrentHashMap等。换句话说,除非有极其特殊的原因存在,否则在并发的情况下,一定要优先选择ConcurrentHashMap,而不是Collections.syschronizedmap或者Hashtable。

      java.util.concurrent包中还提供了阻塞队列,该队列极大的简化了生产者线程和消费者线程模型的编码工作。

      对于同步器,concurrent包中给出了四种主要的同步器对象:CountDownLatch、Semaphore、CyclicBarrier和Exchanger。这里前两种比较常用。在该条目中我们只是简单介绍一个CountDownLatch的优势,该类允许一个或者多个线程等待一个或者多个线程来做某些事情。CountDownLatch的唯一构造函数带有一个int类型的参数 ,这个int参数是指允许所有在等待的线程被处理之前,必须在锁存器上调用countDown方法的次数。

      现在我们给出一个简单应用场景,然后再给出用CountDownLatch实现该场景的实际代码。场景描述如下:

      假设想要构建一个简单的框架,用来给一个动作的并发执行定时。这个框架中包含单个方法,这个方法带有一个执行该动作的executor,一个并发级别(表示要并发执行该动作的次数),以及表示该动作的runnable。所有的工作线程自身都准备好,要在timer线程启动时钟之前运行该动作。当最后一个工作线程准备好运行该动作时,timer线程就开始执行,同时允许工作线程执行该动作。一旦最后一个工作线程执行完该动作,timer线程就立即停止计时。直接在wait和notify之上实现这个逻辑至少来说会很混乱,而在CountDownLatch之上实现则相当简单。见如下示例代码:

public static long time(Executor executor,int concurrency,final Runnable action) {

         final CountDownLatch ready = new CountDownLatch(concurrency);

         final CountDownLatch start = new CountDownLatch(1);

         final CountDownLatch done = new CountDownLatch(concurrency);

         for (int i = 0; i < concurrency; i++) {

             executor.execute(new Runnable() {

                 public void run() {

                     ready.countDown();

                     try {

                         start.await();

                         action.run();

                     } catch (InterruptedException e) {

                         Thread.currentThread().interrupt();

                     } finally {

                         done.countDown();

                     }

                 }

             });

             //等待工作者线程准备可以执行,即所有的工作线程均调用ready.countDown()方法。

             ready.await();

             //这里使用nanoTime,是因为其精确度高于System.currentTimeMills()。

             long startNanos = System.nanoTime();

             //该语句执行后,工作者线程中的start.await()均将被唤醒。

             start.countDown();

             //下面的等待,只有在所有的工作者线程均调用done.countDown()之后才会被唤醒。

             done.await();

             return System.nanoTime() - startNanos;

         }

     }
七十一、慎用延迟初始化:

 

      延迟初始化作为一种性能优化的技巧,它要求类的域成员在第一次访问时才执行必要的初始化动作,而不是在类构造的时候完成该域字段的初始化。和大多数优化一样,对于延迟初始化,最好的建议"除非绝对必要,否则就不要这么做"。延迟初始化如同一把双刃剑,它确实降低了实例对象创建的开销,却增加了访问被延迟初始化的域的开销,这一点在多线程访问该域时表现的更为明显。见如下代码:

public class TestClass {

         private final FieldType field;

         synchronized FieldType getField() {

             if (field == null)

                 field = computeFieldValue();

             return field;

         }

     }

 从上面的代码可以看出,在每次访问该域字段时,均需要承担同步的开销。如果在真实的应用中,在多线程环境下,我们确实需要为一个实例化开销很大的对象实行延迟初始化,又该如何做呢?该条目提供了3中技巧:

      1. 对于静态域字段,可以考虑使用延迟初始化Holder class模式:

public class TestClass {

         private static class FieldHolder {

             static final FieldType field = computeFieldValue();

         }

         static FieldType getField() {

             return FieldHolder.field;

         }

     }

 当getField()方法第一次被调用时,它第一次读取FieldHolder.field,导致FieldHolder类得到初始化。这种模式的魅力在于,getField方法没有被同步,并且只执行一个域访问,因此延迟初始化实际上并没有增加任何访问成本。现在的VM将在初始化该类的时候,同步域的访问。一旦这个类被初始化,VM将修补代码,以便后续对该域的访问不会导致任何测试或者同步。

      2. 对于实例域字段,可使用双重检查模式:

public class TestClass {

         private volatile FieldType f;

         FieldType getField() {

             FieldType result = f;

             if (result == null) {

                 synchronized(this) {

                     result = f;

                     if (result == null)

                         f = result = computeFieldValue();

                 }

             }

             return result;

         }

     }

   注意在上面的代码中,首先将域字段f声明为volatile变量,其语义在之前的条目中已经给出解释,这里将不再赘述。再者就是在进入同步块之前,先针对该字段进行验证,如果不是null,即已经初始化,就直接返回该域字段,从而避免了不必要的同步开销。然而需要明确的是,在同步块内部的判断极其重要,因为在第一次判断之后和进入同步代码块之前存在一个时间窗口,而这一窗口则很有可能造成不同步的错误发生,因此第二次验证才是决定性的。

      在该示例代码中,使用局部变量result代替volatile的域字段,可以避免在后面的访问中每次都从主存中获取数据,从而提高函数的运行性能。事实上,这只是一种代码优化的技巧而已。

      针对该技巧,最后需要补充的是,在很多并发程序中,对某一状态的测试,也可以使用该技巧。

      3. 对于可以接受重复初始化实例域字段,可使用单重检查模式:

public class TestClass {

         private volatile FieldType f;

         FieldType getField() {

             FieldType result = f;

             if (result == null)

                 f = result = computeFieldValue();

             return result;

         }

     }
七十五、考虑使用自定义的序列化形式:

 

      设计一个类的序列化形式和设计该类的API同样重要,因此在没有认真考虑好默认的序列化形式是否合适之前,不要贸然使用默认的序列化行为。在作出决定之前,你需要从灵活性、性能和正确性多个角度对这种编码形式进行考察。一般来讲,只有当你自行设计的自定义序列化形式与默认的形式基本相同时,才能接受默认的序列化形式。比如,当一个对象的物理表示法等同于它的逻辑内容,可能就适合于使用默认的序列化形式。见如下代码示例:

public class Name implements Serializable {

         private final String lastName;

         private final String firstName;

         private final String middleName;

         ... ...

     }

   从逻辑角度而言,该类的三个域字段精确的反应出它的逻辑内容。然而有的时候,即便默认的序列化形式是合适的,通常还必须提供一个readObject方法以保证约束关系和安全性,如上例代码中,firstName和lastName不能为null等。

      下面我们再看一个极端的例子:

public final class StringList implements Serializable {

         private int size = 0;

         private Entry head = null;

         private static class Entry implements Serializable {

             String data;

             Entry next;

             Entry previous;

         }

     }

 对于上面的示例代码,如果采用默认形式的序列化,将会导致双向链表中的每一个节点的数据以及前后关系都会被序列化。因此这种物理表示法与它的逻辑数据内容有实质性的区别时,使用默认序列化形式会有以下几个缺点:

      1. 它使这个类的导出API永远的束缚在该类的内部表示法上,即使今后找到更好的的实现方式,也无法摆脱原有的实现方式。

      2. 它会消耗过多的空间。事实上对于上面的示例代码,我们只需要序列化数据部分,可以完全忽略链表节点之间的关系。

      3. 它会消耗过多的时间。

      4. 它会引起栈溢出。

     根据以上四点,我们修订了StringList类的序列化实现方式,见如下代码:

public final class StringList implements Serializable {

         private transient int size = 0;

         private transient Entry head = null;

         private static class Entry {

             String data;

             Entry next;

             Entry previous;

         }

         private void writeObject(ObjectOutputStream s) throws IOException {

             s.defaultWriteObject();

             s.writeInt(size);

             for (Entry e = head; e != null; e = e.next)

                 s.writeObject(e.data);

         }

         private void readObject(ObjectInputStream s)

             throws IOException, ClassNotFoundException {

             s.defaultReadObject();

             int numElemnet = s.readInt();

             for (int i = 0; i < numElements; i++)

                 add((String)s.readObject());

         }

         public final void add(String s) { ... }

         ... ...

     }

 在修订代码中,所有的域字段都是transient,但writeObject和readObject方法的首要任务仍然是先调用defaultWriteObject和defaultReadObject方法,即便这对于缺省序列化形式并不是必须的。因为在今后的修改中,很有可能会为该类添加非transient域字段,一旦忘记同步修改writeObject或readObject方法,将会导致序列化和反序列化的数据处理方式不一致。

      对于默认序列化还需要进一步说明的是,当一个或多个域字段被标记为transient时,如果要进行反序列化,这些域字段都将被初始化为其类型默认值,如对象引用域被置为null,数值基本域的默认值为0,boolean域的默认值为false。如果这些值不能被任何transient域所接受,你就必须提供一个readObject方法。它首先调用defaultReadObject,然后再把这些transient域恢复为可接受的值。

      最后需要说明的是,无论你是否使用默认的序列化形式,如果在读取整个对象状态的任何其他方法上强制任何同步,则也必须在对象序列化上强制这种同步,见如下代码:

private synchronized void writeObject(ObjectOutputStream s) throws IOException {

         s.defaultWriteObject();

     }
七十六、保护性的编写readObject方法:

 

      在条目39中介绍了一个不可变的日期范围类,它包含可变的私有Date域。该类通过在其构造器和访问方法中保护性的拷贝Date对象,极力的维护其约束条件和不可变性。见如下代码:

public final class Period {

         private final Date start;

         private final Date end;

         public Period(Date start, Date end) {

             this.start = new Date(start.getTime());

             this.end = new Date(end.getTime());

             if (this.start.compareTo(this.end) > 0)

                 throw new IllegalArgumentException();

         }

         public Date start() {

             return new Date(start.getTime());

         }

         public Date end() {

             return new Date(end.getTime());

         }

         public String toString() {

             return start + " - " + end;

         }

         ... ...

     }

   这个对象的物理表示法和其逻辑表示法完全匹配,所以我们可以使用默认的序列化形式。因此在声明该类的地方增加" implements Serializable "。然而,如果你真是这样做了,那么这个类将不再保证他的关键约束了。

      问题在于,如果反序列化的数据源来自于该类实例的正常序列化,那么将不会引发任何问题。如果恰恰相反,反序列化的数据源来自于一组伪造的数据流,事实上,反序列化的机制就是从一组有规则的数据流中实例化指定对象,那么我们将不得不面对Period实例对象的内部约束被破坏的危险,见如下代码:

public class BogusPeriod {

         private static final byte[] serializedForm = new byte[] {

             ... ... //这里是一组伪造的字节流数据

         };

         public static void main(String[] args) [

             Period p = (Period)deserialize(serializedForm);

             System.out.println(p);

         }

         private static Object deserialize(byte[] sf) {

             try {

                 InputStream is = new ByteArrayInputStream(sf);

                 ObjectInputStream ois = new ObjectInputStream(is);

                 return ois.readObject();

             } catch (Exception e) {

                 throw new IllegalArgumentException(e);

             }

         }

     }

  如果执行上面的代码就会发现Period的约束被打破了,end的日期早于start。为了修正这个问题,可以为Period提供一个readObject方法,该方法首先调用defaultReadObject,然后检查被反序列化之后的对象的有效性。如果检查失败,则抛出InvalidObjectException异常,使反序列化过程不能成功地完成。

private void readObject(ObjectInputStream s)

         throws IOException,ClassNotFoundException {

         s.defaultReadObject();

         if (start.compareTo(end) > 0)

             throw new InvalidObjectException(start + " after " + end);

     }

  如果执行上面的代码就会发现Period的约束被打破了,end的日期早于start。为了修正这个问题,可以为Period提供一个readObject方法,该方法首先调用defaultReadObject,然后检查被反序列化之后的对象的有效性。如果检查失败,则抛出InvalidObjectException异常,使反序列化过程不能成功地完成。

private void readObject(ObjectInputStream s)

         throws IOException,ClassNotFoundException {

         s.defaultReadObject();

         if (start.compareTo(end) > 0)

             throw new InvalidObjectException(start + " after " + end);

     }

   除了上面的攻击方式之外,还存在着另外一种更为隐匿的攻击方式,它也是通过伪造序列化数据流的方式来骗取反序列化方法的信任。它在伪造数据时,将私有域字段的引用在外部保存起来,这样当对象实例反序列化成功后,由于外部仍然可以操作其内部数据,因此危险仍然存在。如何避免该风险呢?见如下修订后的readObject方法:

private void readObject(ObjectInputStream s)

         throws IOException,ClassNotFoundException {

         s.defaultReadObject();

         //执行保护性copy

         start = new Date(start.getTime());

         end = new Date(end.getTime());

         if (start.compareTo(end) > 0)

             throw new InvalidObjectException(start + " after " + end);

     }

 注意,保护性copy一定要在有效性检查之前进行。

      这里给出一个基本的规则,可以用来帮助确定默认的readObject方法是否可以被接受。规则是增加一个公有的构造器,其参数对应于该对象中每个非transient域,并且无论参数的值是什么,都是不进行检查就可以保存到相应的域中的。对于这样的做法如果仍然可以接受,那么默认的readObject就是合理,否则就需要提供一个显式的readObject方法。

      对于非final的可序列化类,在readObject方法和构造器之间还有其他类似的地方,readObject方法不可以调用可被覆盖的方法,无论是直接调用还是间接调都不可以。如果违反了该规则,并且覆盖了该方法,被覆盖的方法将在子类的状态被反序列化之前先运行。程序很可能会失败。


作者:chen.yu
深信服三年半工作经验,目前就职游戏厂商,希望能和大家交流和学习,
微信公众号:编程入门到秃头 或扫描下面二维码
零基础入门进阶人工智能(链接)