六、消除过期的对象引用:

      尽管Java不像C/C++那样需要手工管理内存资源,而是通过更为方便、更为智能的垃圾回收机制来帮助开发者清理过期的资源。即便如此,内存泄露问题仍然会发生在你的程序中,只是和C/C++相比,Java中内存泄露更加隐匿,更加难以发现,见如下代码:
  1. public class Stack {  
  2.          private Object[] elements;  
  3.          private int size = 0;  
  4.          private static final int DEFAULT_INITIAL_CAPACITY = 16;  
  5.          public Stack() {  
  6.              elements = new Object[DEFAULT_INITIAL_CAPACITY];  
  7.          }  
  8.          public void push(Object e) {  
  9.              ensureCapacity();  
  10.              elements[size++] = e;  
  11.          }  
  12.          public Object pop() {  
  13.              if (size == 0)   
  14.                  throw new EmptyStackException();  
  15.              return elements[--size];  
  16.          }  
  17.          private void ensureCapacity() {  
  18.              if (elements.length == size)  
  19.                  elements = Arrays.copys(elements,2*size+1);  
  20.          }  
  21.      }  
 以上示例代码,在正常的使用中不会产生任何逻辑问题,然而随着程序运行时间不断加长,内存泄露造成的副作用将会慢慢的显现出来,如磁盘页交换、OutOfMemoryError等。那么内存泄露隐藏在程序中的什么地方呢?当我们调用pop方法是,该方法将返回当前栈顶的elements,同时将该栈的活动区间(size)减一,然而此时被弹出的Object仍然保持至少两处引用,一个是返回的对象,另一个则是该返回对象在elements数组中原有栈顶位置的引用。这样即便外部对象在使用之后不再引用该Object,那么它仍然不会被垃圾收集器释放,久而久之导致了更多类似对象的内存泄露。修改方式如下: 
  1. public Object pop() {  
  2.          if (size == 0)   
  3.              throw new EmptyStackException();  
  4.          Object result = elements[--size];  
  5.          elements[size] = null//手工将数组中的该对象置空  
  6.          return result;  
  7.      }  
  由于现有的Java垃圾收集器已经足够只能和强大,因此没有必要对所有不在需要的对象执行obj = null的显示置空操作,这样反而会给程序代码的阅读带来不必要的麻烦,该条目只是推荐在以下3中情形下需要考虑资源手工处理问题:
      1)    类是自己管理内存,如例子中的Stack类。
      2)    使用对象缓存机制时,需要考虑被从缓存中换出的对象,或是长期不会被访问到的对象。
      3)    事件监听器和相关回调。用户经常会在需要时显示的注册,然而却经常会忘记在不用的时候注销这些回调接口实现类。
   
七、避免使用终结方法:
 
      任何事情都存在其一定的双面性或者多面性,对于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区块中调用该方法,见如下代码:  
  1. public void test() {  
  2.          FileInputStream fin = null;  
  3.          try {  
  4.              fin = new FileInputStream(filename);  
  5.              //do something.  
  6.          } finally {  
  7.              fin.close();  
  8.          }  
  9.      }  

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

  1. public class FinalizeTest {  
  2.          //@Override  
  3.          protected void finalize() throws Throwable {  
  4.              try {  
  5.                  //在调试过程中通过该方法,打印对象在被收集前的各种状态,  
  6.  //如判断是否仍有资源未被释放,或者是否有状态不一致的现象存在。  
  7.  //推荐将该finalize方法设计成仅在debug状态下可用,而在release  
  8.  //下该方法并不存在,以避免其对运行时效率的影响。  
  9.                  System.out.println("The current status: " + _myStatus);  
  10.              } finally {  
  11.                  //在finally中对超类finalize方法的调用是必须的,这样可以保证整个class继承  
  12.  //体系中的finalize链都被执行。  
  13.                  super.finalize();   
  14.              }  
  15.          }  
  16.      }  
 八、覆盖equals时请遵守通用约定:
  对于Object类中提供的equals方法在必要的时候是必要重载的,然而如果违背了一些通用的重载准则,将会给程序带来一些潜在的运行时错误。如果自定义的class没有重载该方法,那么该类实例之间的相等性的比较将是基于两个对象是否指向同一地址来判定的。因此对于以下几种情况可以考虑不重载该方法:
      1.    类的每一个实例本质上都是唯一的。
      不同于值对象,需要根据其内容作出一定的判定,然而该类型的类,其实例的自身便具备了一定的唯一性,如Thread、Timer等,他本身并不具备更多逻辑比较的必要性。
      2.    不关心类是否提供了“逻辑相等”的测试功能。
      如Random类,开发者在使用过程中并不关心两个Random对象是否可以生成同样随机数的值,对于一些工具类亦是如此,如NumberFormat和DateFormat等。
      3.    超类已经覆盖了equals,从超类继承过来的行为对于子类也是合适的。
      如Set实现都从AbstractSet中继承了equals实现,因此其子类将不在需要重新定义该方法,当然这也是充分利用了继承的一个优势。
      4.    类是私有的或是包级别私有的,可以确定它的equals方法永远不会被调用。
   
      那么什么时候应该覆盖Object.equals呢?如果类具有自己特有的“逻辑相等”概念,而且超类中没有覆盖equals以实现期望的行为,这是我们就需要覆盖equals方法,如各种值对象,或者像Integer和Date这种表示某个值的对象。在重载之后,当对象插入Map和Set等容器中时,可以得到预期的行为。枚举也可以被视为值对象,然而却是这种情形的一个例外,对于枚举是没有必要重载equals方法,直接比较对象地址即可,而且效率也更高。
      在覆盖equals是,该条目给出了通用的重载原则:
      1.    自反性:对于非null的引用值x,x.equals(x)返回true。
      如果违反了该原则,当x对象实例被存入集合之后,下次希望从该集合中取出该对象时,集合的contains方法将直接无法找到之前存入的对象实例。
      2.    对称性:对于任何非null的引用值x和y,如果y.equals(x)为true,那么x.equals(y)也为true。 
  1. public final class CaseInsensitiveString {  
  2.          private final String s;  
  3.          public CaseInsensitiveString(String s) {  
  4.              this.s = s;  
  5.          }  
  6.          @Override public boolean equals(Object o) {  
  7.              if (o instanceof CaseInsensitiveString)   
  8.                  return s.equalsIgnoreCase((CaseInsensitiveString)o).s);  
  9.              if (o instanceof String) //One-way interoperability  
  10.                  return s.equalsIgnoreCase((String)o);  
  11.              return false;  
  12.          }  
  13.      }  
  14.      public static void main(String[] args) {  
  15.          CaseInsensitiveString cis = new CaseInsensitiveString("Polish");  
  16.          String s = "polish";  
  17.          List<CaseInsensitiveString> l = new ArrayList<CaseInsensitiveString>();  
  18.          l.add(cis);  
  19.          if (l.contains(s))   
  20.              System.out.println("s can be found in the List");  
  21.      }  
  对于上例,如果执行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实现可以做如下修改:  
  1. @Override public boolean equals(Object o) {  
  2.          if (o instanceof CaseInsensitiveString)   
  3.              return s.equalsIgnoreCase((CaseInsensitiveString)o).s);  
  4.          return false;  
  5.      }  

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

      3.    传递性:对于任何非null的引用值x、y和z,如果x.equals(y)返回true,同时y.equals(z)也返回true,那么x.equals(z)也必须返回true。 
  1. public class Point {  
  2.          private final int x;  
  3.          private final int y;  
  4.          public Point(int x,int y) {  
  5.              this.x = x;  
  6.              this.y = y;  
  7.          }  
  8.          @Override public boolean equals(Object o) {  
  9.              if (!(o instanceof Point))   
  10.                  return false;  
  11.              Point p = (Point)o;  
  12.              return p.x == x && p.y == y;  
  13.          }  
  14.      }  
  对于该类的equals重载是没有任何问题了,该逻辑可以保证传递性,然而在我们试图给Point类添加新的子类时,会是什么样呢? 
  1. public class ColorPoint extends Point {  
  2.          private final Color c;  
  3.          public ColorPoint(int x,int y,Color c) {  
  4.              super(x,y);  
  5.              this.c = c;  
  6.          }  
  7.          @Override public boolean equals(Object o) {  
  8.              if (!(o instanceof ColorPoint))   
  9.                  return false;  
  10.              return super.equals(o) && ((ColorPoint)o).c == c;  
  11.          }  
  12.      }  
 如果在ColorPoint中没有重载自己的equals方法而是直接继承自超类,这样的相等性比较逻辑将会给使用者带来极大的迷惑,毕竟Color域字段对于ColorPoint而言确实是非常有意义的比较性字段,因此该类重载了自己的equals方法。然而这样的重载方式确实带来了一些潜在的问题,见如下代码:  
  1. public void test() {  
  2.          Point p = new Point(1,2);  
  3.          ColorPoint cp = new ColorPoint(1,2,Color.RED);  
  4.          if (p.equals(cp))  
  5.              System.out.println("p.equals(cp) is true");  
  6.          if (!cp.equals(p))  
  7.              System.out.println("cp.equals(p) is false");  
  8.      }  
  9.    从输出结果来看,ColorPoint.equals方法破坏了相等性规则中的对称性,因此需要做如下修改:@Override public boolean equals(Object o) {  
  10.          if (!(o instanceof Point))   
  11.              return false;  
  12.          if (!(o instanceof ColorPoint))  
  13.              return o.equals(this);  
  14.          return super.equals(o) && ((ColorPoint)o).c == c;  
  15.      }  
  经过这样的修改,对称性确实得到了保证,但是却牺牲了传递性,见如下代码:  
  1. public void test() {  
  2.          ColorPoint p1 = new ColorPoint(1,2,Color.RED);  
  3.          Point p2 = new Point(1,2);  
  4.          ColorPoint p1 = new ColorPoint(1,2,Color.BLUE);  
  5.          if (p1.equals(p2) && p2.equals(p3))  
  6.              System.out.println("p1.equals(p2) && p2.equals(p3) is true");  
  7.          if (!(p1.equals(p3))  
  8.              System.out.println("p1.equals(p3) is false");  
  9.      }  

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

  1. @Override public boolean equals(Object o) {  
  2.          if (o == null || o.getClass() == getClass())   
  3.              return false;  
  4.          Point p = (Point)o;  
  5.          return p.x == x && p.y == y;  
  6.      }  
   这样的Point.equals确实保证了对象相等性的这几条规则,然而在实际应用中又是什么样子呢? 
  1. class MyTest {  
  2.          private static final Set<Point> unitCircle;  
  3.          static {  
  4.              unitCircle = new HashSet<Point>();  
  5.              unitCircle.add(new Point(1,0));  
  6.              unitCircle.add(new Point(0,1));  
  7.              unitCircle.add(new Point(-1,0));  
  8.              unitCircle.add(new Point(0,-1));  
  9.          }  
  10.          public static boolean onUnitCircle(Point p) {  
  11.              return unitCircle.contains(p);  
  12.          }  
  13.      }  
  如果此时我们测试的不是Point类本身,而是ColorPoint,那么按照目前Point.equals(getClass方式)的实现逻辑,ColorPoint对象在被传入onUnitCircle方法后,将永远不会返回true,这样的行为违反了"里氏替换原则"(敏捷软件开发一书中给出了很多的解释),既一个类型的任何重要属性也将适用于它的子类型。因此该类型编写的任何方法,在它的子类型上也应该同样运行的很好。
      如何解决这个问题,该条目给出了一个折中的方案,既复合优先于继承,见如下代码: 
  1. public class ColorPoint {  
  2.          //包含了Point的代理类  
  3.          private final Point p;  
  4.          private final Color c;  
  5.          public ColorPoint(int x,int y,Color c) {  
  6.              if (c == null)  
  7.                  throw new NullPointerException();  
  8.              p = new Point(x,y);  
  9.              this.c = c;  
  10.          }  
  11.          //提供一个视图方法返回内部的Point对象实例。这里Point实例为final对象非常重要,  
  12.  //可以避免使用者的误改动。视图方法在Java的集合框架中有着大量的应用。  
  13.          public Point asPoint() {  
  14.              return p;  
  15.          }  
  16.          @Override public boolean equals(Object o) {  
  17.              if (!(o instanceof ColorPoint))   
  18.                  return false;  
  19.              ColorPoint cp = (ColorPoint)o;  
  20.              return cp.p.equals(p) && cp.c.equals(c);  
  21.          }  
  22.      }  
  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的判断,见如下代码:  
  1. @Override public boolean equals(Object o) {  
  2.          if (o == null)  
  3.              return false;  
  4.          if (!(o instanceof MyType))   
  5.              return false;  
  6.          ...  
  7.      }  
  注意以上代码中的instanceof判断,由于在后面的实现中需要将参数o进行类型强转,如果类型不匹配则会抛出ClassCastException,导致equals方法提前退出。在此需要指出的是instanceof还有一个潜在的规则,如果其左值为null,instanceof操作符将始终返回false,因此上面的代码可以优化为:  
  1. @Override public boolean equals(Object o) {  
  2.          if (!(o instanceof MyType))   
  3.              return false;  
  4.          ...  
  5.      }  
  鉴于之上所述,该条目中给出了重载equals方法的最佳逻辑:
      1.    使用==操作符检查"参数是否为这个对象的引用",如果是则返回true。由于==操作符是基于对象地址的比较,因此特别针对拥有复杂比较逻辑的对象而言,这是一种性能优化的方式。
      2.    使用instanceof操作符检查"参数是否为正确的类型",如果不是则返回false。
      3.    把参数转换成为正确的类型。由于已经通过instanceof的测试,因此不会抛出ClassCastException异常。
      4.    对于该类中的每个"关键"域字段,检查参数中的域是否与该对象中对应的域相匹配。
      如果以上测试均全部成功返回true,否则false。见如下示例代码:  
  1. @Override public boolean equals(Object o) {  
  2.          if (o == this)   
  3.              return true;  
  4.            
  5.          if (!(o instanceof MyType))  
  6.              return false;  
  7.                
  8.          MyType myType = (MyType)o;  
  9.          return objField.equals(o.objField) && intField == o.intField   
  10.              && Double.compare(doubleField,o.doubleField) == 0   
  11.              && Arrays.equals(arrayField,o.arrayField);  
  12.      }  
 从上面的示例中可以看出,如果域字段为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方法也需要被覆盖。如果将会导致该类无法和基于散列的集合一起正常的工作,如HashMapHashSet。来自JavaSE6的约定如下:
      1.    在应用程序执行期间,只要对象的equals方法的比较操作所用到的信息没有被修改,那么对这同一个对象多次调用,hashCode方法都必须始终如一地返回同一个整数。在同一个应用程序的多次执行过程中,每次执行所返回的整数可以不一致。
      2.    如果两个对象根据equals(Object)方法比较是相等的,那么调用这两个对象中任意一个对象的hashCode方法都必须产生同样的整数结果。
      3.    如果两个对象根据equals(Object)方法比较是不相等的,那么调用这两个对象中任意一个对象的hashCode方法,则不一定要产生不同的整数结果。但是程序员应该知道,给不相等的对象产生截然不同的整数结果,有可能提高散列表的性能。
      如果类没有覆盖hashCode方法,那么Object中缺省的hashCode实现是基于对象地址的,就像equalsObject中的缺省实现一样。如果我们覆盖了equals方法,那么对象之间的相等性比较将会产生新的逻辑,而此逻辑也应该同样适用于hashCode中散列码的计算,既参与equals比较的域字段也同样要参与hashCode散列码的计算。见下面的示例代码: 
  1. public final class PhoneNumber {  
  2.          private final short areaCode;  
  3.          private final short prefix;  
  4.          private final short lineNumber;  
  5.          public PhoneNumber(int areaCode,int prefix,int lineNumber) {  
  6.              //做一些基于参数范围的检验。  
  7.              this.areaCode = areaCode;  
  8.              this.prefix = prefix;  
  9.              this.lineNumber = lineNumber;  
  10.          }  
  11.          @Override public boolean equals(Object o) {  
  12.              if (o == this)  
  13.                  return true;  
  14.              if (!(o instanceof PhoneNumber))   
  15.                  return false;  
  16.              PhoneNumber pn = (PhoneNumber)o;  
  17.              return pn.lineNumber = lineNumber && pn.prefix == prefix && pn.areaCode = areaCode;  
  18.          }  
  19.      }  
  20.      public static void main(String[] args) {  
  21.          Map<PhoneNumber,String> m = new HashMap<PhoneNumber,String>();  
  22.          PhoneNumber pn1 = new PhoneNumber(707,867,5309);  
  23.          m.put(pn1,"Jenny");  
  24.          PhoneNumber pn2 = new PhoneNumber(707,867,5309);  
  25.          if (m.get(pn) == null)  
  26.              System.out.println("Object can't be found in the Map");  
  27.      }  

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

  1. @Override public int hashCode() {  
  2.          int result = 17;  
  3.          result = 31 * result + areaCode;  
  4.          result = 31 * result + prefix;  
  5.          result = 31 * result + lineNumber;  
  6.          return result;  
  7.      }  

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

  1. public final class PhoneNumber {  
  2.          private final short areaCode;  
  3.          private final short prefix;  
  4.          private final short lineNumber;  
  5.          private final int myHashCode;  
  6.          public PhoneNumber(int areaCode,int prefix,int lineNumber) {  
  7.              //做一些基于参数范围的检验。  
  8.              this.areaCode = areaCode;  
  9.              this.prefix = prefix;  
  10.              this.lineNumber = lineNumber;  
  11.              myHashCode = 17;  
  12.              myHashCode = 31 * myHashCode + areaCode;  
  13.              myHashCode = 31 * myHashCode + prefix;  
  14.              myHashCode = 31 * myHashCode + lineNumber;  
  15.          }  
  16.          @Override public boolean equals(Object o) {  
  17.              if (o == this)  
  18.                  return true;  
  19.              if (!(o instanceof PhoneNumber))   
  20.                  return false;  
  21.              PhoneNumber pn = (PhoneNumber)o;  
  22.              return pn.lineNumber = lineNumber && pn.prefix == prefix && pn.areaCode = areaCode;  
  23.          }  
  24.          @Override public int hashCode() {  
  25.              return myHashCode;  
  26.          }  
  27.      }  

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

十、始终要覆盖toString 

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

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

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