Effective Java--读书笔记(三)【待完成】

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

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

10.1 什么情况下不用覆盖equals

  • 类的每个实例本质上都是唯一的。关注的实体本体还不是里面的值的类。例如Thread
  • 类没有必要提供“逻辑相等(logical equality)”的测试功能。例如java.util.regex.Pattern可以覆盖equals,已检查两个Pattern实例是否代表同一个正则表达式。(设计者认为程序员不需要这样的功能)
  • 超类已经覆盖了equals,超类的行为对于这个类也是合适的。例如大多数Set实现了都从AbstractSet继承equals实现。
  • 类是私有的,或者包是私有的,可以确定它的equals方法永远都不会被调用。但是如果你非要规避风险,可以覆盖equals方法,以确保它不会被意外调用:
/**
 * @author yuxingang
 * date 2020-12-24  09:29
 */
public class Equals {
    private class DonWantEqual{
        @Override
        public boolean equals(Object obj) {
            System.out.println("1111");
            throw new AssertionError("断言异常");//Method is never called
        }
    }
}

断言:对参数和环境等做出判断,避免程序因不当的输入或错误的环境而产生逻辑异常。
简单来说:断言主要使用在代码开发和测试时期,用于对某些关键数据的判断,如果这个关键数据不是你程序所预期的数据,程序就提出警告或退出。

10.2 什么情况下应该覆盖equals

如果类具有逻辑相等(logical equality)概念,而且超类还没有覆盖equals。这通常属于“值类(value class)”。值类仅仅是一个表示值的类,例如:Integer或者String。就必须覆盖equals。而且这样做也使得这个类的实例可以被用作映射表(map)的键(key),或者集合(set)的元素,使得映射或者集合表现出预期的行为。

10.2.1 特殊情况

有一种值类,不需要覆盖equals方法,即实例受控(静态工厂提供的类或者枚举类型等)确保“每个字至多只存在一个对象”的类。对于枚举类型,逻辑相同和对象等同是一回事。因此没必要重写equals。

10.2.2 重写equals规范

必须遵守以下通用约定:
equals方法实现了等价关系,其属性如下:

  • 自反性:对于任何非null的引用值x,x.equals(x)必须返回true
  • 对称性:对于任何非null的引用值x和y,如果x.equals(y)返回true,那么y.equals(x)返回true
  • 传递性:对于任何非null的引用值x,y,和z。如果x.equals(y)返回true,y.equals(z)返回true,那么x.equals(z)返回true
  • 一致性:对于任何非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有修改,多次调用x.equals(y)返回值必须一样
  • 对于任何非null的引用值x,x.equals(null)必须返回false。

10.3 违反equals重写规范的可能性

10.3.1 自反性

基本上不可能违反,除非你秀逗了

10.3.2 对称性

示例:如下,它实现了区分大小写的字符串。字符串由toString保存,但在equals操作中被忽略。

/**
 * @author yuxingang
 * date 2020-12-24  10:45
 */
public final class CaseInsensitiveString {
    private final String s;
    public CaseInsensitiveString(String s){
        this.s = Objects.requireNonNull(s);
    }
    //Broken - violates symmetry!
    @Override
    public boolean equals(Object obj) {
        if(obj instanceof CaseInsensitiveString)
            return s.equalsIgnoreCase(((CaseInsensitiveString) obj).s);
        if(obj instanceof String)//One-way interoperability!
            return s.equalsIgnoreCase((String)obj);
        return false;
    }
}

测试:

        CaseInsensitiveString cis = new CaseInsensitiveString("YuXinGang");
        String s = "yuxingang";
        System.out.println("正向比较cis?s===>"+ cis.equals(s));
        System.out.println("反向比较cis?s===>"+s.equals(cis));

结果:
在这里插入图片描述
结论:问题在于上面的equal方法左边的会进行大小写处理,但是右边是不会的,它只是一个参数。所以在s.equals(cis),中s本身就是小写,在进行大小写处理的时候还是小写,而右边的参数是存在大小写的,所以返回false。一旦违反了equals约定,当其它对象面对你的对象时,你完全不知道这些对象的行为会怎么样
解决方案:

    @Override
    public boolean equals(Object obj) {
        return obj instanceof CaseInsensitiveString && ((CaseInsensitiveString)obj).s.equalsIgnoreCase(s);
    }
10.3.3 传递性【待完成】

示例:假设讲一个新的值组件添加到超类中,换句话说,子类增加的信息会影响equals的比较结果。如下是一个不可变的二维整数型Point类
Point类

/**
 * @author yuxingang
 * date 2020-12-24  10:45
 */
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 == y && p.y == y;
    }
}

这个时候想要扩展这个类,想加点颜色信息
ColorPoint类

/**
 * @author yuxingang
 * date 2020-12-24  11:31
 */
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实现

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

问题所在:在比较普通点和有色点,以及相反的情形时,可能会得到不同的结果。普通点.equals(有色点)直接忽略了颜色信息。而相反的,由于参数类型不一致,所以永远都是false

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

如上,p.equals(cp)返回true。cp.equals§返回false。我们可以在ColorPoint.equeals在进行“混合比较”时忽略颜色信息。

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof Point))
            return false;
        //if obj is a normal Point,do a color-blind comparison
        if(!(obj instanceof ColorPoint))
            return super.equals(this);
        //obj is a ColorPoint; do a full comparison
        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.BLUE);

此时,p1.equals(p2)和p2.equals(p1)返回true,但是p1.equals(p3)则返回false,这虽然保证了对称性,但是违反了传递性。前两种没有考虑颜色,但是后一种把颜色算了上去。并且这还可能会导致无限递归的可能性。例如有Point有2个子类,如果各自都带有上面的方法,就会抛StackOverflowError异常。

10.3.4 一致性

如果两个对象相等,它们就必须使用保持相等,除非它们中有对象被修改了。所以先判断对象是否可变,如果不可变,那必须的保证两个对象equals方法必须相等。

10.3.5 非空性

所有的对象都不能等于null

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

以上if测试是不必要的。为了测试其参数的等同性,equals方法必须先把参数转化成适当的类型,以便可以调用它的方法或成员变量。在进行转化之前,equals必须使用instanceof操作符,检查其参数是否是该类的对象或子类对象。
如果漏掉了instanceof检查,并且传递给equals方法的参数又是错误类型,那么equals方法将会抛出ClassCastException异常,这就违反了equals的约定。但是,如果instanceof的第一个操作数为null,那么,不管第二个操作数是什么类型,instanceof操作符都指定应该返回false,因此不需要单独的null检查,而应该用instanceof。

10.4 实现高质量equals的诀窍

1、“使用==操作符检查“参数是否为这个对象的引用”。如果是则返回true。这是一种性能优化

if(this==obj)
    return true;

2、“使用instanceof操作符检查“参数是否为正确的类型”,如果不是则返回false。所谓的正确的类型是指equals方法所在的那个类,或者是该类的父类或接口
3、把参数转化成正确的类型:因为上一步已经做过instanceof测试,所以确保转化会成功
4、对于该类的每个“关键”域,检查参数中的域是否与该对象中对应的域相匹配(其实就是比较两个对象的值是否相等了)
5、当你编写完equals方法之后,应该问自己三个问题:它是否是对称的传递的一致的?当然equals也必须满足其他两个约定(自反性、非空性)但是这两种约定通常会自动满足。

10.5 使用equals的注意点

  • 覆盖equals时总要覆盖hashCode(见第9条)
  • 不要企图让equals方法过于智能:不要想过度地去寻求各种等价关系,否则容易陷入各种麻烦
  • 不要将equals声明中的Object对象替换为其他类型,不然就不是重写equals了,而是重载了。加上@override可以避免这种错误发生
  • 使用Coogle开源的AutoValue框架,会自动生成这些方法。通过类的注释就能触发。这个框架比IDEA的好,因为IDEA的冗长,可读性差,无法自动追踪类的变化,因此需要测试。
    总结:不要轻易覆盖equals,如果覆盖,一定要比较这个类的所有关键域,并且查看它们是否遵守equals合约的五项条款

第十一条:覆盖equals时总要覆盖hashCode

在每个覆盖了equals方法的类中,都必须覆盖hashcode方法

11.1 hashCode的通用规定

  • 在程序执行期间,只要对象的equals方法比较操作所用到的信息没有被修改,那么对同一个对象的多次调用,hashCode方法都必须始终返回同一个值。在一个应用程序与另一个应用程序的执行过程中,执行hashCode方法所返回的只可以不一致。
  • 如果两个对象根据equals(Object)方法比较是相等的,那么调用这两个对象中的hashCode方法都必须产生同样的整数结果。
  • 如果两个对象根据equals(Object)方法比较是不相等的,那么调用这两个对象中的hashCode方法,则不一定要求hashCode方法必须产生不同的结果。
    注意:因为没有覆盖equals会违反通用约定第二条:相等的对象必须具有相等的散列码。

11.2 没有覆盖hashCode的后果

以如下为例

        HashMap<User, String> map = new HashMap<>();
        User user = new User("zhangsan");
        map.put(new User("zhangsan"),"zhangsan");
        System.out.println(map.get(user));//结果为null

此时你期望返回的是“zhangsan”,但它实际上返回的是null。注意这里涉及两个User实例,这两个实例逻辑相等。由于没有覆盖hashCode方法,从而导致两个相等的实例具有不同的散列码,违反了hashCode约定。因此,put方法把User对象存放在散列桶中,get方法却在另一个散列桶中查找这个User对象。即使两个实例正好被放在同一个散列桶中,get方法也必定会返回null,因为HashMap有一项优化,可以将与每个项相关联的散列码缓存起来,如果散列码不匹配,也就不再去检验对象的等同性。
解决方案:只需加上hashCode方法即可。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值