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

Posted JMW1407

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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方法请遵守通用规范

以上是关于Java:Effective java学习笔记之 覆盖equals时请遵守通用约定的主要内容,如果未能解决你的问题,请参考以下文章

Java:Effective java学习笔记之 避免使用终结方法

Java:Effective java学习笔记之 消除过期对象引用

Java:Effective java学习笔记之 列表优先于数组

Java:Effective java学习笔记之 用enum代替int常量

Java:Effective java学习笔记之 复合优先于继承

Java:Effective java学习笔记之 接口优于抽象类