1. 覆盖equals方法
-
问题
在面对equals方法时,会有这样的疑问,什么时候该覆盖equals方法,什么时候不应该覆盖,也就是说覆盖equals方法的时机是什么?如果覆盖equals方法,那么应该写?
-
解决
-
覆盖equals方法的时机
覆盖equals方法看起来似乎很简单,但是有许多覆盖方式会导致错误,并且后果非常严重,最容易避免这类问题的办法就是不覆盖equals方法,在这种情况下,类的每个实例都只与它自身相等。下面这几种情况就不需要覆盖equals()方法:
- 类的每个实例本质上都是唯一的。对于代表实体而不是值(value)的类来说确实如此,例如Thread。Object提供的equals实现对于这些类来说是正确的行为;
- 不关心类是否提供了“逻辑相等(logical equality)”的测试功能。如java.util.Random覆盖了equals,以检查两个Random实例是否产生相同的随机序列,但是调用者并不期望这样的功能。在这样的情况下,从Object继承得到的equals实现已经足够了;
- 父类已经覆盖了equals,从父类继承过来的行为对于子类来说也是合适的。例如大多数Set实现都从AbstractSet继承equals实现,类似的有List和Map等;
- 类是私有的或是包级私有的,可以确定它的equals方法永远不会被调用。
-
覆盖equals方法的规范写法
在覆盖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;
编写的技巧有:
- 使用==操作符检查“参数是否为这个对象的引用”;
- 使用instanceof操作符检查“参数是否为正确的类型”;
- 经过instanceof类型检查之后把参数转换成正确的类型;
- 对于该类中的每个“关键”域,检查参数中的域是否与该对象中对应的域相匹配。对于不是double和float的基本类型,可以使用==进行比较,对于引用类型,可以递归调用equals方法,对于float域,可以使用Float.compare方法,对于double域,可以使用Double.compare方法;
- 当编写完equals方法时,应该问自己三个问题:它是否满足对称性、传递性、以及一致性;
- 覆盖equals方法总要覆盖hashCode()方法;
- 判断各个域值是否相等的逻辑不要过于复杂;
- 不要将所覆盖的equals方法中的入参Object对象替换成其他对象,应该使用@Override。
-
-
结论
当面对equals方法时,应该根据覆盖equals方法的时机去判断是否需要覆盖equals方法,如果需要覆盖equals方法时,要严格遵守equals方法的规范。
2. 覆盖equals方法同时覆盖hashCode方法
-
问题
在每个覆盖了equals方法的类中,也必须覆盖hashCode方法,如果不这样的话,就会违反了Object.hashCode的通用约定,从而导致该类无法结合所有基于散列的集合一起正常动作,比如说HashMap,HashSet,Hashtable。那么,Object.hashCode规范是什么?以及一个性能良好的hashCode应该怎样写?
-
解决
-
Object.hashCode规范
- 在应用程序的执行期间,只要对象的equals方法的比较操作所用到的信息没有被修改,那么对这同一个对象调用多次,hashCode方法必须都始终如一地返回同一个整数
- 如果两个对象根据equals(Object)方法比较是相等的。那么调用这两个对象中任意一个对象的hashCode方法都必须产生同样的整数结果。
- 如果两个对象根据equals(Object)方法比较是不相等的,那么调用这两个对象中的任意一个对象的hashCode方法,则不一定要产生不同的整数结果。
-
hashCode的写法
一个好的散列函数通常倾向于“为不相等的对象产生不相等的hashCode”,编写好的hashCode也如下这种简单的方式:
- 把某个非零的常数值,比如说17保存在一个名为result的int类型的变量中。
- 对于对象中的每个关键域f(指equals方法中涉及的每个域),完成以下步骤:
a. 为该域计算int类型的散列码c:
- 如果该域是boolean类型,则计算(f ? 1 : 0)
2). 如果该域是byte、char、short或者int类型,则计算(int)f
- 如果该域是long类型,则计算(int)(f^(f>>>32))。
- 如果该域是float类型,则计算Float.floatToIntBits(f)。
- 如果该域是double类型,则计算Double.doubleToLongBits(f),然后按照步 骤2.a.3),为得到的long类型值计算散列值。
- 如果该域是一个对象引用,并且该类的equals方法通过递归地调用equals方式来比较这个域,则同样为这个域递归地调用hashCode。如果需要更复杂的比较,则为这个域计算一个范式,然后针对这个范式调用hashCode。如果这个域的值为null,则返回0(不绝对,但通常是0)。
- 如果该域是一个数组,则要把每个元素当做单独的域来处理。也就是说,递归地应用上面的规则,对每个重要的元素计算一个散列码。然后再用2中的方法组合起来。如果数组中的每个元素都很重要,则可以用Arrays.hashCode方法。
b. 按照下面的公式,把步骤2.a计算得到的散列码c合并到result中。
result = 31 * result + c;
返回result。
-
示例
public final class PhoneNumber { private final short areaCode; private final short prefix; private final short lineNumber; @Override public int hashCode() { int result = 17; result = 31 * result + areaCode; result = 31 * result + prefix; result = 31 * result + lineNumber; return result; } }
-
-
结论
- 如果覆盖了equals方法一定要覆盖hashCode方法,否则会造成基于散列值得集合使用出现问题,如HashMap或者HashSet等;
- 不要试图从散列码计算中排除一个对象的关键部分来提高性能。虽然这样可能使计算的速度得到提升,但是效果并不见得会好,可以会导致散列表慢到根本无法使用,如果因此大量的实例映射到极少的散列码上,那基于散列的集合将会显示出平方级的性能。Java平台类库中的许多类如 String、Integer、Date,都可以把它们的hashCode方法返回确切值规定为该实例的一个函数,一般来说,这并不是一个好主意,因为这样做严格地限制了在将来的版本中改进散列函数的能力。
3. 覆盖toString方法
-
问题
Object中默认的toString方法,它返回的字符串只类类名加上一个“@符号”,后面是十六进制形式的hashCode,这些信息对我们来说用处不大,所以为了提供更好的关于类和对象的说明,我们应该总是覆盖toString()方法来提供更加清晰的说明,覆盖toString方法的好处以及覆盖toString的注意事项?
-
解决
-
覆盖toString方法的好处
toString方法虽然不会像equals这样的方法对类造成那么大的影响,但是一个好的toString可以使类用起来更加的舒服。当对象被传给println、printf、字符串联操作符(+)以及assert或者被调试器打印出来时,toString方法会被自动调用。这是一种重要的调用手段,如果不重写toString提供更明确的信息,这将很难让人理解。toString的输出,也可以方便我们debug
-
覆盖toString的注意事项
- 在实际应用中,toString方法应该返回对象中包含的所有值得关注的信息,如果对象太大或者对象中包含的状态信息难以用字符来表达,这样做就有点不切实际了,在这种情况下toString方法应该返回类的关键域信息;
- 在覆盖toString时可以指定输出格式,这样就可以编写相应的代码来解析这种字符串表示法,产生字符串表示法,以及把字符串表示嵌入到持久的数据中。但是,将来一旦输出格式变化了,会造成更大的问题。是否指定输出格式应该权衡。
-
-
总结
在实际开发过程中最好要覆盖toString方法,将类的有用信息使用toString方法进行输出,这样就可以方便调试或者打印的时候输出
4.实现comparable
-
问题
compareTo方法是Comparable接口中唯一的方法,不但允许进行简单的等同性比较,而且允许执行顺序比较。一旦实现了Comparable接口,就可以跟许多泛型方法以及依赖于该接口的集合实现类进行协作。实现CompareTo方法有哪些规范?
-
解决
使用compareTo方法有一个重要的约定,就是通常情况下compareTo方法施加的等同性测试和equals方法一致。如果不一致的话,集合接口一般是使用equals方法来进行等同性测试,而有序集合是采用compareTo方法进行等同性测试,如果两者不一致的话,容易造成灾难性的后果;
-
将对象与指定的对象进行比较。当该对象小于、等于或者大于指定对象的时候,分别返回一个负整数,零或者正整数,如果由于指定对象的类型而无法与该对象进行比较,则抛出ClassCastException。在下面的说明中,符号sgn(表达式)表示数学中的signum函数,它根据表达式(expression)的值为负值、零和正值,分别返回-1、0、1。
- 必须确保所有的x和y都满足sgn(x.compareTo(y)) == -sgn(y.compareTo(x))。这也暗示着当且仅当y.compareTo(x)抛出异常时,x.compareTo(y)才抛出异常。
- 必须确保这个比较关系是可传递的:(x.compareTo(y) > 0 && y.compareTo(z) > 0)暗示着x.compareTo(z) > 0也成立。对应着equals使用规范里面的传递性。
- 必须确保x.compareTo(y) == 0暗示着所有的z都满足sgn(x.compareTo(z)) == sgn(y.compareTo(z))。
- 强烈建议(x.compareTo(y) == 0) == (x.equals(y)),但是这个并非绝对必要。一般来说,任何实现了Comparable接口的类,若违反了这个条件,都应该明确予以说明。推荐使用这样的说法:“注意,该类具有内在的排序功能,但是与equals不一致”。
-
示例
如果一个类有多个关键域,那么比较这些关键域的顺序非常关键。必须从最关键的域开始,逐步进行到所有的重要域。如果某个域的比较产生了非零的结果(0代表着相等),则整个比较操作结束,并返回该结果。如果最关键的域是相等的,则再比较下一个关键域,以此类推,如果所有域都是相等的,那么才返回0。例如下面的例子:
public final class PhoneNumber implements Comparable { private final short areaCode; private final short prefix; private final short lineNumber; public PhoneNumber(int areaCode, int prefix, int lineNumber) { this.areaCode = (short) areaCode; this.prefix = (short) prefix; this.lineNumber = (short) lineNumber; } @Override public int compareTo(PhoneNumber 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.lineNumber) return -1; if (lineNumber > pn.lineNumber) return 1; return 0; } }
可以改进如下:
public int compareTo(PhoneNumber pn) { int areaCodeDiff = areaCode - pn.areaCode; if (areaCodeDiff != 0) return areaCodeDiff; int prefixDiff = prefix - pn.prefix; if (0 != prefixDiff) return prefixDiff; return lineNumber - pn.lineNumber; }
使用这种方法的时候需要注意,有符号的32位整数还不足以大到能够表达任意两个32位整数的差值,如果i是一个很大的正整数,j是一个很小的负整数,i-j有可能会溢出,并且返回一个负值。
-
结论
在实现Comparable接口时,应该遵守这些规范,特别是在做等同性测试的时候,要和equals等同性测试结果保持一致。