Effective Java 第三章 对于所有对象都通用的方法

第三章 对于所有对象都通用的方法

   尽管Object是一个具体类,但设计它主要是为了扩展。它所有的非final方法(equals, hashCode,toString,clone和finalize)都有明确的通用约定(general contract),因为它们是被设计成被覆盖的(orverride)的。任何一个类,它在覆盖这些方法的时候,都有责任遵守这些通用约定;如果不能做到这一点,其他依赖于这些约定的类(例如HashMap和HashSet)就无法结合该类一起正常运作。
   本章将讲述何时以及如何覆盖这些非final的Object方法。本章不再讨论finalize方法,因为在第8条以及讨论过这个方法了。而comparable,compareTo虽然不是Object方法,但是本章也将对它们进行讨论,因为它具有类似的特征。

第10条 覆盖equals时请遵守通用约定

   覆盖equals方法看起来似乎很简单,但是有许多覆盖方法会导致错误,并且后果非常严重。最容易避免这类问题的办法就是不覆盖equals方法,在这种情况下,类的每个实例都只有它自身相等。如果满足了以下任何一个条件,这就正是所期望的结果:
类的每个实例本质上都是唯一的。 对于代表活动实体而不是值(value)的类来说确实如此。例如,Thread。Object提供的equasl实现对于这些类来说正是正确的行为。
类没有必要提供“逻辑相等”的测试功能。 例如,java.util.regex.Pattern可以覆盖equals,以检查两个Pattern实例是否代表同一个正则表达式,但是设计者并不认为客户需要或者期望这样的功能。在这类情况下,从Object继承得到的equals实现就已经足够了。
超类已经覆盖了equals,超类的行为对于这个类也是合适的。 例如,大多数的Set实现都从AbstractSet继承equals实现,List实现AbstractList继承equasl实现,Map实现从AbstractMap继承equals实现。
类是私有的,或者是包级私有的,可以确定它的equals方法永远不会被调用。 如果你非常想要规避风险,可以覆盖equals方法,以确保它不会被意外调用。

@Override
public boolean equals(Object o) {
   throw new AssertionError(); // Method is never called
}

   那么,什么时候应该覆盖equals方法呢?如果类具有自己特有的“逻辑相等”(不同于对象等同的概念)概念,而且超类还没有覆盖equals。这通常属于“值类”的情形。值类仅仅是一个表示值的类,例如Integer和String。程序员再利用equals方法来比较值对象的引用时,希望知道它们在逻辑上是否相等,而不是想了解它们是否指向同一个对象。为了满足程序员的要求,不仅必须覆盖equals方法,而且这样做也使得这个类的实例可以被用作映射表的建(map的key),或者集合(set)的元素,使映射或集合表现出预期的行为。
   有一种值类不需要覆盖equals方法。即用实例受控确保“每个值至多只存在一个对象”的类。枚举类型就属于这种类。对于这样的类而言,逻辑相同与对象相同是一回事,因此Object的equals方法等同于逻辑上的equals方法。
   在覆盖equals方法的时候,必须要遵守它的通用约定。下面是约定的内容,来着Object规范。
   equals方法实现了等价关系,其属性如下:

  • 自反性:对于任何非null的引用值x, x.equals(x)必须返回true
  • 对称性:对于任何非null的引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true
  • 传递性:对于任何非null的引用值x、y和z,如果x.equals(y)返回true,并且 y.equals(z)返回true,那么x.equals(z)必须返回true
  • 一致性:对于任何非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)就会一致的返回true,或者一致的返回false
  • 对于任何非null的引用值x,x.equals(null)必须返回false
       除非你对数学特别感兴趣,否则这些规定看起来可能有点让人感到恐惧,但是决定不要忽视这些规定!如果违反了,就会发现程序将会变现的不正常,甚至崩溃,而且很难找到失败的根源。用Hohn Donne的话说,没有哪个类是孤立的。一个类的实例通常会被频繁的传递给另一个类的实例。有许多类,包括所有的集合类在内,都依赖于传递给它们的对象是否遵守了equals约定。
       现在你已经知道了违反equals约定又多可怕,下面将更细致的讨论这些约定。值得欣慰的是,这些约定虽然看起来吓人,实际上并不复杂。一旦理解了这些约定,要遵守它们并不困难。
       那么什么等价关系呢?不严格的说,它是一个操作符,将一组元素划分到其元素与另一个元素等价的分组中。这些分组被称作等价类。从用户的角度来看,对于有用的equals方法,每个等价类中的所有元素是可交换的。现在我们按照顺序逐一查看以下5个要求。
    自反性第一个要求仅仅说明对象必须等于其自身。很难想象会无意识的违反这一条。加入违背了这一条,然后把该类的实例添加到集合中,该集合的contains方法将果断告诉你,该集合不包含刚添加的实例。
    对称性第二个要求是说,任何两个对象对于“它们是否相等”的问题都必须保持一致。与第一个要求不同,若无意识中违反这一条,这种情形倒是不难想象。例如下面的类,它实现了一个区分大小写的字符串。字符串由toString保存,但在equals操作中被忽略。
public final class CaseInsensitiveString {
    
    private final String s;
    
    public CaseInsensitiveString(String s){
        this.s = Objects.requireNonNull(s);
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof CaseInsensitiveString) {
            return s.equalsIgnoreCase(
                    ((CaseInsensitiveString) obj).s);
        }
        if (obj instanceof String) {
            return s.equalsIgnoreCase((String) obj);
        }
        return false;
    }
}

   在这个方法中,equals方法的意图非常好,它企图与普通的字符串对象进行互操作。假设我们有一个不区分大小写的字符串和一个普通的字符串:

CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
String s = "polish";

   不出所料,cis.equals(s)返回true。问题在于,虽然CaseInsensitiveString类中的equals方法知道普通的字符串对象,但是,String类的equals方法却并不知道不区分大小写的字符串。因此,s.equals(cis)返回false。显然违反了对象性。假设你把不区分大小写的字符串对象放到一个集合中:

List<CaseInsensitiveString> list = new ArrayList();
list.add(cis);

   此时list.contains(s)会返回什么结果呢?没人知道。在当前的OpenJDK实现中,它碰巧返回false,但这只是这个特定实现得出的结果而已。在其他的实现中,它有可能返回true,或者抛出一个运行时异常。一旦违反了equals约定,当其他对象面对你的对象时,你完全不知道这些对象的行为会怎么样。
   为了解决这个问题,只需把企图与String对象互操作的这段代码从equals方法中去掉就可以了。这样做之后,就可以重构该方法,使它变成一条单独的返回语句:

 @Override
public boolean equals(Object obj) {
  return obj instanceof CaseInsensitiveString
    && ((CaseInsensitiveString) obj).s.equalsIgnoreCase(s);
}

传递性   equals 方法约定的第三个要求是,如果一个对象等于等二个对象,而第二个对象又等于第三个对象,则第一个对象一定等于第三个对象。同样的,无意识的违反这条规则的情形也不难想象。用子类举个例子。假设它将一个新的值组件添加到了超类中。换句换说,子类增加的信息会影响equals的比较结果。我们首先以一个简单的不可变的二维整数型Point类作为开始:

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 obj) {
		if (! (obj instanceof Point)) {
			return false;
		}
		Point p = (Point) obj;
		return p.x == x && p.y = y;
	}
}

   假设你想要扩展这个类,为一个点添加颜色信息:

public class ColorPoint extends Point {
	
	private final Color color;

	public ColorPoint(int x, int y, Color color) {
		super(x, y);
		this. color = color;
	}
}

   equals 方法会是怎么样的呢?如果完全不提供equals方法,而是直接从Point继承过来,在equals方法作比较的时候颜色信息就被忽略掉了。虽然这样做不会影响equals约定,但很明显这样做是无法接受的。假设编写了一个equals方法,只有当它的参数是另一个有色点,并且具有同样的位置和颜色时,它才会返回true:

@Override
public boolean equals(Object obj) {
	if (! (obj instanceof ColorPoint)) {
		return false;
	}
	return super.equals(obj) && ((ColorPoint) obj).color = color;
}

   这个方法的问题在于,在比较普通点和有色点,以及相反的情形时,可能会得到不同的结果。前一种比较忽略了颜色信息,而后一种比较总是返回false,因为参数的类型不正确。为了直观的说明问题所在,我们创建一个普通点和一个有色点:

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

   然后,p.equals(cp)返回true,cp.equals§返回false。你可以这样做这样的尝试来修正这个问题,让ColorPoint.equals在进行“混合比较”时忽略颜色信息:

@Override
public boolean equals(Object obj) {
	if (! (obj instanceof Point)) {
		return false;
	}
	if (! (obj instanceof ColorPoint)) {
		return obj.equals(this);
	}
	return super.equals(obj) && ((ColorPoint) obj).color = color;
}

   这种方法确实提供了对称性,但是却牺牲了传递性:

ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.BULE);

   此时,p1.equals(p2)和p2.equals(p3)都返回true,但是p1.equals(p3)则返回false,很显然这违反了传递性。前两种比较不考虑颜色信息,而第三种则考虑了颜色信息。
   此外,这种方法还可能导致无限递归问题:假设Point有两个子类,如ColorPoint和SmellPoint,它们各自带有这种equals方法。那么对myColorPoint.equals(mySmellPoint)的调用将会抛出StackOverFlowError异常。
   那该怎么解决呢?事实上,这是面向对象语言中关于等价关系的一个基本问题。我们无法在扩展可实例化的类的同事,既增加新的值组件,同时又保留equals约定,除非愿意放弃面向对象的抽象所带来的优势。
   你可能听说过,在equals方法中用getClass测试代替instanceof测试,可以扩展可实例化的类和增加新的值组件,同时保留equals约定:

@Overried
public boolean equasl(Object obj) {
	if (ojb == null || obj.getClass() != getClass()) {
		return false;
	}	
	Point p = (Point) obj;
	return p.x = && p.y == y;
}

   这段程序只有当对象具有相同的实现类时,才能使对象等同。虽然这样也不算太糟糕,但结果却是无法接受的:Point子类的实例仍然是一个Point,它仍然需要发挥作用,但是如果采用了这种方法,它就无法完成任务!假设我们要编写一个方法,以检验某个点是否处在单位圆中。下面是可以采用的其中一种方法:

private static final Set<Point> unitCircle = Set.of(
			new Point(1, 0), new Point(0, 1), 
			new Point(-1, 0), new Point(0, -1);
public static boolean onUnitCircle(Point p) {
	return unitCircle.contans(p);
}

   虽然这可能不是实现这种功能的最快方式,不过它的效果很好。但是假设你通过某种不添加值组件的方式扩展了Point,例如让它的构造器记录创建了多少个实例:

public class CounterPoint extends Point {
	private static final AtomicInteger counter = new AtomicInteger();
	public CounterPoint(int x, int y) {
		super(x, y);
		counter.incrementAndGet();
	}
	public static int numberCreated() {
		return counter.get();
	}
}

   里氏替换原则认为,一个类型的任何重要属性也将适用于它的子类型,因此为该类型编写的任何方法,在它的子类型上也应该同样运行的很好。针对上述Point的子类(如CounterPoint)仍然是Point,并且必须发挥作用的例子,这个就是它的正式语句。但是假设我们将CounterPoint实例传给了onUnitCircle方法。如果Point类使用了基于getClass的equals方法,无论Counter实例的x和y值是什么,onUnitCircle方法都会返回false。这是因为像onUnitCircle方法所用的HashSet这样的集合,利用equals方法检验包含条件,没有任何CounterPoint实例与任何Point对应。但是,如果在Point上使用适当的基于instanceof的equals方法,当遇到CounterPointe时,相同的onUnitCircile方法就回工作的很好。
   虽然没有一种令人满意的办法可以既扩展不可实例化的类,又增加值组件,但还是有一种不错的权宜之计:遵守第18条“复合优先于继承”的建议。我们不再让ColorPoint继承Point,而是在ColorPoint中加入一个私有的Point域,以及一个公有的视图(view)方法,此方法返回一个与该有色点处在相同位置的普通Point对象:

public class ColorPoint {
	private final Point point;
	private final Color color;
	
	public ColorPoint(int x, int y, Color color) {
		point = new Point(x, y);
		this.color = Objects.requireNonNull(color);
	}
	
	public Point asPoint() {
		return point;
	}

	@Override
	public boolean equals(Object obj) {
		if (! (obj instanceof ColorPoint)) {
			return false;
		} 
		ColorPoint cp = (ColorPoint) obj;
		return cp.point.equals(point) && cp.color.equals(color);
	}
}

   在Java平台类库中,有一些类扩展了可实例化的类,并添加了新的值组件。例如,java.sql.Timestamp对java.util.Date进行了扩展,并增加了nanoseconds域。Timestamp的equals实现确实违反了对称性,如果TImestamp和Date对象用于同一个集合中,或者以其他方法被混合在一起,则会引起不正确的行为。Timestamp类哟一个免责声明,告诫程序员不要混合使用Date和Timestamp对象。只要你不把它们混合在一起,就不会有麻烦,除此之外没有其他的措施可以防止你这么做,而且结果导致的错误将很难调试。Timestamp类的这种行为是个错误,不值得效仿。
   注意,你可以在一个抽象类的子类中增加新的值组件且不违反equals约定。对于根据第23条的建议而得到的那种类层次结构来说,这一点非常重要。例如,你可能有一个抽象的Shape类,它没有任何值组件,Circle子类添加了一个redius域,Rectangle子类添加了length和width域。只要不可能直接创建超类的实例,前面所述的种种问题就都不会发生。
一致性equals约定的第四个要求是,如果两个对象相等,它们就必须始终保持相等,除非它们中有一个对象(或者两个都)被修改了。换句话说,可变对象在不同的时候可以与不同的对象相等,而不可变对象则不会这样。当你在写一个类的时候,应该仔细考虑它是否应该是不可变的。如果认为它应该是不可变的,就必须保证equals方法满足这样的限制条件:相等的对象永远相等,不相等得的对象永远不相等。
   无论类是否是不可变的,都不要使equals方法依赖于不可靠的资源。如果违反了这条禁令,要想满足一致性的要求就十分困难。例如,java.net.URL的equals方法依赖于对URL中主机IP地址的比较。将一个主机名转变成IP地址可能需要访问网络,随着时间的推移,就补鞥呢确保会产生相同的结果,既有可能IP地址发生了改变。这样会导致URL equals方法违反的equals约定,在实践中有可能引发一些问题。URL equals方法的行为是一个大错误并且不应该被模仿。遗憾的是,因为兼容性的要求,这一行为无法被改变。为了避免发生这种问题,equals方法应该对驻留在内存中的对象执行确定性的计算。
非空性最后一个要求没有正式名称,我姑且称它为“非空性”,意思是指所有的对象都不能等于null。尽管很难想象在什么情况下o.equals(null)调用会意外的返回true,但是意外抛出NullPointrException异常的情形却不难想象。通过约定不允许抛出NullPointerException异常。许多类的equals方法都通过一个显示的null测试来防止这种情况:

@Overried
public boolean equals(Object obj) {
	if (obj == null) {
		return false;
	}
}

   这项测试是不必要的。为了测试其参数的等同性,equals方法必须先把参数转换成适当的类型,以便可以调用它的访问方法,或者访问它的域。在进行转换之前,equasl方法必须使用instanceof操作符,检查其参数的类型是否正确:

@OVerride
public boolean equals(Object obj) {
	if (! (obj instanceof myType)) {
		return false;
	}
	myType mt = (MyType) obj;
}

   如果漏掉了这一步的类型检查,并且传递给equals方法的参数又是错误的类型,那么equals方法将会抛出ClassCaseException异常,这就违反了equals预定。但是,如果instanceof的第一个操作数为null,那么,不管第二个操作数是哪种类型,instanceof操作符都指定应该返回false。因此,如果把null传给equals方法,类型检查就会返回false,所以不需要显示的null检查。
   结合所有这些要求,得出了以下实现高质量equals方法的诀窍:
   1. 使用操作符检查“参数是否为这个对象的引用”。如果是,则返回true。这只不过是一种性能优化,如果比较操作有可能很昂贵,就值得这么做。
   2. 使用instanceof操作符检查“参数是否为正确的类型”。如果不是,则返回false。一般来说,所谓“正确的类型”是指equals方法所在的那个类。某些情况下,是指该类所实现的某个接口。如果类实现的接口改进了equasl约定,允许在实现了该接口的类之间进行比较,那么就是用该接口。集合接口如Set、List、Map和Map.Entry具有这样的特性。
   3. 把参数转换成正确的类型。因为转换之前进行过instanceof测试,所以确保会成功。
   4. 对于该类中的每个“关键”域,检查参数中的域是否与该对象中对应的域相匹配。如果这些测试全部成功,则返回true;否则返回false。如果第2步中的类型是个接口,就必须通过接口方法访问参数中的域;如果该类型是个类,也许就能够直接访问参数中的域,这要取决于它们的可访问性。
   对于既不是float也不是double类型的基本类型域,可以使用==操作符进行比较;对于对象引用域,可以递归的调用equals方法;对于float域,可以使用静态Float.compare(float, float)方法;对于double域,则使用Double.compare(double, double)。对float和double域进行特殊的处理是有必要的,因为存在着Float.NaN、-0.0f以及类似的double常量;详细信息请参考JLS 15.21.1或者Float.equals的文档。虽然用静态方法Float.equals和Double.equals对float和double域进行比较,但是每次比较都要进行自动装箱,这会导致性能下降。对于数组域,则要把以上这些指导原则应用到每一个元素上。如果数组域中的每个元素都很重要,就可以使用其中一个Arrays.equals方法。
   有些对象引用域包含null可能是合法的,所以,为了避免可能导致NullPointerException异常,则使用静态方法Objects.equals(object, object)来检查这类域的等同性。
   对于有些类,比如前面提到的CaesInsensitiveString类,域的比较要比简单的等同性测试复杂的多。如果是这种情况,可能希望保存该域的一个“范式”,这样equals方法就可以根据这些范式进行低开销的精确比较,而不是高开销的非精确比较。这种方法对于不可变类是最为合适的;如果对象可能发生变化,就必须使其范式保持最新。
   域的比较顺序可能影响equals方法的性能。为了获得最佳的性能,应该最先比较最有可能不一致的域,或者是开销最低的域,最理想的情况是两个条件同时满足的域。不应该比较那些不属于对象逻辑状态的域,例如用于同步操作的Lock域。也不需要比较衍生域,因为这些域可以由“关键域”计算获得,但是这样做有可能提高equals方法的性能。如果衍生域代表了整个对象的综合描述,比较这个域可以节省在比较失败时去比较实际数据所需要的开销。例如,假设有一个Polygon类,并缓存了该面积。如果啷个多边形有着不同的面积,就没有必要去比较它们的边和顶点。
   在编写equals方法之后,应该问自己三个问题: 它们是否是对称的、传递的、一致的?并且不要只是自问,还要编写单元测试来检验这些特性,除非用AutoValue生成equals方法,在这种情况下就可以放心的省略测试。如果答案是否定的,就要找出原因,再相应的修改equals方法的代码。当然,equals方法也必须满足其他两个特性,但是这两种特性通常会自动满足。
   根据上面的诀窍构建equals方法的具体例子,请看下面这个简单的PhoneNumber类:

public final class PhoneNumber {

    private final short areaCode, prefix, lineNum;

    public PhoneNumber(int areaCode, int prefix, int lineNum) {
        this.areaCode = rangeCheck(areaCode, 999, "area code");
        this.prefix = rangeCheck(prefix, 999, "prefix");
        this.lineNum = rangeCheck(lineNum, 999, "line num");
    }

    private static short rangeCheck(int val, int max, String arg) {
        if (val < 0 || val > max) {
            throw new IllegalArgumentException(arg + ":" + val);
        }
        return (short) val;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == this) {
            return true;
        }
        if (!(obj instanceof PhoneNumber)) {
            return false;
        }
        PhoneNumber pn = (PhoneNumber) obj;
        return pn.lineNum == lineNum
                && pn.prefix == prefix
                && pn.areaCode == areaCode;

   下面是最后的一些告诫:
覆盖equals时总要覆盖hashCode。
不要企图让equals方法过于智能。如果只是简单的测试域中的值是否相等,则不难做到遵守equals约定。如果想过度的寻求各种等价关系,则很容易陷入麻烦之中。把任何一种别名形式考虑到等价的范围内,往往不是个好主意。例如,File类不应该试图把指向同一个文件的符号链接当做相等的对象来看待。所以File类没有这样做。
不要将equals声明中的Object对象替换为其他的类型。程序员编写出下面这样的equals方法并不鲜见,这会使程序员花上数个小时都搞不清为什么它不能正常工作:

public boolean equals(myClass obj) {
	// ....
}

   问题在于,这个方法并没有覆盖Object.equals,因为它的参数应该是Object类型,相反,它重载了Object.equals。在正常equals方法的基础上,再提供一个“强类型”的equals方法,这是无法接受的,因为会导致子类中的Override注解产生错误的正值,带来错误的安全感。
   @Override注解的用法一直,就如本条目中所示,可以防止放这种错误。这个equals方法不能编译,错误消息会告诉你到底哪里出了问题:

// Still broken, but won't compile
@Override
public boolean equals(myClass obj) {
	// ....
}

   编写和测试equals(及hashCode)方法都首十分繁琐的,得到的代码也很繁琐。代替手工编写和测试这些方法的最佳途径,是使用Google开源的AutoValue框架,它会自动替你生成这些方法,通过类中的单个注解就能触发。在大多数情况下,AutoValue生成的方法本质上与你亲自编写的方法是一样的。
   IDE也有工具可以生成equals和hashCode方法,但得到的源代码比使用AutoValue的更加冗长,可读性也更差,它无法自动追踪类中的变化,因此需要进行测试。也就是说,让IDE生成equals(及hashCode)方法,通常优于手工实现它们,因为IDE不会犯粗心的错误,但是程序员会犯错。
   总而言之,不要轻易覆equals方法,除非迫不得已。因为在许多情况下,从Object处继承的实现正是你想要的。如果覆盖equals,一定要比较这个类的所有关键域,并且查看它们是否遵守equals合约的所有五个条款。

第11条 覆盖equals时总要覆盖hashCode

   在每个覆盖了equals方法的类中,都必须覆盖hashCode方法。如果不这样做,就会违反hashCode的通用约定,从而导致该类无法结合所有基于散列的集合一起正常运作,这类集合包括HashMap和HashSet。下面是约定的内容,摘自Object规范:

  • 在应用程序的执行期间,只要对象的equals方法的比较操作所用到的信息没有被修改,那么对同一个对象的多次调用,hashCode方法都必须始终返回同一个值。在一个应用程序与另一个程序的执行过程中,执行hashCode方法所返回的值可以不一致。
  • 如果两个对象根据equals(Object)方法比较是相等的,那么调用这两个对象的hashCode方法都必须产生同样的整数结果。
  • 如果两个对象根据equals(Object)方法比较是不想等的,那么调用这两个对象中的hashCode方法,则不一定要求hashCode方法都必须产生不同的结果。但是程序员应该知道,给不相等的对象产生截然不同的整数结果,有可能提高散列表(hash talbe)的性能。
       因没有覆盖hashCode而违反的关键约定是第二条:相等的对象必须具有相等的散列码(hash code)。根据类的equals方法,两个截然不同的实例在逻辑上有可能是相等的,但是根据Object类的hashCode方法,他们仅仅是两个没有任何共同之处的对象。因此,对象的hashCode方法返回两个看起来是随机的整数,而不是根据第二个约定所要求的那样,返回两个相等的整数。
       假设在HashMap中用第10条中出现过的PhoneNumber类的实例作为键:
 Map<PhoneNumber, String> m = new HashMap<>();
 map.put(new PhoneNumber(707, 867, 5309), "Jenny");

   此时,你可能期望m.get(new PhoneNumber(707, 867, 5309))会返回“Jenny“,但它实际上返回的是null。注意,这里涉及两个PhoneNumber实例:第一个被插入HashMap中,第二个实例与第一个相等,用于从map中根据PhoneNumber去获取用户名字。由于PhoneNumber类没有覆盖hashCode方法,从而导致两个相等的实例具有不相等的散列码,违反了hashcode的约定。因此,put方法把电话号码对象存放在一个散列桶(hash bucker)中,get方法也必定会返回null,因为HashMap有一项优化,可以将与每个项相关联的散列码缓存起来,如果散列码不匹配,也就不再去检验对象的等同性。
   修正这问题非常简单,只需为PhoneNumber类提供一个适当的hashCode方法即可。那么,hashCode方法应该是什么样的呢?编写一个合法但并不好用的hashCode方法又没有任何价值。例如,下面的这个方法总是合法的,但是它永远都不应该被正式使用:

// The worst possible legal hashCode implementation - never use!
@Override 
public int hashCode() { return 42; }

   上面这个hashCode方法是合法的,因为它确保了想等的对象总是具有同样的散列码。但它也极为恶劣,因为它使得每个对象都具有想等的散列码。因此,每个对象都被映射到同一个散列桶中,使散列表退化成链表(linked list)。它使得本该线性时间运行的程序变成了以平方级时间在运行。对于规模很大的散列表而言,这会关系到散列表能否正常工作。
   一个好的散列函数通产倾向于“为不相等的对象产生不想等的散列码”。这正是hashCode约定中第三条的含义。理想情况下,散列函数应该把集合中不相等的实例均匀的分布到所有可能的int值上。要先完全达到这种理想的情形是非常困难的。幸运的是,相对接近这种情形则并不太困难。下面给出一种简单的解决办法:
   1. 声明一个 int 变量并命名为 result ,将它初始化为对象中第一个关键域的散列码 c,如步骤 2.a 中计算所示(如第 10 条所述,关键域是指影响巳quals 比较的域)
   2. 对象中剩下的每 个关键域 都完成以下步骤:
      a. 为该域计算 int 类型的散列码 c:
         ① 如果该域是基本类型,则计算 Type. hashCode(),这里的 Type 是装箱基本类型的类,与 f 的类型相对应。
         ②.如果该域是一个对象引用,并且该类的 equals 方法通过递归地调用 equals的方式来比较这个域,则同样为这个域递归地调用 hashCode 如果需要更复杂的比较,则为这个域计算一个“范式”( canonical representation ),然后针对这个范式调用 hashCode 如果这个域的值为 null 返回 (或者其他某个常数,但通常是0)。
         ③如果该域是 个数组,则要把每一个元素当作单独的域来处理 就是说,递归地应用上述规则,对每个重要的元素计算 个散列码,然后根据步骤 2.b 中的做法把这些散列值组合起来 如果数组域中没有重要的元素,可以使用一个常量,但最好不要用 如果数组域中的所有元素都很重要,可以使用Arrays hashCode 方法。
      b. 按照下面的公式,把步骤 中计算得到的散列码 合并到 result:
      resutl = 31 * result + c;
   3. 返回result。
   写完了hashCode方法之后,问问自己“想等的实例是否都具有相等的散列码”。要编写单元测试来验证你的推断(除非利用AutoValue生成 equals hashCode 方法,这样你就可放心省略这些测试) 如果相等的实例有着不相等的散列码, 要找出原因,并修正错误。
   在散列码的计算过程中,可以把衍生域排除在外。换句话说,如果已个域的值值可以根据参与计算的其 域值计算出来, 可以把这样的域排除在外。必须排除equals比较计算中没有用到的任何域,否则很有可能违反hashCode约定的第二条。
   步骤 2.b 中的乘法部分使得散列值依赖于域的顺序。例如,如果String散列函数省略了这个乘法部分,那么只是字母顺序不同的所有字符串都将会有相同的散列码。之所以选择31,是因为它是一个奇素数。如果乘数是偶数,并且乘法溢出的话,信息就回丢失,因为与2相乘等价于移位运算。使用素数的好处并不很明显,但是习惯上都是用素数来计算散列结果。31 有个很好的特性,即用移位和减法来代替乘法,可以得到更好的性能: 31 * i == (i << 5) - i。现代的虚拟机可以自动完成这种优化。
   现在我们要把上述解决办法用到PhoneNumber类中:

// Typical hashCode method
@Override
public int hashCode() {
	int result = Short.hashCode(areaCode);
	result = 31 * result + Short.hashCode(prefix);
	result = 31 * result + Short.hashCode(lineNun);
	return result;
}

   因为这个方法返回的结果是一个简单、确定的计算结果,它的输入只是PhoneNumber实例中的三个关键域,因此相等的PhoneNumber实例显然都会有相等的散列码。实际上,对于PhoneNumber的hashCode实现而言,上面这个方法是非常合理的,相当于Java平台类库中的实现。它的做法非常简单,也相当便捷,恰当的把不相等的电话号码分散到不同的散列桶中。
   虽然本条目中前面给出的hashCode实现方法能够获得相当好的散列函数,但它们并不是最先进的。它们的质量堪比Java平台类库的值类型中提供的散列函数,这些方法对于绝大多数应用程序而言已经足够了。如果执意想让散列函数尽可能的不会造成冲突,请参阅Guava’s com.google.common.hash.Hashing[Guava]。
   Objects类有一个静态方法,它带有任意数量的对象,并为它们返回一个散列码。这个方法名为hash,是让你只需要编写一行代码的hashCode方法,与根据本条目前面介绍过的解决方案编写出来的相比,它的质量是与之相当的。遗憾的是,运行速度更慢一些,因为它们会引发数组的创建,以便传入输入可变的参数,如果参数中有基本类型,还需要装箱和拆箱。建议只将这类散列函数用于不太注重性能的情况。下面就是用这种方法为PhoneNumber编写的散列函数:

// One-line hashCode method - mediocre perforamnce
@Override
public int hashCode() {
	return Ojbects.hash(lineNum, prefix, areaCode);
}

   如果一个类是不可变得,并且计算散列码的开销也比较大,就应该考虑把散列码缓存在对象内部,而不是每次请求的时候,都重新计算散列码。如果你觉得这种类型的大多数对象会被用作散列键(hash keys),就应该在创建实例的时候计算散列码。否则,可以选择“延迟初始化”散列码,即一直到hashCode被第一次调用的时候才初始化。虽然我们的PhoneNumber类并不值得这样处理,但是可以通过它来说明这种方法该如何实现。注意hashCode域的初始值(在本例中是0),一般不能成功创建的实例的散列码。

// hashCode method with lazily inintialized cache hash code
private int hashCode; // Automatically initialized to 0 

@Override
public int hashCode() {
	int result = hashCode;
	if (result == 0) {
		result = Short.hashCode(areaCode);
		result = 31 * result + Short.hashCode(prefix);
		result = 31 * reesult + Short.hashCode(lineNum);
	}
	return result;
}

   不要试图从散列码计算中排除掉一个对象的关键域来提供性能。虽然这样得到的散列码运行起来可能更快,但是它的效果不见得会好,可能会导致散列表慢到根本无法使用。特别是在实践中,散列函数可能面临大量的实例,在你选择忽略的区域之中,这些实例仍然区别非常大。如果是这样,散列函数就会把所有这些实例映射到极少数的散列码上,原本应该以线性级时间运行的程序,将会以平方级的时间运行。
   这不只是一个理论问题。在Java 2发行版本之前,一个String散列函数最多只能使用16个字符,若长度少于16个字符就计算所有的字符,否则就从第一个字符开始,再整个字符串中,间隔均匀的选取样本进行计算。对于像URL这种层次状名称的大型集合,该散列函数正好表现出了这里所提到的病态行为。
   不要对hashCode方法的返回值做出具体的规定,因此客户端无法理所当然的依赖它;这样可以为修改提供灵活性。Java类库中的许多类,比如String和Integer,都可以把它们的hashCode方法返回的确切值规定为该实例值的一个函数。一般来说,这并不是个好主意,因为这样做严格的限制了:再未来的版本中改进散列函数的能力。如果没有规定散列函数的细节,那么当你发现了它的内部缺陷时,或者发现了更好的散列函数时,就可以在后面的发行版本中修正它。
   总而言之,每当覆盖equals方法时,都必须覆盖hashCode,否则程序将无法正确运行。hashCode方法必须遵守Object规定的通用约定,并且必须完成一定的工作,将不相等的散列码分配给不相等的实例。这个很容易实现,但是如果不想那么费力,也可以使用前面建议的解决办法。如第10条所述,AutoValue框架提供了很好的替代方法,可以不洗手工编写equals和hashCode方法,并且现在的集成开发环境IDE也提供了类似的部分功能。

第12条 始终要覆盖toString

   虽然Object提供了toString方法的一个实现,但它返回的字符串通常并不是用户所期望看到的。它包含类的名称,以及一个“@”符号,接着是散列码的无符号十六进制表示法,例如PhoneNumber@163b91。toString的通用约定指出,被返回的字符串应该是一个“简洁的但信息丰富,并且易于阅读的表达形式”。尽管有人认为PhoneNumber@@163b91算的上是简洁和易于阅读了,但是与707-867-5309比较起来,他还算不是信息丰富的。toString约定进一步指出,“建议所有的子类都覆盖这个方法”。这个一个很好的建议,真的!
   遵守toString约定并不像遵守equals和hashCode的约定那么重要,但是,提供好的toString实现可以使类用起来更加舒适,使用了这个类的系统也更易于调试。当对象被传递给println、printf、字符串联操作符(+)以及assert,或者被调试器打印出来时,toString方法会被自动调用。即使你永远不调用对象的toString方法,但是其他人也许可能需要。例如,带有对象引用的一个组件,在它的记录的错误消息中,可能包含该对象的字符串表示法。如果你没有覆盖toString,这条消息可能就毫无用处。
   如果为PhoneNumber提供了好的toString方法,那么要产生有用的诊断消息会非常容易:

System.out.println("Failed to connect to " + phoneNumber);

   不管是否覆盖了toString方法,程序员都将以这种方式来产生诊断消息,但是如果没有覆盖toString方法,产生的消息将难以理解。提供好的toString方法,不仅有益于这个类的实例,同样也有益于那些包含这些实例的引用的对象,特别是集合对象。打印Map时会看到消息{Jenny = PhoneNumber@163b91} 或 {Jenny = 707-869-5309},你更愿意看到哪一个?
   在实际应用中,toString方法应该返回对象中包含的所有值得关注的信息,例如上述电话号码例子那样。如果对象太大,或者对象中包含的状态信息难以用字符串来表达,这样做就有点不太实际。在这种情况下,toString应该返回一个摘要信息,例如“Manhattan residential phone directory (1487536 listings)”或者“Thread[main, 5, main]”。理想情况下,字符串应该是自描述的。(Thread例子不满足这样的要求。)如果对象的字符串表示法中没有包含对象的所有必要信息,测试失败时得到的报告就会像下面这样:

Assertion failure: expected {abc, 123}, but was {abc, 123}.

   在实现toString的时候,必须要做出一个很重要的决定:是否在文档中指定返回值的格式。对于值类,比如电话号码类、矩阵类,建议这样做。指定格式的好处是,它可以被用作一种标准的、明确的、适合人阅读的对象表示法。这种表示法可以用于输入和输出,以及用在永久适合人类阅读的数据对象中,例如CSV文档。如果你指定了格式,通常最好再提供一个相匹配的静态工厂或者构造器,以便程序员可以很容易的在对象及字符串表示法之间来回转换。Java平台类库中的许多值类都采用了这种做法,包括BigInteger、BigDecimal和绝大多数的基本类型包装类。
   指定toString返回值的格式也有不足之处:如果这个类以及被广泛使用,一旦指定格式,就必须始终统一坚持这种格式。程序员将会编写出相应的代码来解析这种字符串表示法、产生字符串表示法,以及把字符串表示法嵌入持久的数据中。如果将来的发行版本中改变了这种表示法,就会破坏它们的代码和数据,它们当然会抱怨。如果不指定格式,就可以保留灵活性,便于在将来的发行版本中增加信息,或改进格式。
   无论是否决定去指定格式,都应该在文档中明确的表明你的意图。如果要指定格式,则应该严格的这样取走。例如,下面是第11条中PhoneNumber类的toString方法:
在这里插入图片描述
   如果你决定不指定格式,那么文档注释部分也应该有如下所示的指示信息;

/**
Return a bried descripition of this potion. The exact detils of the representation are unspecified and subject to change.but the followingmay be regarded as typical: "Potion #9: type = love, smell = trupentine, look = indeia ink"
*/
@Override 
public String toString() {
	// ...
}

   对于那些依赖格式的细节进行编程或者产生永久数据的程序员,在读到这段注释之后,一旦格式被改变,则只能自己承担后果。
   无论是否指定格式,都为toString返回值中包含的所有信息体用一种可以通过编程访问的途径。例如,PhoneNumber类应该包含对areaCode、prefix和line number的访问访问。如果不这么做,就会迫使需要这些信息的程序员不得不自己去解析这些字符串。除了降低程序的性能,使得程序员们去做这些不必要的工作之外,这个解析过程也很容易出错,会导致系统不稳定,如果格式发生变化,还会导致系统崩溃。如果没有提供这些访问方法,即使你已经指明了字符串的格式是会变化的,这个字符串格式也成了事实上的API。
   在静态工具类中编写toString方法是没有意义的。也不要在大多数枚举类型中编写toString方法,因为Java已经为你提供了非常万恶米的方法。但是,在所有其子类共享通用字符串表示法的抽象类中,一定要编写一个toString方法。例如,大多数集合实现中的toString方法都是继承自抽象的集合类。
   在第10条中讨论的Google公司开源的AutoValue工具,会替你生成toString方法,大多数集成开发环境IDE也有这样的功能。这些方法都能很好的告诉你每个域的内容,但是并不特定于该类的意义。因此,比如对于上述PhoneNumber类就不适合用自动生成的toString方法,但是我们的Potion类就非常适合。也就是说,自动生成的toString方法要远远优先于继承自Object的方法,因为它无法告诉你任何关于对象值的信息。
   总而言之,要在你编写的每一个可实例化的类中覆盖Object的toString实现,除非已经在超类中已经这么做了。这样会使类使用起来更加舒适,也更易于调试。toString方法应该以美观的格式赶回一个关于对象的简洁、有用的描述。

第13条 谨慎的覆盖clone

   Cloneable接口的目的是作为一个对象的一个mixin接口,表明这样的对象允许克隆。遗憾的是,它并没有成功的达到这个目的。它的主要缺陷在于缺少一个clone方法,而Object的clone方法是受保护的。如果不借助于反射,就不能仅仅因为一个对象实现了Cloneable,就调用clone方法。即使是反射调用也有可能失败,因为不能保证该对象一定具有可访问的clone方法。尽管存在这样或那样的问题,这项设施仍然被广泛使用,因此值得我们进一步了解。本条目将告诉你,如果实现一个行为良好的clone方法,并讨论何时适合这样做,同时也简单的讨论了其他的可替代做法。
   既然Cloneable接口并没有包含任何方法,那么它到底有什么作用呢?它决定了Object中受保护的clone方法实现的行为:如果一个类实现了Clone,Object的clone方法就返回该对象的逐域拷贝,否则就抛出CloneNotSupportException异常。这是接口的一种极端非典型的用法,也不值得仿效。通常情况下,实现接口是为了表明类可以为它的客户做些什么。然而,对于Cloneable接口,它改变了超类中受保护的方法的行为。
  虽然规范中没有明确指出,事实上,实现Cloneable接口的类,是为了:提供一个功能适当的共有的clone方法。为了打到这个目的,类及其所有超类都必须遵守一个相当复杂的、不可实施的,并且基本上没有文档说明的协议。由此得到一种语言之外的机制:它无需调用构造器就可以创建对象。
  clone方法的通用约定是非常弱的,下面是来自Object规范中的约定内容:
  创建和返回该对象的一个拷贝。这个“拷贝”的精确含义取决于该对象的类。一般的含义是,对于任何对象x,表达式 x.clone != x,将会返回结果true,并且表达式 x.clone.getClass() == x.getClass() 将会返回true,但这些并不是绝对的要求。虽然通常情况下,表达式x.clone.equals(x)将会返回true,但是,这也不是一个绝对的要求。按照约定,这个方法返回的对象应该通过调用super.clone获得。如果类及其超类(Object除外)遵守这一约定,那么:x.clone.getClass() == x.getClass()。按照约定,返回的对象应该不依赖被克隆的对象。为了成功的实现这种独立性,可能需要在super.clone返回对象之前,修改对象的一个或者更多个域。
  这种机制大体上类似于自动的构造器调用链,只不过它不是强制要求的:如果类的clone方法返回的实例不是通过super.clone方法获得,而是通过调用构造器获得,编译器就不会发出警告,但是该类的子类调用了super.clone方法,得到的对象就会拥有错误的类,并阻止了clone方法的子类正常工作。如果final类的clone方法没有调用super.clone方法,这个类就没有理由去实现Cloneable接口了,因为它不依赖于Object克隆实现的行为。
  假设你希望在一个类中实现Cloneable接口,并且它的超类都提供了良好的clone方法。首先,调用super.clone方法。由此得到的对象将是原始对象功能完整的克隆。在这个类中声明的域将等同于被克隆对象中相应的域。如果每个域包含一个基本类型的值,或者包含一个指向不可变对象的引用,那么被返回的对象则可能正是你所需要的对象,在这种情况下不需要做进一步处理。例如,第11条中的PhoneNumber类正是如此,但是要注意,不可变的类永远都不应该提供clone方法,因为它只会激发不必要的克隆。因此,PhoneNumber的clone方法应该是这样的:

// Clone method for class with no reference to mutable state
@Override
public PhoneNumber clone() {
	try {
		return (PhoneNumber) super.clone();
	} catch (Exception e) {
		throw new AssertionError(); // Can't happen
	}
}

   为了让这个方法生效,应该修改PhoneNumber的类声明为实现Cloneable接口。虽然Object的clone方法返回的是Object,但这个clone方法返回的却是PhoneNumber。这么做是合理的,也是我们所期望的,因为Java支持协变返回类型。换句话说,目前覆盖方法的返回类型可以是被覆盖方法的返回类型的子类了。这样在客户端中就不必进行转换了。我们在返回结果之前,先将super.clone从Object转换成PhoneNumber,当然这种转换也是一定会成功的。
   对super.clone方法的调用应该包含在一个try-catch块中。这是因为Object声明其clone方法抛出CloneNotSupportException,这是一个受检异常(checked exception)。由于PhoneNumber实现了Cloneable接口,我们知道调用super.clone方法一定会成功。对于这个样板代码的需求表明,CloneNotSupportException应该还没有检查到。
   如果对象中包含的域引用了可变的对象,使用上述这种简单的 clone 实现可能会导致灾难性的后果 例如第 条中的 Stack类:

public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        this.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; // Eliminate obsolete reference
        return result;
    }

    // Ensure space for least one more element.
    private void ensureCapacity() {
        if (elements.length == size) {
            elements = Arrays.copyOf(elements, 2 * size + 1);
        }
    }
}

   假设你希望把这个类做成可克隆的(cloneab 如果它的 clone 方法仅仅返回 super.clone (),这样得到的 Stack 实例,在其 size 域中具有正确的值,但是它的 elements域将引用与原始 Stack 实例相同的数组。 修改原始的实例会破坏被克隆对象中的约束条件,反之亦然 很快你就会发现,这个程序将产生毫无意义的结果,或者抛出 NullPointerException 异常.
   如果调用 Stack 类中唯一的构造器,这种情况就永远不会发生。 实际上, clone 方法就是另一个 器; 必须确保它不伤害到原始的对象, 并确保正确地创建被克隆对象中的约束条件。为了使Stack类中的clone方法正常工作,它必须要拷贝的内部信息。最容易的做法是,在elements数组中递归的调用clone:

// Clone method for class with references to mutable state
    @Override
    protected Object clone() {
        try {
            Stack result = (Stack) super.clone();
            result.elements = elements.clone();
            return result;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }

   注意,我们不一定要将 elements.clone ()的结果转换成 Object [] 在数组上调clone 返回的数组,其编译时的类型与被克隆数组的类型相同 这是复制数组的最佳习惯做法。事实上,数组是 clone 方法唯一吸引人的用法。
  
  
  

第14条 考虑实现Comparable接口

  

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值