EffectiveJava(v3) - chapter2: Methods Common to All Objects

277 阅读31分钟

Methods Common to All Objects

虽然Object类是一个具体的类, 但是主要设计出来还是用来进行拓展的. 对于Object类中的一些非不变(nofinal)的方法, 如equals, hashCode, toString, clone和finalize方法, 都是设计来进行覆盖的. 但是在覆盖的同时, 子类也是需要遵循这些方法的约定. 如果 不遵守的话可能导致一些别的类(依赖这些约定的类)使用时出现问题, 如HashMap, HashSet.

本章主要讲解如何正确地覆盖这些方法, 其中finalize方法在Item8中讲解了, 不推荐使用. 另外添加了一个新的, 不是Object的方法: Comparable.compareTo方法.

Introduce

EffectiveJava 第三版读书笔记,如果各位觉得翻译的不对或者内容有误,请及时联系我,敬请斧正。原文链接.

Item 10: Obey the general contract when override equals

覆盖equals方法看起来非常简单, 但是特别容易出错, 导致的后果也是非常严重的. 最简单的方法就是不要覆盖, 这种情况下下就是单纯的比较实例对象是否相同(比较内存地址), 在以下这些情况中, 是推荐不覆盖的.

  • 每个类的实力对象都是独一无二的. 如每一个Thread对象的实例都是代表一个独一无二的对象, 代表着自己的特性, 因此Object默认的实现正是所需要的.
  • 当一个类没有需要提供逻辑的等的需求时. 如java.util.regex.Pattern本来可以覆盖该方法来提供验证两个Pattern对象是否代表同一个表达式, 但是设计者认为用户端是没有这个需求的, 也就不进行覆盖, 使用Object默认的也是可以的.
  • 如果父类已经覆盖了equals方法, 而父类覆盖的行为也适合当前类, 那么也没有必要进行覆盖. 如, Set中equals的实现方法就是继承自父类AbstractSet中的实现, List中的equals方法也是继承自父类AbstractList.
  • 如果一个类是私有的, 你可以百分百保证这个类不会被调用equals方法, 那么也是没有必要覆盖的. 甚至, 如果你为了防止别人碰巧调用了, 可以覆盖equals方法, 在内部抛出一个Error.

什么情况下需要覆盖equals方法呢? 这个类对象需要的更多的是逻辑上的等操作, 而不是实例对象的等操作(是否引用到同一个对象), 并且父类并没有覆盖equals方法. 这种类一般称为值类型. 值类型一般代表着值对象, 如Integer, String等. 程序员比较两个对象更加倾向于比较内部的值, 而不是外部的引用, 并且希望在Map中, Set中也是按照值比较来区分的话, 覆盖equals方法是一个很好的选择. 但是有一种值类型却不用遵守这个规则, 那就是枚举类型(Enum), 枚举类型通过控制实例引用, 保证一个值只有一个引用对象实例来完成这个需求, 对于这种类, Object中引用等操作和逻辑上的等操作是等价的, 所以也就没有必要覆盖equals方法.

当你覆盖equals方法时, 需要遵守如下约定:

  • Reflexive(自反性): 对于非null的对象引用x, x.equals(x)必须为true.
  • Symmetric(对称性): 对于非null的对象引用x和y, 如果 x.equals(y) 为true, 那么y.equals(x)也必须为true.
  • Transitive(传递性): 对于非null的对象引用x, y和z, 如果x.equals(y)为true, y.equals(z)为true, 那么x.equals(z)也必须为true.
  • Consistent(一致性): 对于非null的对象引用x和y, 多次调用x.equals(y)必须返回相同的值, 要么全为true, 要么全为false, 即调用equals方法的过程中不能修改x和y内部的信息.
  • null判断: 对于非null的对象引用x, x.equals(null) 必须返回 false.

上面这些要求看起来有些繁琐和吓人, 但是千万不要忽视它们. 一旦违背了其中某条, 你会发现你的程序行为会变得不正确和容易崩溃, 同时这也非常难定位到问题所在. 正如John Donne所说, 没有类是孤岛. 所有的类都会传递给别的类, 而许多类(包括所有的集合类)都依靠传递过来的类需要遵循equals的约定.

虽然这些约定看起来有点吓人, 但是当你真正理解它们的时候, 你会发现遵守它们并不难. 那么什么是等价关系呢? 简单来说, 就是将一组元素划分为许多小组, 每个小组内的元素都必须要相等, 这些小组就是等价类对象. 那如何区分呢, 则是需要依靠equals方法. 下面就详细讲解一下equals的内部约定.

Reflexive(自反性)

一个对象必须等于它自己, 很难想象如果你违背了这条准则, 会发生什么后果: 如果你将一个实例放到一个集合中去, 然而集合告诉你集合中没有这个元素.这是非常严重的.

Symmetric(对称性)

任意两个对象必须满足相等的约定. 考虑如下的覆盖函数:

//Broken - violates symmetry
public final class CaseInsensitiveString {
private final String s;

public CaseInsensitiveString(String s) {
	this.s = Objects.requireNonNull(s);
}

//Broken - violates symmetry
pubic boolean equals(Object o) {
	if (o instanceof CaseInsensitiveString)
		return s.equalsIgnoreCase((CaseInsensitiveString) o).s);
	if (o instanceof String)
		return s.equalsIgnoreCase((String) o);
	return false;
	}
}

这个方法明显违反了对称性, 如果有以下两个对象:

String str = "abolish";
CaseInsensitiveString cis = new CaseInsensitiveString("Abolish");

当我们调用cls.equals(str)时, 肯定是返回true, 但是str.equals(cls)时明显返回false. 当你将cis放入到一个List对象中时, list.contain(str) ?, 谁也不确定. 碰巧在这个JDK中返回的是false, 但是这取决于你在List中的实现(cis.equals(str), 还是相反), 在别的实现中很容易就返回true, 并且出现运行时异常. 一旦你违反了这个等价的约定, 你不会知道别的对象和你的对象比较时, 会发生什么事.

要解决这个问题也很简单, 简单排除造成困扰的判断:

pubic boolean equals(Object o) {
	return o.instanceof CaseInsensitiveString && ((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
}

Transitivity(传递性)

如果第一个对象等于第二个对象, 第二个对象等于第三个对象, 那么第一个对象等于第三个对象. 考虑一下如下这种情况:

public class Point {
private final int x;
private final int y;

public Point(int x, int y) {
	this.x = x;
	this.y = y;
}

public boolean equals(Object o) {
	if (!(o instanceof Point))
		return false;
	Point p = (Point) o;
	return p.x == x && p.y == y;
}
}

这是一个简单的二维点类, 假设你想拓展它, 添加一个新的属性:

publc class ColorPoint extends Point {
private final Color color;

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

刚开始你不打算覆盖equals方法, 但是你很快发现问题, Color属性总是被忽略, 相同x,y不同的Color的ColorPoint总是返回true, 这是不能接受的. 于是你覆盖equals方法.

//Broken - violates symmetry!
public boolean equals(Object o) {
if (!(o instanceof ColorPoint))
	return false;
retrun super.equals(o) && ((ColorPoint) o).color == color;
}

新的方法满足你的要求, 会比较三个属性. 但是却违背了对称性, Point.equals(ColorPoint)总是为true, 而反过来总是为false. 这个问题也是需要解决, 为了满足对称性, 你重写了equals方法.

//Broken - violates transitivity
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).color == color;
}

这个解决方法虽然解决了对称性: 如果是比较Point, 就单纯的比较x和y, 否则还要比较color. 但是却破坏了传递性.

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

p1.equals(p2)为true, p2.equals(p3)为true, 但是p1.equals(p3)为false. 很明显这违反了传递性. 并且这还容易导致无限循环. 如果有两个子类ColorPointSmellPoint都是有自己的单独属性, 那么比较的时候就会抛出StackOverflowError(无限循环卡在了第二步判断).

那么有什么解决方法呢? 事实上这是一个原理上的问题: 你没有办法通过继承一个类(可实例化的)来拓展值属性, 却可以遵守equals准则, 除非你放弃继承. 你可能听过通过getClass进行校验是否相同的解决方法:

//Broken - violate Liskov substitution principle
public boolean equals(Object o) {
	if (o == null || o.getClass() != getClass())
		return false;
	Point p = (Point) o;
	return p.x == x && p.y == y;
}

这限制了所有的对象只能是同一个具体Class对象, 这同样会带来严重的副作用: 任何Pointer的子类, 都无法进行功能性的比较. 也就没办法用接口编程(父类编程), 它的结果关联了具体的实现类. Liskov substitution principle中说一个对象任何重要的属性可以被所有的子类所包含, 因此任何基于这些属性的方法都应该保持一致在所有的子类中.

实际上还有一个迂回的解决方法, 那就是使用组合, 通过使用组合而不是继承可以很好的解决这个问题.

public class Pointer {
	private final Point point;
	private final Color color;
	
	public ColorPoint(int x, int y, Color color) {
		point = new Pointer(x, y);
		this.color = Objects.requireNonNull(color);
	}
	
	public Point asPoint() {
		return point;
	}
	
	public boolean equals(Object o) {
		if (!(o instanceof ColorPoint))
			return false;
		ColorPoint cp = (ColorPoint) o;
		return cp.point.equals(point) && cp.color.equals(color);
	}
	
	...
}

在Java类库中有许多类通过继承一个可实例化的类来拓展属性, 如java.sql.Timestamp拓展自java.util.Date, 然后添加了一个新的属性nanoseconds. 其中equals的方法违背了对称性, 可能导致严重的后果, 我们应该在使用时注意这一点(不要放在一起使用).

同样你可以通过继承一个不可实例化父类(抽象类)来拓展属性, 这是允许的, 且不会违背等价关系的. 因为父类没办法实例化, 也就不会违反对称性.

Consistent(一致性)

如果两个对象是否相同, 它们必须长期保持一致, 除非中间修改了对象. 这就意味着可变对象可能不一定相同(过程中可以修改), 但是不可变对象就必须保证在任何情况下都要保持等价性(无法修改).

无论一个类是否是可变的, equals方法都不能依靠不可靠的资源, 否则就很难保证维护这条要求. 如java.net.URL的equals方法依赖比较主机的IP地址, 但是获取IP地址需要网络连接, 不能保证可以一直正确获取. 所以URL的equals方法可能导致严重的后果, 应该避免使用equals方法.

Non-nullity(非空性)

任何对象都必须不为null, 即所有的对象都要不等于null. 如果我们违反了这个约定, 我们的程序可能随时抛出NullPointerException, 并且很难找到原因. 一般为了保证这个条件都是在equals方法内添加限制:

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

这是一种显式的判断, 现在也存在一些非显式的判断, 可以更加优化这个功能: 调用instanceof进行判断.

public boolean equals(Object obj) {
	if (obj instanceof XXX) {
		return false;
	}
	...
}

在instanceof函数中会自动进行空判断, 如果为空直接返回false, 并且可以对象是否为某个对象的实例.

总结

要如何实现合理的equals方法, 这里给出一些建议:

  • 使用==操作来检查传递的对象是否指向同一个对象. 如果是, 返回true. 如果比较的代价较高的话, 可以显著提高性能.

  • 使用instanceof来检查传递的对象是否是同一个实例对象. 如果不是, 返回false. 需要注意的是instanceof同样支持实现接口. 有些类实现同一个接口, 依旧可以的.

  • 对传递的对象进行转换成正确的类型. 由于上一步的保证, 这一步是肯定可以成功的.

  • 对类对象内所有重要的的域, 进行比较. 如果全部都比较通过了, 返回true, 否则返回false. 对于这些域, 如果是int, byte等原始类型(除了flaoat和double)可以直接使用==进行值比较. 而对于对象类型, 则递归地调用该对象的equals方法. 对于float和double, 可以使用Float.compare(float, float), Double.compare(double, double)进行比较, 主要是处理flaot和double存在一些特殊值(如Float.NaN, -0.0f等). 这里不推荐使用Float.equals方法和Double的equals方法, 因为这会导致自动装箱. 对于数组类型, 推荐使用Arrays.equals方法, 进行验证. 如果有些对象类型允许空, 可以使用Objects.equals(Object, Object)进行比较. 对于有些类如果比较的代价非常高, 可以存储一个规范的标准域(CanonicalForm)(如, 对对象内所有的域取哈希码), 然后比较这个域是否相同, 而不是进行昂贵的比较. 但是这一般用于不变类.

  • 当你完成equals方法的时候, 问下你自己这个方法是否满足对称性, 是否满足传递性, 是否满足一致性. 有时候一个合理的测试将会起到更好的校验效果.

同时这里有一些equals方法的注意事项:

  • 覆盖equals方法的时候, 一定要覆盖hashCode方法.

  • 不要过分强调equals, 对于一些域的判断还是非常容易实现的, 不要添加太多标示为来进行比较.

  • 不要替代equals方法中传递的Object参数. 有的人会替代成对应的实际类, 但是这是不推荐的. 首先这不会覆盖默认的equals方法(就算添加了@Override, 也会报错), 第二这就强调了比较的对象为具体的实际类, 不推荐这么使用.

有的时候我们可以借助一些工具来帮我们进行覆盖equals方法, 如Google AutoValue, Lombok等, 这些通过简单地添加一个注释就可以完成任务. 而注释生成的方法往往就是你想要的. 通过IDEs自动生成equals方法是不推荐的, 可读性不是很好, 并且没有办法动态生成(如果你修改了某个一属性, 你就需要重新生成).

总而言之, 不要覆盖equals方法, 一般来说默认的实现就是你要的. 如果你要覆盖的话, 一定要比较类中所有的域, 并且比较的时候要满足5大要求.

Item 11: Always override hashCode when you override equals.

覆盖了equals方法但是却不覆盖hashCode可能导致严重的后果. 因为这违背了hashCode方法的基本准则, 而hashCode的准则为:

  • 保持一致性, 即多次调用hashCode方法, 只要通过equals方法保持为true, 就一定要返回一样的值.

  • 如果两个对象调用equals为true, 那么调用hashCode就一定要返回一样的值.

  • 如果两个对象不相同, 不要求hashCode返回的值一定相同, 但是尽量保持.

而我们覆盖equals方法但是却不覆盖hashCode的做法, 违背了第二条要求. 如新建两个对象, 赋予一样的成员变量(满足equals方法为true), 但是调用hashCode时是调用Object的hashCode方法(比较引用地址), 肯定是返回false的. 这里用一个例子来说明危害:

Map<PhoneNumber, String> datas = new HashMap<PhoneNumber, String>();
datas.put(new PhoneNumber(172, 168, 32), "Jerry");
String name = datas.get(new PhoneNumber(172, 168, 32)); //null, 并不是 Jerry.

其中PhoneNumber就是覆盖了equals方法, 没有覆盖hashCode方法. 当我们查询的时候, 传递的是一个不同的hashCode值, 内部查询时从不同的桶进行查询(有可能一样).

解决方法也很简单, 那就是覆盖一个合适的HashCode方法. 什么样的HashCode方法是合适的呢? 这里有一个简单的HashCode方法:

public int hashCode() {
	return 42;
}

这是非常野蛮的, 但是可以使用. 因为这个方法满足了hashCode的所有限制. 但是这也可能导致严重的性能后果, 如在HashMap中根据对象的hashCode进行分桶存储, 但是由于hashCode全部相同, 就放到同一个桶里面. 导致HashMap原先的设计特性: 线性查询时间无法实现. 特别是对于包含大量数据的HashMap或者taable就可能导致无法进行查询的后果.

一个好的hashCode函数应该为每一个unequal的对象返回同一个hashCode值. 这是非常完美实现的, 但是可以近乎完美的实现, 这里提供一个实现方法:

  • int result = first.hashCode(), 初始化一个int对象result, 取第一个对象的hashCode值.

  • 然后对于剩余的每一个对象(域)取hashCode(c)进行组合.

    • result = 31 * result + c;
    • 其中一个域f是原始类型, 通过Type.hashCode(f)进行计算, Type是对应的封装型. 如: Integer.hashCode(f).
    • 如果这个域f是一个对象引用, 并且在equals中进行调用比较了, 调用对象的hashCode方法. 如果为null, 就使用0.
    • 如果域f是一个数组, 并且数组中每个元素都非常重要, 就分离出来(分别调用对应hashCode方法)使用Arrays.hashCode进行生成hashCode值, 如果数组中没有成员变量, 使用0来代替.
  • 返回result.

Item 12: Always override toString

Object提供了原始的toString方法: 对象的类名 + @ + 十六进制的哈希码(如:PhoneNumber@163b91). 这是非常令人困惑的, 也是很难理解的. toString方法本意是让我们返回一个简单而精确的描述字符串, 但这显然没有满足要求. 在Object的toString方法也显示告诉我们去重写这个方法: It is recommended that all subclasses override this method.;

虽然不像equalhashCode的限制一样, 但也是非常推荐进行重写该方法的. 不仅可以让类更加容易使用, 在调试的时候也能起到很好的作用. 如707-867-5309PhoneNumber@163b91更加直接明了, 让人理解. 并且很多时候, 都会不自觉地调用toString方法: 如传递对象给print,printf, string的级联操作(如+), assert或者debuger中输出. 这些时候都会输出对应toString()的结果, 如果我们重写了该方法, 可以带来很好的帮助.

当重写toString方法的时候, 推荐包含所有对象内重要的信息. 有些特殊情况可以不满足这个: 如果一个对象太大了, 属性过多而无法实现. 或者就是一个对象内部的信息不适合用String来进行描述. 在这些特殊情况下推荐使用一些总结的话语, 如之前的PhoneNumer可以返回Manhattan residential phone directory(1487536 listings). 一般这个总结应该是容易让人理解的.

同时在重写toString方法的时候, 你可以在toString的文档中显示表明返回的String的样式. 一旦你规定返回的样式, 那么返回的结果将会是唯一的, 没有异议的, 容易读取的. 同时一旦规定了返回的样式, 那么实现一个静态转换方法或者构造函数来接收这个样式的String进行转换也是非常好的. 就如同大多数原始类型封装类, BigInteger, BidDecimal等.

但是规定返回的样式也有一个缺点: 如果规定了样式, 那就就需要永久维护它. 如果这个类被广泛的使用, 别的程序员将会频繁使用这个方法, 甚至将String对象写入持久化层(如数据库)中去, 一旦你修改了返回的样式或者不兼容之前的格式, 那别人依赖这个方法的代码就会全部崩溃, 后果非常严重. 如果不规定返回的样式, 你会保留了修改的灵活性(自由添加修改属性).

无论你是否确定规定返回的格式, 都应该显示的在描述中说明返回的样式. 如果你规定了返回的样式, 那么这个需要更加的精确. 如Integer的toString实现方法:

/**
 * Returns a string representation of the first argument in the
 * radix specified by the second argument.
 *
 * <p>If the radix is smaller than {@code Character.MIN_RADIX}
 * or larger than {@code Character.MAX_RADIX}, then the radix
 * {@code 10} is used instead.
 *
 * <p>If the first argument is negative, the first element of the
 * result is the ASCII minus character {@code '-'}
 * ({@code '\u005Cu002D'}). If the first argument is not
 * negative, no sign character appears in the result.
 *
 * <p>The remaining characters of the result represent the magnitude
 * of the first argument. If the magnitude is zero, it is
 * represented by a single zero character {@code '0'}
 * ({@code '\u005Cu0030'}); otherwise, the first character of
 * the representation of the magnitude will not be the zero
 * character.  The following ASCII characters are used as digits:
 *
 * <blockquote>
 *   {@code 0123456789abcdefghijklmnopqrstuvwxyz}
 * </blockquote>
 *
 * These are {@code '\u005Cu0030'} through
 * {@code '\u005Cu0039'} and {@code '\u005Cu0061'} through
 * {@code '\u005Cu007A'}. If {@code radix} is
 * <var>N</var>, then the first <var>N</var> of these characters
 * are used as radix-<var>N</var> digits in the order shown. Thus,
 * the digits for hexadecimal (radix 16) are
 * {@code 0123456789abcdef}. If uppercase letters are
 * desired, the {@link java.lang.String#toUpperCase()} method may
 * be called on the result:
 *
 * <blockquote>
 *  {@code Integer.toString(n, 16).toUpperCase()}
 * </blockquote>
 **/

如果没有规定特定的返回样式, 那也应该清楚表达你的意思:

/**
 * Returns a brief description of this potion. the exact details of 
 * the representation are unspecified and subject to change,
 * but the following may be regarded as typical:
 *
 * "[Potion #9: type=love, smell=turpentine, look=india ink]"
 **/

当使用者看到这些注释的时候, 就知道这个格式可能会改变, 就不会进行格式的持久性的存储或转换.

无论你有没有规定格式, 在toString中返回的信息, 都应该提供一个显示的获取途径(Accessor). 不然的话, 程序员需要使用的时候, 需要自己手动去构造String对象, 这是非常容易出错的, 甚至导致系统崩溃. 同时不推荐为静态工具类重写toString方法, 同样也不推荐为Enum枚举类型重写该方法(Java库中已经为你准备一个很好的实现), 同时你应该在一个抽象类中去实现这个方法, 那么它的子类就可以进行共享. 如很多集合类的toString实现就是继承自父抽象类的实现.

在Google的AutoValue, lombok等自动化生成的toString方法, 一般不是完美的. 它们更加趋向于告诉别人内部的值. 而不是这个对象代表的含义. 推荐自己进行重写该方法.

总而言之, 为你每一个可实例化的对象重写toString方法, 除非父类已经重写了合适的格式. 这会让类更加容易使用和调试. 同时toString方法应该返回一个简洁漂亮的String格式.

Item 13: Override clone judiciously

Cloneable接口被设计成一个简单的声明式接口, 没有任何方法. 只是用来决定Object中的clone()方法的可用性. 如果一个对象实现了Cloneable接口, 那就说明这个对象支持克隆功能, Object's的clone()方法应该返回一个域拷贝的新对象, 否则的话调用该方法则会抛出一个CloneNotSupportedException异常.

理论上来说实际上只要一个对象实现了Cloneable接口, 那么就应该重写clone()方法来提供一个合适public的clone()方法. 这个机制是非常脆弱的, 危险的, 超语言的(创建一个对象却不调用对象的构造函数).

clone()方法的一般约定为:

x.clone() != x;							//Must be true, clone object is not the same
x.clone().getClass() == x.getClass();	//Must be true, class is the same 
x.clone().equals(x);					//Not absolute require.

一般约定调用clone()方法时, 首先调用super.clone()进行clone(有点类似构造函数). 通过这个约定, 可以保证上述的第二个约定一定可以完成. 当然你可以直接调用构造函数进行直接创建对象, 这样的话可能存在问题: 如果子类调用super.clone()方法, 返回的class和当前的克隆对象的class就不相同. 子类就不能满足第二条约定, 除非将clone()方法声明为final, 这样就不用担心(但是子类就无法重写了). 不推荐使用构造函数, 而是推荐使用super.clone(). 另外不可变的类不应该重写这个方法(防止无用拷贝). 这是一个标准的clone()函数.

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

为了让clone()函数正确执行, PhoneNumber必须实现Cloneable接口. 来保证方法调用不会抛出异常. 这里的返回的是PhoneNumber利用了Java的covariant return types, 返回的是Object的子类, 是允许的. 并且放在try语句中, 保证如果类没有实现接口的话, 报出异常. 这适用于类里面所有的变量都是原始数据类型或不变的(即final的).

对于那些存在可变变量的对象, 直接调用super.clone()将会导致严重的错误:

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 at least one more element
	private void ensureCapacity() {
		if (elements.length == size)
			elements = Arrays.copyOf(elements, 2 * size + 1);
	}
}

如果在Stack类的clone方法中直接返回super.clone()方法, 那么返回的对象拥有正确的size值, 但是在elements上却是指向同一个数组, 并没有进行拷贝. 修改原数组中对象时, 克隆的数组中也进行了变化. 这破坏了克隆的不变性.

最简单解决方法就是在clone()实现构造函数的功能, 返回一个全新的对象(为可变对象进行克隆). 因为你必须保证克隆出来的对象不会影响原来的对象.

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

注意这里的result.elements = elements.clone();, 中数组没有进行类型转换, 因为数组的clone函数会根据实际情况进行返回, 不需要进行转换(数组的拷贝特别适合克隆). 注意这里也可能存在一个问题, 那就是如果将elements设置为final的, 那这个解决方法就不能生效了(因为你无法重新赋值elements). 并且这是设计的Bug(一直存在的): Cloneable与final引用(指向可变对象)不兼容. 除非这个可变对象可以安全的分享给所有克隆对象.

并且有时候单纯对可变成员对象进行clone也不能很好的解决问题. 如当你克隆HashTable时, HashTable使用Entry[] buckets来存储对象, 而Entry为一个单链表形式.

public class HashTable  implements Cloneable {
	private Entry[] buckets = ...;
	private static class Entry {
		final Object key;
		Object value;
		Entry next;
		
		Entry(Object key, Object value, Entry next) {
			this.key = key;
			this.value = value;
			this.next = next;
		}
	}
	...
}

当你简单的使用数组的拷贝:

//Broken clone method - result in shared mutable state!
@Override
public HashTable clone() {
	try {
		HashTable result = (HashTable) super.clone();
		result.buckets = buckets.clone();
		return result;
	} catch (CloneNotSupportedException e) {
		throw new AssertionError();
	}
}

看起来很完美, buckets变成一个新的buckets存储新的对象列表. 但是内部存在一个问题, 虽然buckets中对象是新的对象. 但是对象内的链表却是指向原来的(即只修改了链表头的对象, 剩余部分并没有修改). 这样就破坏了clone的不变性, 使用的时候可能造成不确定行为.

为了解决这个问题, 那就必须处理内部的所有的链表对象. 其中一个解决方法如下:

public class HashTable  implements Cloneable {
	private Entry[] buckets = ...;
	private static class Entry {
		final Object key;
		Object value;
		Entry next;
		
		Entry(Object key, Object value, Entry next) {
			this.key = key;
			this.value = value;
			this.next = next;
		}
		
		Entry deepCopy() {
			return new Entry(key, value, next == null ? null : next.deepCopy());
		}
	}
	@Override
	public HashTable clone() {
		try {
			HashTable result = (HashTable) super.clone();
			result.buckets = buckets.clone();
			for (Entry entry: result.buckets)
				if (entry != null) 
					entry = entry.deepCopy();
			return result;
		} catch (CloneNotSupportedException e) {
			throw new AssertionError();
		}
	}
	...
}

这是一种解决方法, 并且运行时也可以达到我们的要求. 但是这里隐藏一个问题, 那就是deepCopy()方法使用了递归进行完成, 如果链表足够长的话, 就很有可能导致Stack over flow的问题. 因此改进的方法为修改递归为循环.

Entry deepCopy() {
	Entry result = new Entry(key, value, next);
	for (Entry p = result; p.next != null; p = p.next) 
		p.next = new Entry(p.next.key, p.next.value, p.next.next);
	return result;
}

虽然这解决了我们的问题, 但是这个clone方法没有我们预想的跑的那么快, 也破坏了clone方法的简单和优雅的特性.

类似构造函数, clone方法内部不能调用任何可重写的方法. 一旦你这么做了, 如果子类重写了这些方法, 会给clone函数带来不可预知的风险. 另外, 在Object的clone方法中抛出了CloneNotSupportedException, 但是如果你实现Cloneable接口, 并不需要抛出这个异常(因为并不会出现), 可以进行省略来简化使用. 如果要设计一个类用来继承, 那么推荐不实现Cloneable接口, 模仿Object的方法进行抛出异常. 由子类自行决定是否实现clone方法. 因为一旦父类实现了该接口, 那么子类也必须进行维护, 以保证兼容性. 甚至有些限制方法, 禁止子类实现该接口:

@Override
protected final Object clone() throws CloneNotSupportedException {
	throw new CloneNotSupportedException();
}

另外当你写一个线程安全的类时, 记住让clone方法进行同步, 就像别的方法一样.

总而言之, 所有实现了Cloneable接口的类都应该重写clone方法(以public的形式), 返回的类型为自己本身. 首先调用super.clone(), 然后修复需要修复的成员变量(指向可变类型的变量): 对于指向任何可变对象的引用, 对该可变变量进行深拷贝, 然后将引用指向新的拷贝. 一般的做法就是对其可变变量进行克隆, 虽然这不是最好的解决方法. 对于不可变的变量和原始类型数据, 则不需要进行修复, 但是也有一些例外, 如serial number或其他unique id, 这些虽然是不变, 仍然需要进行修复.

换句话说, 付出这么多努力来维护clone方法是必须的吗? 答案是否定的, 但是如果父类实现了Cloneable接口, 那当然没有别的选择, 自能进行维护. 否则的话, 还有一些更好的方法来实现对象拷贝. 那就是: copy constructorcopy factory. 传递一个对象, 然后拷贝发返回一个新的对象.

//Copy constructor
public Yum(Yum yum) { ... };

//Copy factory
public static Yum newInstance(Yum yum) { ... };

相比clone方法, 这种方式有很多好处:

  • 不依赖特殊的, 充满风险的创建方式(clone不调用构造函数).
  • 不和final域使用冲突.
  • 不会抛出异常
  • 不需要显式转换.
  • 可以更加参数进行自定义返回对象. 正如conversion constructorconversion factories.

使用Cloneable接口时, 需要经常想到这个方法的带来的负面影响. 用于继承的类不推荐实现该接口, final类也不推荐实现该接口. 并且作为对象的拷贝功能, 构造函数和静态工厂类往往更加合适. 最好使用该方法对象, 那一定是数组.

Item 14: Consider implementing Comparable

不像别的方法都是定义在Obect对象内, ‘public int compareTo(T o);‘, compareTo方法是定义在Comparable接口中的一个单独的方法. 这个方法有点类似equals方法, 但是作用要更大一点, 提供次序的比较. 一般来说一个对象实现了Comparable接口, 意味着这个对象的实例默认拥有次序. 如对于这类对象的数组a, 如果需要进行排序:

Arrays.sort(a);

如果需要对一个String数组进行去重和排序:

Set<String> set = new TreeSet<>();
Collections.addAll(s, args);
System.out.println(s);

上面这些数组排序和集合排序等操作都是依赖于对象中compareTO方法. 而compareTo方法的定义如下:

Compares this object with the specified object for order. Returns a negative integer, zero, or a positive integer as this object is less than, equal to, or greater than the specified object.

compareTo方法中, 使用sgn来处理返回值, 负数返回-1, 正数1, 相等0. 方法的约定为:

  • 对于所有的x和y, sign(x.compareTo(y)) == -sign(y.compareTo(x)).
  • 比较需要有传递性: if (x.compareTo(y) > 0 && y.compareTo(z) > 0) then x.compareTo(z) must >0.
  • if (x.compareTo(y) == 0), then ( sign(x.compareTo(z)) == sign(y.compareTo(z)));
  • 强烈要求 if (x.compareTo(y) == 0) then x.equals(y) == true. 如果不满足这个条件, 应该在注释中显示说明这一点. 如Note: This class has a natural ordering that is inconsistent with equals.;

compareTo方法的限制没有equals方法那么复杂, 因为equals方法面对的是所有的对象, 而compareTo一般用于相同对象之间的比较(即类相同), 一般用于内部比较. 当出现不同的类型进行比较的时候, 往往会抛出异常.

有点类似hashCode函数, 如果不遵守hashCode的约定, 就会让很多依赖hashCode的方法或者对象就会出错。 如果不遵守compareTo方法的约定, 那么很多依赖compareTo的方法和对象就会出错. 如排序的集合: TreeMap, TreeSet, 集合工具类Clollections数组工具类Arrays中的排序和搜索功能.

前三个规定有点类似equals中的限制: 对称性, 传递性, 自反性. 因此这里也存在同样的限制: 如果想通过继承一个对象来添加新的属性, 而这个对象实现了Comparable接口, 那也会破坏这三条特性(详细查看Item10), 推荐使用组合的形式完成.

最后一条限制, 强烈推荐兼容equals方法, 如果不兼容equals方法, 在一些集合类中容易出现问题. 因为默认的等价判断应该是使用equals方法, 但是有些集合类中使用的是compareTo进行替换, 如果compareTo不兼容equals方法的话, 会导致严重的后果. 如BigDecimal类中compareTo方法就不兼容equals方法, 如果往一个HashSet中添加new BigDecimal("1.0")和new BigDecimal("1.00"), 那么可以成功添加两个不同的对象. 但是如果你使用TreeSet的话, 就会只添加一个对象. 因为二者等价关系的判断是不一样的. 并且这种问题是很难发现的.

在compareTo方法中, 按照顺序比较对象内所有的成员(即递归地调用compareTo方法), 如果有一个成员对象没有实现Comparable接口或者你需要自定义排序的规则, 可以使用Comparator, 自己进行构造一个特殊的比较器进行比较.

public final class CaseInsensitiveString implements Comparable<CaseInsensitiveString> {
	public int compareTo(CaseInsensitiveString cis) {
		return String.CASE_INSENSITIVE_ORDER.compare(s, cis);
	}
	... //Remainder omitted
}

在compareTo方法中比较原始类型数据时, 推荐使用对应装箱类中的工具方法, 如Integer.compare, Float.compare等. 而不是显式的使用<>, 可以很好的提高代码的阅读性, 减少犯错机会.

如果一个对象有多个成员变量, 那么比较时候的排序就非常重要了. 一般推荐先从最重要的成员进行比较, 轮流进行比较. 如:

public int compareTo(PhoneNumber pn) {
	int result = Short.compare(areaCode, pn.areaCode);
	if (result == 0) {
		result = Short.compare(prefix, pn.prefix);
		if (result == 0)
			result = Short.compare(lineNum, pn.lineNum);
	}
	return result;
}

在Java8中, Comparator接口被广泛使用. 通过Comparator接口可以很快的构建一个良好的比较器, 虽然会带来一些性能上的损失. 在使用比较器的时候, 推荐预先构建好静态的对象(static), 并让命名简单明了.

private static final Comparator<PhoneNumber> COMPARATOR = comparingInt((PhoneNumber pn) -> pn.areaCode)
	.thenComparingInt(pn -> pn.prefix)
	.thenComparingInt(pn -> pn.lineNum);

public int compareTo(PhoneNumber pn) {
	return COMPARATOR.compare(this, pn);
}

注意这里使用了Lambda表达式, 并且后续的传递对象并没有进行类型转换(PhoneNumber), 因为JVM足够聪明可以识别. 需要注意的是, 有些Comparator使用hashCode进行比较:

static Comparator<Object> hashCodeOrder = new Comparator<>() {
	public int compare(Object o1, Object o2) {
		return o1.hashCode() -  o2.hashCode();
	}
}

这是非常危险的, 因为可能存在Integer的溢出或者浮点数(浮点数存储方式的不同). 正确的方法应该使用Integer.compare方法.

static Comparator<Object> hashCodeOrder = new Comparator<>() {
	public int compare(Object o1, Object o2) {
		return Integer.compare(o1.hashCode(), o2.hashCode());
	}
}
//Simply
static Comparator<Object> hashCodeOrder = Comparator.comparingInt(o -> o.hashCode());

总而言之, 当你实现一个值类型, 并且有敏感的次序的时候. 推荐实现Compreable接口. 这样在数组或者集合中时可以很容易被排序或者查找. 另外不要显式使用<>, 而是使用原始类型封装类的compare方法进行比较或者使用Comparator进行比较.