Java对象通用方法equals,hashCode,toString以及Comparable接口

总括:

1.覆盖equals方法时请遵守通用约定
2.覆盖equals时应同时覆盖hashCode方法
3.始终要覆盖toString方法
4.考虑实现Comparable接口

1.覆盖equals方法时请遵守通用约定

不用覆盖equals的情况:
(1)类的实例本质上是唯一的
(2)类没有提供“逻辑相等”的功能,object的默认实现是引用相同
(3)超类已经覆盖了equals,且超类的行为对于这个类也是合适的
(4)类是private或protected的
所以,当类具有逻辑相等的概念并且超类没有覆盖equals时应该覆盖equals方法,例如我们使用的String类便是如此。
equals方法的覆盖应使对象 满足如下约定:自反性(x=x),对称性(x=y,则y=x),传递性(若x=y且y=z,则x=z),一致性(对象是不可变的),非空性(x.equals(null)必须返回false)
实际上,我们无法在扩展可实例化类的同时既增加新的属性,同时又不破坏如上约定。这意味着,如果子类在父类的基础上扩展了新的属性,那就不要在equals方法中考虑对子类和父类对象进行比较了,那样会打破上面的规则。

实现equals方法时候的技巧:
(1)使用“==”操作符比较参数是否与这个对象的引用相同
(2)使用instanceof操作符检查“参数是否为正确的类型”,这样也同时判断了参数是否为null
(3)把参数转换成正确的类型
(4)判断相等时要检查类的“关键”域(涉及逻辑等价的属性)是否一致。有些属性允许为null,为了避免空指针异常,使用静态方法Objects.equals(Object, Object)来比较这类属性

注意:
覆盖equals方法时需要同时覆盖hashCode方法。
编写完equals方法之后一定要进行单元测试,检查是否满足上面的约定。
equals方法的参数参数声明为Object类型,如下:

public boolean equals(Object o){
    //...
}

如果改为其他类型(比如你自己的类),就成了重载而非覆盖。
手动实现equals方法常常出错,建议使用AutoValue框架自动生成。也可以使用IDE来生成,但可能要进行手动优化。
总之不要轻易覆盖equals方法,除非迫不得已。

2.覆盖equals时应同时覆盖hashCode方法

若想要将覆盖了equals方法的类用于散列集合(如HasMap,HashSet),那么必须覆盖hashCode方法。
覆盖hashCode方法应该遵循的约定:不可变对象的hashCode应该始终返回相同的值;若两个对象equals比较相等,则它们的hashCode方法返回的值也必须相同,若比较结果为不相等则hashCode不一定不同,但我们希望不相等的对象也产生不同的散列码。
实现hashCode时可以遵循如下步骤:
(1)声明int变量result并且将它初始化为对象第一个关键属性(equals比较时需要用到的属性)的散列码c
(2)对对象的每一个关键属性完成如下操作:

  • a.为该属性计算int类型的散列码:若来类型为基本类型,则调用该装箱类型的hashCode方法;若该属性为对象引用,则自行设计散列函数生成它的散列码,若为 null则为0;若属性为数组,则将数组的每个元素视为关键属性进行相同的处理,若数组元素不重要则使用一个常量(最好不为0),若每个元素都很重要,考虑使用Array.hashCode()方法
  • b.将2.a中的散列码c合并到result中,公式为:result = 31*result + c;

(3)返回result

如一个拥有三个short类型的关键属性areaCode,prefix,lineNum的类PhoneNumber,它的hashCode方法如下:

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

完成之后一定要进行测试。
当然也可以使用其他的散列函数,或者直接使用静态方法Objects.hash(),如下:

@Override
public int hashCode(){
    return Objects.hash(areaCode, prefix, lineNum);
}

但hash()方法性能较差,适用于对性能要求不高的场合。
若以各类不可变且计算散列码的开销很大,则考虑将散列码缓存在对象内部,而不是每次请求都计算散列码。
hashCode方法同样可以使用AutoValue或IDE来生成。

3.始终要覆盖toString方法

提供一个好的toString方法使类使用起来更加舒适且便于调试。当对象被传递给println、字符连接操作符+、assert时会自动调用toString方法。
toString方法应该返回对象中包含的所有值得关注的信息。
返回字符串时可以决定使用特定的格式或字符串表示法,应该在文档中表明你是否规定了特定格式。
静态工具类和大多数枚举类型中不需要编写toString方法;在所有子类共享通用字符串表示法的抽象类中,应该编写一个toString方法。

4.考虑实现Comparable接口

当类的实例具有内在的排序关系(比如按照字母/数值/日期排序)时,应该实现Comparable接口。Comparable接口有一个唯一的方法compareTo:

public interface Comparable<T> {
    int compareTo(T t);
}

实现compareTo方法同样要遵循自反性、对称性、传递性的约定。与equals一样,我们无法在扩展可实例化类的同时又不破坏这些约定。此外建议compareTo方法与equals方法具有一致性,即:(x.compareTo(y)==0) == (x.equals(y)) 。这样做的好处是可以在有序集合中使用你的类,因为有序集合的等同性测试是基于compareTo的。
compareTo不应该比较不同类型的对象,在比较不同类型的对象时,应该抛出ClassCastException异常。若参数为null,则抛出空指针异常。
compareTo方法对类中的关键属性进行顺序比较,对于类型为引用对象的属性可以递归调用compareTo进行比较。通常来说,从最重要的属性开始逐一比较关键属性,若某个属性的比较产生了非零结果,则终止整个比较过程,返回结果;若属性相等,则继续比较其他的关键属性。
还可以使用Comparator接口提供的比较器构造方法,以之前的PhoneNumber类为例,它的compareTo方法实现如下:

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

public int compareTo(PhoneNumber pn){
    return COMPARATOR.compare(this, pn);
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值