Java:Effective java学习笔记之 覆盖equals时请遵守通用约定

覆盖equals时请遵守通用约定

1、为什么要覆盖equals

因为默认equals在比较两个对象时,是看他们是否指向同一个地址。但有时,我们需要两个不同对象只要是某些属性相同就认为它们equals()的结果为true。比如:

class Person{
    String name;
    public Person(String name){
        this.name = name;
    }
}

Person aperson = new Person("a")
Person bperson = new Person("a")

我们希望的结果是aperson 等于 bperson.

解决方法

1. 使用 ==

java中 a == b 判断a 和 b 是否引用同一个object, 所以aperson == bperson会返回false, 这种方法不行

2. 覆盖euqals()

因为java中的所有class 都直接或间接地继承自 Object 类, 而Object类有一些基本的方法如 equals(), toString()等等……我们就可以覆盖其中的equals方法, 然后调用aperson.equals(bperson)来判断两者是否相等.

我的第一次实现如下

class Person{
    String name;
    public Person(String name){
        this.name = name;
    }
    @Override
    public boolean equals(Object o){
        return this.name == o.name;
    }
}

但是第8行return this.name == o.name; 处报错了 name can’t be resolved or not a field

于是我又换了种写法:

class Person{
    String name;
    public Person(String name){
        this.name = name;
    }
    @Override
    public boolean equals(Person o){
        return this.name == o.name;
    }
}

然而这次第七行public boolean equals(Person o)处又报错了The method equals(Person) of type Person must override or implement a supertype method

这两个错误的原理我不是很清楚, 等之后我弄清楚之后会更新上来

于是我只好再改个方法如下

class Person{
    String name;
    public Person(String name){
        this.name = name;
    }
    @Override
    public boolean equals(Object o){
        return this.name == o;
    }
}

这次倒是没有报错了, 不过调用的时候非常不美观, 得写成aperson.equals(bperson.name)

最终版本如下:

class Person{
    String name;
    public Person(String name){
        this.name = name;
    }
    @Override
    public boolean equals(Object o) {
        if(this == o) {
            return true;
        }
        if(!(o instanceof Person)) {
            return false;
        }
        Person person = (Person) o;
        return this.name.equals(person.name);
    }
}

终于aperson.equals(bperson)可以返回true

2、需要覆盖equals方法的时机

如果类具有自己特有的“逻辑相等”概念(不同于对象等同的概念),而且超类还没有覆盖equals以实现期望的行为,这时候我们就需要覆盖equals方法。

2.1、不需要覆盖equals方法的情况

(1)类的每个实例本质上都是唯一的。

对于代表活动实体而不是值(value)的类来说,例如Thread。Object提供的equals实现对于这些类来说是正确的行为。
在这里插入图片描述
(2)不关心类是否提供了“逻辑相等(logical equality)”的测试功能。

例如,java.util.Random覆盖了equals,以检查两个Random实例是否产生相同的随机数列,但是设计者并不认为客户需要或者期望这样的功能。在这样的情况下,从Object继承的equals实现就已经足够了。

(3)超类已经覆盖了equals,从超类继承过来的行为对于子类也是合适的。

(4)类是私有的或者包级私有的,可以确定它的equals方法永远不会被调用。

在这种情况下,无疑是应该覆盖equals的,以防止它被意外调用。这种情况下,只是对equals的一种废弃,并没有加什么新的功能。

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

2.2、需要覆盖equals方法的情况

那么,什么时候应该覆盖equals方法呢?

如果类具有自己特有的"逻辑相等"概念(不同于对象等同的概念),而且超类还没有覆盖equals方法,这通常属于值类的情形。

值类仅仅是一个表示值的类,例如:Integer或者String,程序员在利用equals方法来比较值对象的引用时,希望知道它们在逻辑上是否相等,而不是像了解它们是否指向同一个对象,不仅必须覆盖equals方法,而且这样做也使得这个类的实例可以被用作映射表(map)的键(key)或者集合(set)的元素,使映射或者集合表现出逾期的行为。
在这里插入图片描述

有一种"值类"不需要覆盖equals方法,即用实例受控确保"每个值最多只存在一个对象"的类,枚举类型就属于这种类,对于这样的类而言,逻辑相同与对象相同是一回事。
在这里插入图片描述

在覆盖equals方法的时候,必须遵守通用约定,下面是约定的内容,来自Object的规范:

  • 1、自反性:对于任何非null的引用值x,x.equals(x),必须返回true。
  • 2、对称性:对于任何非null的引用值x、y,当且仅当x.equals(y)返回true时,y.equals(x)也必须返回true。
  • 3、传递性:对于任何非null的引用值x、y、z,如果x.equals(y)返回true,y.equals(z)也返回true,那么x.equals(z)也必须返回true。
  • 4、一致性:对于任何非null引用值x、y,只要equals的比较操作在对象中所用的信息没有修改,多次调用x.equals(y)就会一致的返回true,或者一致的返回false。
  • 5、非空性:对于任何非null引用值x,x.equals(null)必须返false
2.2.1、自反性

自反性说明一个对象是等于其自身, 自己和自己相等, Object的也就实现了这个了:

publicbooleanequals(Object obj){
	return (this == obj);
}
2.2.2、对称性

简单来说, 就是a=b成立的话, 那么b=a必定成立.

来看个例子, 有一个CaseInsensitiveString类, 这个类比较的时候会忽略大小写:

final class CaseInsensitiveString{
    private final String s;
    public CaseInsensitiveString(String s) throws NullPointerException{
        if (s == null) {
            throw new NullPointerException();
        }
        this.s = s;
    }

    @Override
    public 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;
    }
}

覆盖了equals, 判断s与CaseInsensitiveString类的s或者和String忽略大小后是否相等, 否则就返回false, 然后我们来测试一下

CaseInsensitiveString cis1 = new CaseInsensitiveString("boom!");
CaseInsensitiveString cis2 = new CaseInsensitiveString("Boom!");
String s = "BoOM!";
System.out.println(cis1.equals(cis2));
System.out.println(cis1.equals(s));

输出正如我们所料, 都是true, 但是别忘了自反性啊, cis1.equals(s)输出true, s.equals(cis1)也应该是输出true的, 事实上s.equals(cis1)的结果是false. 显然违反了对称性, String类的equals并不知道不区分大小写的CaseInsensitiveString类, 因此s.equals(cis1)返回了false.

为了解决这个问题, 只要将企图与String互操作的那段代码从equals中删除就行了

@Override
    public boolean equals(Object o) {
        return o instanceof CaseInsensitiveString 
        && ((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
    }
2.2.3、传递性

最复杂的就是传递性了, 离散中最麻烦的也是求传递闭包了.

传递性的意思也很简单的, 就是a=b, b=c的话, 那么a和c也是相等的.

有一个Point类, 用来表示坐标点

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

然后又有一个ColorPoint类, 来表示带颜色的点

class ColorPoint extends Point {
    private final Color color;
    public ColorPoint(int x, int y, Color color) {
        super(x, y);
        this.color = color;
    }
}

没有复写equals的情况下, 虽然ColorPoint和Point作比较的时候能饭后正确的结果, 但是两个ColorPoint之间做比较的时候忽略了颜色信息, 这显然不是我们想要的结果, 于是乎:

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

这样对了吗? 抱歉, 问题还是很大, 虽然实现了两个有色点之间的比较, 但是当普通点和有色点比较的时候, 违反了对称性, 普通点和有色点比较会忽略颜色, 而有色点和普通点则总是返回false.

继续改:

@Override
    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;
    }

如果o不是ColorPoint, 就用o去比较this, 这样就会忽略颜色信息了, 测试一下:

        ColorPoint p1 = new ColorPoint(1, 1, Color.RED);
        Point p2 = new Point(1, 1);
        ColorPoint p3 = new ColorPoint(1, 1, Color.GREEN);
        System.out.println(p1.equals(p2));
        System.out.println(p2.equals(p3));

返回的都是true, 很好, 对称性的问题解决了, 等等, 这里不是在讨论传递性吗!!!

按照传递性来说, p1=p2, p2=p3, 所以p1和p3肯定是相等啊, 但是这里很明显就是不相等的, 大家又不是色盲.

事实上,这是面向对象语言中关于等价关系的一个基本问题。我们无法在拓展可实例化的类的同时,既增加新的值组件,同时又保留equals约定,除非愿意放弃面向对象的抽象所带来的优势。

你可能听说,在equals方法中庸getClass()测试代替instanceof测试,可以扩展可实例化的类和增加新的值组件,同时保留equals约定:

@Override
    public boolean equals(Object o) {
        if (o == null || o.getClass() != getClass()) {
            return false;
        }
        Point p = (Point)o;
        return x == p.x && y == p.y;
    }

只有当对象相同时才 比较, 这样虽然解决了问题, 但是却不是我们想要的解决方法.

来看一个更好的实现, 用复合代替继承:

上述问题根据该计划我们不在让ColorPoint扩展Point,而是在ColorPoint中加入一个私有的Point域,以及一个公有视图(view)方法,此方法返回一个与该有色点处在相同位置的普通Point对象,最后代码是:

class ColorPoint {
    private final Color color;
    private final Point point;
    public ColorPoint(int x, int y, Color color) {
        if (color == null) {
            throw new NullPointerException();
        }
        point = new Point(x, y);
        this.color = color;
    }

    public Point asPoint() {
        return point;
    }

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

注意:你可以在一个抽象(abstract)类的子类中增加新的值组件,而不会违反equals约定。

2.2.4、一致性

如果两个对象是相等的, 就应该保持一直是相等的, 除非这两个对象中有一个或者两个都被修改了, 所以记住: 相等的对象永远相等, 不相等的对象永远不相等.

2.2.5、非空性

所有的对象都不能为null, 尽管很难想象什么情况下o.equals(null)会返回true. 但是意外抛出NullPointerException异常的可能却不难想象,

所以可以这样写来不允许抛出NullPointerException异常

@Override
public boolean equals(Object o) {
    if (o == null) {
        return false;
    }
}

3、高质量equals方法的几个注意点

1.使用 == 操作符检查”参数是否为这个对象的引用。

  • 如果是,则返回true,这只不过是一种性能优化,如果比较操作有可能很昂贵,就值得这么做。

2.使用 instanceof 操作符检查”参数是否为正确的类型。

  • 如果不是,则返回false。一般来说,所谓“正确的类型”是指equals方法所在的那个类。某些情况下,是指该类所实现的某个接口。如果类实现的接口进行了equals约定,允许在实现了该接口的类之间进行比较,那就使用接口,集合接口如Set,List,Map和Map.Entry具有这样的特性。

3.把参数转换正确的类型。

  • 因为转换之前进行过instanceof测试,所以确保会成功。

4.对于该类的每个关键域,检查参数中的域是否与该对象中的对应的域相匹配。

  • 如果这些测试全部成功,则返回true,否则返回false,如果第2步中的类型是个接口,就必须接口方法访问参数中的域,如果该类型是一个类,也许就能够直接访问参数中的域,这要取决与它们的可访问性。

当你编写完equals方法之后,应该问自己三个问题:它是否是对称的、传递的、一致的?当然equals也必须满足其他两个约定(自反性、非空性)但是这两种约定通常会自动满足

3.1、规范案例

public final class PhoneNumber {
    private final short areaCode;
    private final short prefix;
    private final short lineNumber;

    public PhoneNumber(short areaCode, short prefix, short lineNumber) {
        rangeCheck(areaCode, 999, "area code");
        rangeCheck(prefix, 999, "prefix");
        rangeCheck(lineNumber, 9999, "line number");
        this.areaCode = (short)areaCode;
        this.prefix = (short)prefix;
        this.lineNumber = (short)lineNumber;
    }
    private static void rangeCheck(int arg,int max,String name) {
        if(arg < 0 || arg > max)
            throw new IllegalArgumentException(name +": "+ arg);
    }

    @Override
    public boolean equals(Object obj) {
        //1、使用==操作符检查“参数是否为这个对象的引用”
        if(obj == this)
            return true;
        //2、使用instanceof操作符检查“参数是否为正确的类型”
        if(!(obj instanceof PhoneNumber))
            return false;
        //3、把参数转化成正确的类型
        PhoneNumber pn = (PhoneNumber)obj;
        //4、对于该类的每个“关键”域,检查参数中的域是否与该对象中对应的域相匹配(其实就是比较两个对象的值是否相等了)
        return pn.lineNumber == lineNumber
            && pn.prefix == prefix
            && pn.areaCode == areaCode;
    }
}

4、除了上述的注意点之外,下面给出一些整体的告诫:

  • 1、覆盖equals方法时总是要覆盖hashCode方法
  • 2、不要企图让equals方法过于智能,如果只是简单的测试域中的值是否相等,则不难做的equals约定,如果项过度的的去寻求各种等价关系,则很容易陷入麻烦之中。
    • 把任何一种别名形式考虑到等价范围内,往往不是个好主意。例如,File类不应该试图把指向同一文件的符号链接当作相等对象来看待。所幸File类没有这样做。
  • 3、不要将equals声明中的Object对象替换为其他的类型。程序员编写出下面这样的equals方法并不鲜见,这会使得程序员花上好几个小时都搞不清楚为什么它不能正常工作。(因为重载会导致父类向下强制类型转换)
public boolean equals(MyClass obj) {  
    ...  
} 

上述代码,使用了具体的类MyClass作为参数,这会导致错误。原因在于,这个方法并没有重写(override)Object.equals方法,而是重载(overload)了它。某些情况下,这个具体化的equals方法会提高一些性能,但这样极有可能造成不易察觉的错误。

4.1、==和equals()的区别

1、= =:是运算符。 既可以比较基本类型也可以比较引用类型。对于基本类型就是比较值,而引用类型就是比较内存地址。并且必须保证符号左右两边的变量类型一致,即都是比较基本类型,或者都是比较引用类型。

2、equals():是java.lang.Object类里面的方法。只能适用于引用数据类型。如果类的该方法没有被重写过默认也是= =

 private String name;
    private int age;

   public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public Customer() {
        super();
    }
    public Customer(String name, int age) {
        super();
        this.name = name;
        this.age = age;
    }
    //自动生成的equals()
    @Override
    public boolean equals(Object obj) {  // 用传进来的对象做比较
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Customer other = (Customer) obj;
        if (age != other.age)
            return false;
        if (name == null) {
            if (other.name != null)
                return false;
        } else if (!name.equals(other.name))
            return false;
        return true;
    }
    
   @Override
    public String toString() {
        return "Customer [name=" + name + ", age=" + age + "]";
    }
}


    public static void main(String[] args) { 
    	Customer cust1 = new Customer("Tom",21);
   	    Customer cust2 = new Customer("Tom",21);
    	System.out.println(cust1.equals(cust2)); //true(因为没有重写就是false,因为调用的是Object父类的equals,这比较的是 == ,是地址值,重写了equals方法才是对比里面的具体内容)。
	}

5、总结

在这里插入图片描述

参考

1、如何正确的覆盖equals和hashCode
2、覆盖equals方法需要注意的
3、Effective Java 【对于所有对象都通用的方法】第10条 覆盖equals方法请遵守通用规范

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值