当前位置: 首页 > news >正文

越秀区建设水务局网站网站怎么推广最

越秀区建设水务局网站,网站怎么推广最,婚纱照,电脑ps软件有免费的吗一、考虑用静态工厂方法代替构造器#xff1a; 构造器是创建一个对象实例最基本也最通用的方法#xff0c;大部分开发者在使用某个class的时候#xff0c;首先需要考虑的就是如何构造和初始化一个对象示例#xff0c;而构造的方式首先考虑到的就是通过构造函数来完成#…一、考虑用静态工厂方法代替构造器 构造器是创建一个对象实例最基本也最通用的方法大部分开发者在使用某个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.    在创建参数化类型实例的时候它们使代码变得更加简洁。 MapString,String m  new HashMapString,String(); 由于Java在构造函数的调用中无法进行类型的推演因此也就无法通过构造器的参数类型来实例化指定类型参数的实例化对象。然而通过静态工厂方法则可以利用参数类型推演的优势避免了类型参数在一次声明中被多次重写所带来的烦忧见如下代码 public static K,V HashMapK,V newInstance() { return new HashMapK,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方式可选参数的缺省值问题也将不再困扰着所有的使用者。这种方式还带来了一个间接的好处是不可变对象的初始化以及参数合法性的验证等工作在构造函数中原子性的完成了。 MapString,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*size1); } } 以上示例代码在正常的使用中不会产生任何逻辑问题然而随着程序运行时间不断加长内存泄露造成的副作用将会慢慢的显现出来如磁盘页交换、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的引用值xx.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; ListCaseInsensitiveString l  new ArrayListCaseInsensitiveString(); 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 SetPoint unitCircle; static { unitCircle  new HashSetPoint(); 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还有一个潜在的规则如果其左值为nullinstanceof操作符将始终返回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) { MapPhoneNumber,String m  new HashMapPhoneNumber,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 cant 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的例子如果不覆盖该方法就会输出PhoneNumber163b91 这样的不可读信息因此也不会给我们诊断问题带来更多的帮助。以下代码重载了该方法那么在我们调用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 之后我们实现的ComparableT接口多为该泛型接口不在推荐直接继承1.5 之前的非泛型接口Comparable了新的compareTo方法的参数也由Object替换为接口的类型参数因此在正常调用的情况下如果参数类型不正确将会直接导致编译错误这样有助于开发者在coding期间修正这种由类型不匹配而引发的异常。 在该条目中针对compareTo的相等性比较给出了一个强烈的建议而不是真正的规则。推荐compareTo方法施加的等同性测试在通常情况下应该返回和equals方法同样的结果考虑如下情况 public static void main(String[] args) { HashSetBigDecimal hs  new HashSetBigDecimal(); 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()); TreeSetBigDecimal ts  new TreeSetBigDecimal(); 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均实现了SetE接口倘若我们的集合是以SetE的参数形式传递到当前添加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 InstrumentedHashSetE extends HashSetE { 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) { InstrumentedHashSetString s  new InstrumentedHashSetString(); 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 ForwardingSetE implements SetE { private final SetE s; public ForwardingSet(SetE 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 InstrumentedHashSetE extends ForwardingSetE { 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类的定义极大的限制了参数的类型这样客户端也无法再传递任何其他的比较策略。为了修正这一问题我们需要让该类成为ComparatorT接口的实现类由于ComparatorT是泛型类因此我们可以随时替换策略对象的参数类型如: class StringLengthComparator implements ComparatorString { 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.    局部类 是四种嵌套类中最少使用的类在任何可以声明局部变量的地方都可以声明局部类并且局部类也遵守同样的作用域规则。 二十三、请不要在新代码中使用原生态类型 先简单介绍一下泛型的概念和声明形式。声明中具有一个或者多个类型参数的类或者接口就是泛型类或接口如ListE这其中E表示List集合中元素的类型。在Java中相对于每个泛型类都有一个原生类与之对应即不带任何实际类型参数的泛型名称如ListE的原生类型List。他们之间最为明显的区别在于ListE包含的元素必须是E(泛型)类型如ListString那么他的元素一定是String否则将产生编译错误。和泛型不同的是原生类型List可以包含任何类型的元素因此在向集合插入元素时即使插入了不同类型的元素也不会引起编译期错误。那么在运行当List的使用从List中取出元素时将不得不针对类型作出判断以保证在进行元素类型转换时不会抛出ClassCastException异常。由此可以看出泛型集合ListE不仅可以在编译期发现该类错误而且在取出元素时不需要再进行类型判断从而提高了程序的运行时效率。 //原生类型的使用方式 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 ListStamp stamps  new ListStamp(); 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); 在之后的升级代码中如果给该函数传入泛型类型的ListE对象将是合法的不会产生编译错误。同时Java的泛型对象在运行时也会被擦除类型即ListE擦除类型后将会变成ListJava之所以这样实现也就是为了保持向后的兼容性。 现在我们比较一下List和ListObject这两个类型之间的主要区别尽管这两个集合可以包含任何类型的对象元素但是前者是类型不安全的而后者则明确告诉使用者可以存放任意类型的对象元素。另一个区别是如果void func(List l)改为void func(ListObject l)ListString类型的对象将不能传递给func函数因为Java将这两个泛型类型视为完全不同的两个类型。 在新代码中不要使用原生类型这条规则有两个例外两者都源于“泛型信息可以在运行时被擦除”这一事实。在Class对象中必须要使用原生类型。JLS不允许使用Class的参数化类型。换句话说List.class, String[].class和int.class都是合法的但是ListString.class和List?.class则是不合法。这条规则的第二个例外与instanceof操作符相关。由于泛型信息可以在运行时被擦除因此在泛型类型上使用instanceof操作符是非法的。如 private void test(Set o) { if (o instanceof Set) { Set? m  (Set?)o; } } 二十四、消除非受检警告 在进行泛型编程时经常会遇到编译器报出的非受检警告(unchecked cast warnings)如SetLark 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的子类与之相反泛型是不可协变的如ListObject objList  new ListLong()是非法的将无法通过编译。因此泛型可以保证更为严格的类型安全性一旦出现插入元素和容器声明时不匹配的现象是将会在编译期报错。二者的另一个区别是数组是具体化的因此数组会在运行时才知道并检查它们的元素类型约束。如将一个String对象存储在Long的数组中时就会得到一个ArrayStoreException异常。相比之下泛型则是通过擦除来实现的。因此泛型只是在编译时强化类型信息并在运行时丢弃它们的元素类型信息。擦除就是使泛型可以与没有使用泛型的代码随意进行交互。由此可以得出混合使用泛型和数组是比较危险的因为Java的编译器禁止了这样的使用方法一旦使用将会报编译错误。见如下用例 public void test() { //这里我们先假设该语句可以通过编译 ListString[] stringLists  new ListString[1]; //该语句是正常的intList中将仅包含值为42的一个整型元素 ListInteger intList  Arrays.asList(42); //该语句也是合法的因为数组支持协变 Object[] objects  stringLists; //由于泛型对象在运行时是擦除对象类型信息的擦除后intList将变为List类型 //而objects是Object类型的数组List本身也是Object的子类因此下面的语句合法。 objects[0]  intList; //下面的语句将会抛出ClassCastException异常。很显然stringLists[0]是ListInteger对象。 String s  stringLists[0].get(0); } 从以上示例得出当你得到泛型数组创建错误时最好的解决办法通常是优先使用集合类型ListE而不是数组类型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(ListE l,FunctionE f,E initVal) { E[] snapshot  l.toArray(); E result  initVal; for (E e : snapshot) { result  f.apply(result,e); } return result; } interface FunctionE { E apply(E arg1,E arg2); } 这样的写法回提示一个编译错误即E[] snapshot  l.toArray();是无法直接转换并赋值的。修改方式也很简单直接强转就可以了如E[] snapshot  (E[])l.toArray();在强转之后仍然会收到编译器给出的一条警告信息即无法在运行时检查转换的安全性。尽管结果证明这样的修改之后是可以正常运行的但是这样的写法确实也是不安全的更好的办法是通过ListE替换E[]见如下修改后的代码 static E E reduce(ListE l,FunctionE f,E initVal) { E[] snapshot  new ArrayListE(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 StackE { 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); } } 上面的泛型集合类StackE在编译时会引发一个编译错误即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 SetE union(SetE s1,SetE s2) { SetE result  new HashSetE(s1); result.addAll(s2); return result; } 和调用泛型对象构造函数来创建泛型对象不同的是在调用泛型函数时无须指定函数的参数类型而是通过Java编译器的类型推演来填充该类型信息见如下泛型对象的构造 MapString,ListString anagrams  new HashMapString,ListString(); 很明显以上代码在等号的两边都显示的给出了类型参数并且必须是一致的。为了消除这种重复可以编写一个泛型静态工厂方法与想要使用的每个构造器相对应如 public static K,V HashMapK,V newHashMap() { return new HashMapK,V(); } 我们的调用方式也可以改为MapString,ListString anagrams  newHashMap(); 除了在以上的情形下使用泛型函数之外我们还可以在泛型单例工厂的模式中应用泛型函数这些函数通常为无状态的且不直接操作泛型对象的方法见如下示例 public interface UnaryFunctionT { T apply(T arg); } private static UnaryFunctionObject IDENTITY_FUNCTION  new UnaryFunctionObject() { public Object apply(Object arg) { return arg; } }; SuppressWarning(unchecked) public static T UnaryFunctionT identityFunction() { return (UnaryFunctionT)IDENTITY_FUNCTION; } 调用方式如下 public static void main(String[] args) { String[] strings  {jute,hemp,nylon}; UnaryFunctionString sameString  identityFunction(); for (String s : strings) System.out.println(sameString.apply(s)); Number[] numbers  {1,2.0,3L}; UnaryFunctionNumber sameNumber  identityFunction(); for (Number n : numbers) System.out.println(sameNumber.apply(n)); } 对于该静态函数如果我们为类型参数添加更多的限制条件如参数类型必须是ComparableT的实现类这样我们的函数对象便可以基于该接口做更多的操作而不仅仅是像上例中只是简单的返回参数对象见如下代码 public static T extends ComparableT T max(ListT l) { IteratorT 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 StackE { public Stack(); public void push(E e); public E pop(); public boolean isEmpty(); } 现在我们需要增加一个方法: public void pushAll(IterableE 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(CollectionE dst) { while (!isEmpty()) dst.add(pop()); } popAll方法将当前容器中的元素全部弹出并以此添加到参数集合中。如果Collections中的类型参数和Stack完全一致这样的写法不会有任何问题然而在实际的应用中我们通常会将Collection中的元素视为更通用的对象类型如Object见如下应用代码 StackNumber numberStack  new StackNumber(); CollectionObject 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 EpopAll的dst参数通过Stack消费E实例因此dst相应的类型为Collection? super E。PECS这个助记符突出了使用通配符类型的基本原则。 在上一个条目中给出了下面的泛型示例函数 public static E SetE union(SetE s1, SetE s2); 这里的s1和s2都是生产者根据PECS原则它们的声明可以改为 public static E SetE union(Set? extends E s1,Set? extends E s2); 由于泛型函数在调用时其参数类型是可以通过函数参数的类型推演出来的如果上面的函数被如下方式调用时将会导致Java的编译器无法推演出泛型参数的实际类型因此引发了编译错误。 SetInteger integers  new SetInteger(); SetDouble doubles  new SetDouble(); SetNumber numbers  union(integers,doubles); 如果想顺利通过编译并得到正确的执行结果我们只能通过显示的方式指定该函数类型参数的实际类型从而避免了编译器的类型参数自动推演见修改后的代码 SetNumber numbers  Union.Numberunion(integers,doubles); 现在我们再来看一下前面也给出过的max方法其初始声明为 public static T extends ComparableT T maxListT srcList); 下面是修改过的使用通配符类的声明 public static T extends Comparable? super T T max(List? extends T srcList); 下面将逐一给出新声明的解释 1.    函数参数srcList产生了T实例因此将类型从ListT改为List? extends T 2.    最初T被指定为扩展ComparableT然而Comparable又是T的消费者用于比较两个T之间的顺序关系。因此参数化类型ComparableT被替换为Comparable? super T。 注Comparator和Comparable一样他们始终都是消费者因此Comparable? super T优先于ComparableT。 二十九、优先考虑类型安全的异构容器 泛型通常用于集合如Set和Map等。这样的用法也就限制了每个容器只能有固定数目的类型参数一般来说这也确实是我们想要的。然而有的时候我们需要更多的灵活性如数据库可以用任意多的Column如果能以类型安全的方式访问所有Columns就好了幸运的是有一种方法可以很容易的做到这一点就是将key进行参数化而不是将容器参数化见以下代码 public class Favorites { public T void putFavorite(ClassT type,T instance); public T T getFavorite(ClassT 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 MapClass?,Object favorites   new HashMapClass?,Object(); public T void putFavorite(ClassT type,T instance) { if (type  null) throw new NullPointerException(Type is null); favorites.put(type,type.cast(instance)); } public T T getFavorite(ClassT type) { return type.cast(favorites.get(type)); } } 可以看出每个Favorites实例都得到一个MapClass?,Object容器的支持。由于该容器的值类型为Object为了进一步确实类型的安全性我们在put的时候通过Class.cast()方法将Object参数尝试转换为Class所表示的类型如果类型不匹配将会抛出ClassCastException异常。以此同时在从Map中取出值对象的时候由于该对象当前的类型是Object因此我们需要再次利用Class.cast()函数将其转换为我们的目标类型。 对于Favorites类的put/get方法有一个非常明显的限制即我们无法将“不可具体化”类型存入到该异构容器中如ListString、ListInteger等泛型类型。这样的限制主要源于Java中泛型类型在运行时的类型擦出机制即ListString.class和ListInteger.class是等同的对象均为List.class。如果Java编译器通过了这样的调用代码那么ListString.class和ListInteger.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.302e23,2.439e6), VENUS(4.869e24,6.052e6), EARTH(5.975e24,6.378e6), MARS(6.419e23,3.393e6), JUPITER(1.899e27,7.149e7), SATURN(5.685e26,6.027e7), URANUS(8.683e25,2.556e7), NEPTUNE(1.024e26,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 MapString,Operation stringToEnum  new HashMapString,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(SetStyle 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(); SetHerb herbsByType  (SetHerb[])new Set[Herb.Type.values().length]; for (int i  0; i  herbsByType.length; i) { herbsByType[i]  new HashSetHerb(); } 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(); MapHerb.Type,SetHerb herbsByType   new EnumMapHerb.Type,SetHerb(Herb.Type.class); for (Herb.Type t : Herb.Type.values()) { herbssByType.put(t,new HashSetHerb()); } 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 EnumT  Operation void test( ClassT 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)); } } 注意参数ClassT 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 HashSetString(), new ArrayListBigInteger(), new HashMapString,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) { SetInteger s  new TreeSetInteger(); ListInteger l  new ArrayListInteger(); 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表示IntegerJava的编译器会将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) { SetInteger s  new TreeSetInteger(); ListInteger l  new ArrayListInteger(); 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 ListCheese cheesesInStock  new ListCheese(); 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循环见如下代码片段 IteratorElement i  c.iterator(); while (i.hasNext()) { doSomething(i.next()); } ... ... IteratorElement i2  c2.iterator(); while (i.hasNext()) {  //BUG! doSomethingElse(i2.next()); } 可以看到在第二个循环的循环条件判断处有一个非常明显的BUG这极有可能是copy-paste所致。然而该类错误如果出现在for循环里将直接引发编译期错误。见如下代码片段    for (IteratorElement i  c.iterator(); i.hasNext(); ) { doSomething(i.next()); } ... ... for (IteratorElement 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 } ... ... CollectionSuit suits  Arrays.asList(Suit.values()); CollectionRank ranks  Arrays.asList(Rank.values()); ListCard deck  new ArrayListCard(); for (IteratorSuit i  suits.iterator(); i.hasNext(); } { for (IteratorRank j  ranks.iterator(); j.hasNext(); ) deck.add(new Card(i.next(),j.next()); //BUG, j被多次迭代 } 上面代码的BUG是比较隐匿的很多专家级的程序员也会偶尔犯类似的错误。下面我们来一下修复后的代码片段 ... ... for (IteratorSuit i  suits.iterator(); i.hasNext(); } { Suit suit  i.next(); for (IteratorRank 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倍。由于第一种做法的开销是随项目数量呈平方级增加而第二种做法是线性增加的所以数目越大差异越大。 五十二、通过接口引用对象 一般来讲在函数参数、返回值、域变量等声明中应该尽量使用接口而不是类作为它们的类型。只有当你利用构造器创建某个对象的时候才真正需要引用这个对象的类如 ListSubscriber subscribers  new VectorSubscriber(); 而不是像下面这样的声明 VectorSubscriber subscribers  new VectorSubscriber(); 如果你养成了用接口作为类型的习惯你的程序将更加灵活。对于上面的例子在今后的改进中如果不想使用Vector作为实例化对象我们只需在如下一出进行修改即可 ListSubscriber subscribers  new ArrayListSubscriber(); 如果之前该变量的类型不是接口类型而是它实际类型的本身那么在做如此修改之前则需要确认在所有使用该变量的代码行是否用到了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); } SetString s  null; try { s  (SetString)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不应该强迫它的客户端为了正常的控制流而使用异常。如IteratorJDK在设计时充分考虑到这一点客户端在执行next方法之前需要先调用hasNext方法已确认是否还有可读的集合元素见如下代码 for (IteratorFoo i  collection.iterator(); i.hasNext(); ) { Foo f  i.next(); } 如果Iterator缺少hasNext方法客户端则将被迫改为下面的写法 try { IteratorFoo 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; } 如果没有在操作之前验证sizeelements的数组也会抛出异常但是由于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 ObservableSetE extends ForwardingSetE { public ObservableSet(SetE set) { super(set); } private final ListSetObserverE observers  new ArrayListSetObserverE(); public void addObserver(SetObserverE observer) { synchronized(observers) { observers.add(observer); } } public boolean removeObserver(SetObserverE observer) { synchronized(observers) { return observers.remover(observer); } } private void notifyElementAdded(E element) { synchronized(observers) { for (SetObserverE 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 SetObserverE { void added(ObservableSetE set,E element); } public static void main(String[] args) { ObservableSetInteger set  new ObservableSetInteger(new HashSetInteger()); set.addObserver(new SetObserverInteger() { public void added(ObservableSetInteger s, Integer e) { System.out.println(e); } }); for (int i  0; i  100; i) set.add(i); } 对于这个测试用例他完全没有问题可以保证得到正确的输出即打印出0-99的数字。 现在我们换一个观察者接口的实现方式见如下代码片段 set.addObserver(new SetObserverInteger() { public void added(ObservableSetInteger 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) { ListSetObserverE snapshot  null; synchronized(observers) { snapshot  new ArrayListSetObserverE(observers); } for (SetObserverE 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数值基本域的默认值为0boolean域的默认值为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方法不可以调用可被覆盖的方法无论是直接调用还是间接调都不可以。如果违反了该规则并且覆盖了该方法被覆盖的方法将在子类的状态被反序列化之前先运行。程序很可能会失败。
http://www.pierceye.com/news/576749/

相关文章:

  • 成都哪些公司可以做网站建网站现软件
  • 深圳wap网站建设传奇霸主页游
  • 做网站首先要干什么营销软文200字
  • 帝国cms做的网站私人定制女装店
  • 网站建设南沙wordpress video
  • 网站建设开票应该开哪个行业什么网站可以免费做视频的软件
  • 百度seo查询收录查询网站推广策划案seo教程
  • 如何免费建立网站中贤建设集团网站
  • 如何做转运网站黄聪 wordpress
  • 临海市住房与城乡建设规划局网站宁波网络推广培训
  • go 网站开发自己在线制作logo
  • 重庆市网站建设公司企业服务账号
  • 网站建设的市场情况网站系统里不能打印
  • 网站如何适应屏幕做网站时无法上传图片
  • 网站的橱窗怎么做嘉兴住房和城乡建设厅网站
  • 吉林省城乡建设官方网站163企业邮箱登录入口官网
  • 做网站参考文献某企业网站建设方案2000字
  • 网站托管哪家好织梦购物网站整站源码
  • 怎么做网站的优化排名wordpress的目录结构(一)
  • 个人可以做公益网站吗美食杰网站的建设目的
  • 宿迁公司企业网站建设《网站基础建设-首保》
  • 做全屏式网站尺寸是多大国外虚拟主机 两个网站
  • 黑龙江建设网站招聘广西住房和城乡建设厅培训中心官方网站
  • 做网站客户最关心的是什么制作网页原型的目的
  • 电子商务网站建设工具河南安阳吧
  • 南通网站建设公司哪个好肯德基的网站建设
  • 高端大气网站源码wordpress做双语网站
  • 360网站推广东莞凤岗
  • 公司网站高端网站建设赣州做网站多少钱
  • dw做网站怎么发布建设银行官方网站登录入口