java:effectivejava学习笔记之覆盖equals时请遵守通用约定(代码片段)

JMW1407 JMW1407     2023-02-21     735

关键词:

覆盖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:effectivejava学习笔记之覆盖equals时请遵守通用约定(代码片段)

Java覆盖equals时请遵守通用约定覆盖equals时请遵守通用约定1、为什么要覆盖equals2、需要覆盖equals方法的时机2.1、不需要覆盖equals方法的情况2.2、需要覆盖equals方法的情况2.2.1、自反性2.2.2、对称性2.2.3、传递性2.2.4、一致性2.2.5、... 查看详情

java:effectivejava学习笔记之覆盖equals时总要覆盖hashcode(代码片段)

Java覆盖equals时总要覆盖hashcode覆盖equals时总要覆盖hashcode1.什么是hashcode方法?2.hashcode相等与对象相等之间的关系:(保证设计是规范的前提下)3.为什么要覆盖hashcode3.1、覆盖equals时总要覆盖hashCode3.2、如何在覆盖... 查看详情

java:effectivejava学习笔记之避免使用终结方法(代码片段)

Java避免使用终结方法避免使用终结方法1、finalize()基本概念2、finalize()的执行过程3、为什么要避免覆盖并使用finalize方法?4、如果类中的资源确实需要被释放,我们应该怎么做?5、终结方法的利弊5.1、终结方法的好... 查看详情

java:effectivejava学习笔记之复合优先于继承(代码片段)

Java复合优先于继承复合优先于继承1、实现继承和接口继承2、在实际开发中继承的缺点2.1、子类依赖于其超类中特定功能的实现细节3、什么是复合?3.1、书上案例4、复合相比较于继承的优点和缺点5、何时使用继承,何时使用... 查看详情

java:effectivejava学习笔记之优先考虑泛型和泛型方法(代码片段)

Java优先考虑泛型和泛型方法1、优先考虑泛型2、优先考虑泛型方法参考1、优先考虑泛型下面我们举个例子,将他作为泛型化的主要备选对象,换句话说,可以适当的强化这个类来利用泛型。publicclassStackprivateObject[]elem... 查看详情

effectivejava学习笔记之创建和销毁对象

一、考虑用静态工厂方法代替构造器1、此处的静态工厂方法是指返回指为类的对象的静态方法,而不是设计模式中的静态工厂方法。2、静态工厂方法的优势有:a、使用不同的方法名称可显著地表明两个静态工厂方法的不同,而... 查看详情

effectivejava学习笔记之不可实例化的类

在没有显式声明一个类的构造方法时,编译器会生成默认的无参构造方法,在设计工具类时,我们通常将方法设置成静态方法,以类名.方法名的形式调用,此时这个类就没有必要创建实例,我们知道抽象类不可以被实例化,但... 查看详情

java:java学习笔记之java单例模式的简单理解和使用(代码片段)

...单例的实现如下:2、懒汉式终极版本:volatile3、EffectiveJava1——静态内部类4、5.2EffectiveJava2——枚举参考Java单例模式1、饿汉式单例的实现如下://饿汉式实现publicclassSingleBprivatestaticfinalSingleBINSTA 查看详情

java:effectivejava学习笔记之考虑实现comparable接口(代码片段)

Java考虑实现Comparable接口考虑实现Comparable接口1、Comparable接口2、为什么要考虑实现Comparable接口3、compareTo方法的通用约定4、何时以及如何实现Comparable接口4.1、多重比较5、实现Comparable接口所需满足的需求6、总结参考考虑实现Compa... 查看详情

java:effectivejava学习笔记之接口只用于定义类型类层次优于标签类(代码片段)

Java接口只用于定义类型1、接口只用于定义类型1.1、常量接口2、类层次优于标签类1、接口只用于定义类型当类实现接口时,接口就充当可以引用这个类的实例的类型。因此,类实现了接口,就表明可以对这个类的实... 查看详情

java:effectivejava学习笔记之列表优先于数组(代码片段)

Java列表优先于数组列表优先于数组1、协变与不可变类型2、运行时检验与编译器检验3、可具体化与不可具体化4、无法很好混用列表和数组5、案例分析参考列表优先于数组1、协变与不可变类型1、数组是协变类型,指继承关... 查看详情

java:effectivejava学习笔记之请不要在新代码中使用原生态类型(代码片段)

Java请不要在新代码中使用原生态类型1、请不要在新代码中使用原生态类型参考1、请不要在新代码中使用原生态类型1、在没有泛型之前,从集合中读取到每一个对象都必须进行转换。如果有人不小心插入了类型错误的对象&#x... 查看详情

java:effectivejava学习笔记之避免创建不必要的对象(代码片段)

Java避免创建不必要的对象避免创建不必要的对象1、采用更合适的API或工具类减少对象的创建2、重用相同功能的对象3、小心自动装箱(autoboxing)4、用静态工厂方法而不是构造器5、正则表达式6、补参考避免创建不必要的... 查看详情

java:effectivejava学习笔记之消除过期对象引用(代码片段)

Java消除过期对象引用消除过期对象引用1、Java的垃圾回收机制2、Java中的内存泄露3、常见的内存泄露参考消除过期对象引用很多人可能在想这么一个问题:Java有垃圾回收机制,那么还存在内存泄露吗?答案是肯定的&#... 查看详情

java:effectivejava学习笔记之接口优于抽象类(代码片段)

Java接口优于抽象类接口优于抽象类1、接口和抽象类2、接口优点3、骨架类3.1、demo参考接口优于抽象类1、接口和抽象类Java中抽象类和接口的区别2、接口优点1、现有的类可以很容易的被更新,以实现新的接口。如果你前期编... 查看详情

java:effectivejava学习笔记之静态工厂方法的简单理解和使用(代码片段)

Java静态工厂方法静态工厂方法一、什么是静态工厂方法?二、静态工厂方法的优势1、静态工厂方法与构造器不同的第一优势在于,它们有名字2、静态工厂方法不用在每次调用的时候都创建一个新的对象3、静态工厂方法... 查看详情

java:effectivejava学习笔记之通过私有构造器强化不可实例化的能力(代码片段)

Java通过私有构造器强化不可实例化的能力通过私有构造器强化不可实例化的能力参考通过私有构造器强化不可实例化的能力并非所有的类都是需要实例化的。有时候,我们可能需要编写至包含静态方法和静态域的类。这些类... 查看详情

java:effectivejava学习笔记之遇到多个构造器参数时要考虑用构建器(代码片段)

Java遇到多个构造器参数时要考虑用构建器构建器1、构建一个对象的几种方式2、定义和使用场景3、优势和不足3、使用参考构建器1、构建一个对象的几种方式1、多构造函数:这种方式虽然最方便,但也最繁琐,假设... 查看详情