最近和同事谈到equals和==的区别。这其实是个非常老套简单的问题,但当你要亲自覆盖equals方法时,才发现,有一些你不知道却又不得不知道的事。覆盖equals,讲究很多。尽管Object是一个很具体的类,但是他的主要作用还是为了扩展。他的所有非final方法都有着明确的通用约定。因为他们被设计成要被覆盖的方法。任何一个类,在覆盖equals、hashCode、toString、clone、finalize时,都有责任遵守这些方法的通用约定。如果不能做到这一点,那么当多个类组合时将难以发挥我们期望的效果。
覆盖equals方法看起来简单,但是有许多覆盖方法会导致错误。最容易避免这种错误的方法就是不覆盖equals,这种情况下每个类的实例都只与自身相等。那么在什么情况下我们可以选择不覆盖equals方法呢?
类的每个实例本质上是唯一的
对于代表活动实体而不是值的类来说确实如此,比如每个线程实例。我们用equals方法比较他是毫无意义的,因为每个线程是唯一的。在这种情况下,我们不用覆盖equals方法,因为Object类中的equals已经完全够用了。
Object类中equals方法实现:
public boolean equals(Object obj) { return (this == obj); }
不关心类是否需要逻辑相等的判断
有些类是一些“数值类”,比较大小,数学运算是这些类的本职工作。在这种情况下,需要我们将类中存放的数值进行比较,需要进行逻辑相等的判断。除此之外的类,很大部分类没有“一个是否等于另外一个”的概念。这种不关心逻辑相等的类不需要覆盖equals方法。
超类实现的equals对子类适用
举个例子,继承了AbstractSet类的HashSet类在equals方法上并没有任何区别,那么HashSet直接使用AbstractSet的equals方法即可。
与上面的内容相反的,我们需要覆盖equals方法的情况就是:如果类具有自己的逻辑相等的概念,并且父类没有进行可用的equals方法覆盖。这时需要我们亲自进行覆盖。
equals方法实现了等价关系。离散数学中学习过等价关系的概念,对于一个R上的二元关系,如果它满足自反对称和传递,那么他就是等价的。我们来具体分析一下equals和这三种性质的关系。
自反性 : 对于任何非null的引用值x,x.equals(x)必须返回true
对称性 : 对于任何非null的引用值x和y,当且仅当y.equals(x)返回 true时,x.equals(y)必定返回true
传递性 : 对于任何非null的引用值x、y和z,如果x.equals(y)返回true、y.equals(z)也返回true,那么x.equals(z)必定返回true。
自反性:∀ a ∈A, => (a, a) ∈ R
对称性:(a, b) ∈R∧ a ≠ b => (b, a)∈R
传递性:(a, b)∈R,(b, c)∈R =>(a, c)∈R
对比一下这三个性质,没有任何问题。由此可得,equals方法实现了等价关系。
Object的equals方法只是简单的看看地址,这显然不可能满足我们的要求。那么在自己编写equals方法进行覆盖时如何才能保证编写出高质量的,逻辑比较的方法呢?equals的编写可以概括为下面四步:
1.使用==操作符检查参数是否只是对象的引用
如果结果相等则返回true,说明x与y是一个对象的不同引用,不需要再进行判断了。
2.使用instanceof操作符检查参数是否类型正确
如果结果不是正确的类型则返回false,因为我们的equals方法是继承自Object类的,所以参数的类型无法避免的是Object,我们先使用instanceof对参数进行类型判断,如果类型都不正确,就不用进行下一步判断了。
3.把参数转换成正确的类型
因为之前做了检测,所以这一步的类型转换没有问题。
4.对每个类中需要逻辑比较的域值进行判断
已经确保x和y是相同类型的不同实例,将需要判断的逻辑比较的域值取出进行比较判断即可。如果全部正确则返回true,否则返回false。
当编写完equals方法后,一定要反复的判断是否符合自反性,对称性和传递性。不仅仅如此,在确保等价的情况下,编写equals方法时还有一些值得注意的事情,我们需要以此改进。
覆盖equals方法时总要覆盖hashCode方法
如果我们在编写有关散列的类时,必须在覆盖equals方法时覆盖hashCode方法。因为在散列表中,逻辑相同的对象应该具有相同的散列码。举个比较简单的例子:将String存入HashSet,有可能两个内容相同的String字串用==判断为false,但是他们在HashSet中只存在了一份。这就是因为逻辑相同的String拥有这相同的hashCode。
普遍性的,如果你为你的类覆盖了equals方法,那么证明在某种情况下会有两个不同对象是逻辑相等的。此时如果与散列相关,那么这两个对象需要相同的hashCode。所以覆盖equals方法时总要覆盖hashCode方法。
不要让equals方法过于智能
如果我们只是简单的按照上面的实现流程来编写equals的方法,既符合规定也不会导致奇怪的错误。但是如果我们非要去追求各种各样、花哨的等价关系,而把代码搞得臃肿不堪,那么既违反了高内聚的初衷,也会让代码出一些莫名其妙的错误。
不要将equals方法的参数类型弄错
说出来可能感觉好笑,但这确实是会发生的情况。修改了参数类型之后的equals方法已完全于Object类没有了关系,编译器不会报错,留下的只是给程序员无尽的头痛。如果不能意识到参数类型是Object,很有可能花费几个小时也搞不清程序为什么不能正常工作。
最近和同事谈到equals和==的区别。这其实是个非常老套简单的问题,但当你要亲自覆盖equals方法时,才发现,有一些你不知道却又不得不知道的事。覆盖equals,讲究很多。尽管Object是一个很具体的类,但是他的主要作用还是为了扩展。他的所有非final方法都有着明确的通用约定。因为他们被设计成要被覆盖的方法。任何一个类,在覆盖equals、hashCode、toString、clone、finalize时,都有责任遵守这些方法的通用约定。如果不能做到这一点,那么当多个类组合时将难以发挥我们期望的效果。
覆盖equals方法看起来简单,但是有许多覆盖方法会导致错误。最容易避免这种错误的方法就是不覆盖equals,这种情况下每个类的实例都只与自身相等。那么在什么情况下我们可以选择不覆盖equals方法呢?
类的每个实例本质上是唯一的
对于代表活动实体而不是值的类来说确实如此,比如每个线程实例。我们用equals方法比较他是毫无意义的,因为每个线程是唯一的。在这种情况下,我们不用覆盖equals方法,因为Object类中的equals已经完全够用了。
Object类中equals方法实现:
public boolean equals(Object obj) { return (this == obj); }
不关心类是否需要逻辑相等的判断
有些类是一些“数值类”,比较大小,数学运算是这些类的本职工作。在这种情况下,需要我们将类中存放的数值进行比较,需要进行逻辑相等的判断。除此之外的类,很大部分类没有“一个是否等于另外一个”的概念。这种不关心逻辑相等的类不需要覆盖equals方法。
超类实现的equals对子类适用
举个例子,继承了AbstractSet类的HashSet类在equals方法上并没有任何区别,那么HashSet直接使用AbstractSet的equals方法即可。
与上面的内容相反的,我们需要覆盖equals方法的情况就是:如果类具有自己的逻辑相等的概念,并且父类没有进行可用的equals方法覆盖。这时需要我们亲自进行覆盖。
equals方法实现了等价关系。离散数学中学习过等价关系的概念,对于一个R上的二元关系,如果它满足自反对称和传递,那么他就是等价的。我们来具体分析一下equals和这三种性质的关系。
自反性 : 对于任何非null的引用值x,x.equals(x)必须返回true
对称性 : 对于任何非null的引用值x和y,当且仅当y.equals(x)返回 true时,x.equals(y)必定返回true
传递性 : 对于任何非null的引用值x、y和z,如果x.equals(y)返回true、y.equals(z)也返回true,那么x.equals(z)必定返回true。
自反性:∀ a ∈A, => (a, a) ∈ R
对称性:(a, b) ∈R∧ a ≠ b => (b, a)∈R
传递性:(a, b)∈R,(b, c)∈R =>(a, c)∈R
对比一下这三个性质,没有任何问题。由此可得,equals方法实现了等价关系。
Object的equals方法只是简单的看看地址,这显然不可能满足我们的要求。那么在自己编写equals方法进行覆盖时如何才能保证编写出高质量的,逻辑比较的方法呢?equals的编写可以概括为下面四步:
1.使用==操作符检查参数是否只是对象的引用
如果结果相等则返回true,说明x与y是一个对象的不同引用,不需要再进行判断了。
2.使用instanceof操作符检查参数是否类型正确
如果结果不是正确的类型则返回false,因为我们的equals方法是继承自Object类的,所以参数的类型无法避免的是Object,我们先使用instanceof对参数进行类型判断,如果类型都不正确,就不用进行下一步判断了。
3.把参数转换成正确的类型
因为之前做了检测,所以这一步的类型转换没有问题。
4.对每个类中需要逻辑比较的域值进行判断
已经确保x和y是相同类型的不同实例,将需要判断的逻辑比较的域值取出进行比较判断即可。如果全部正确则返回true,否则返回false。
当编写完equals方法后,一定要反复的判断是否符合自反性,对称性和传递性。不仅仅如此,在确保等价的情况下,编写equals方法时还有一些值得注意的事情,我们需要以此改进。
覆盖equals方法时总要覆盖hashCode方法
如果我们在编写有关散列的类时,必须在覆盖equals方法时覆盖hashCode方法。因为在散列表中,逻辑相同的对象应该具有相同的散列码。举个比较简单的例子:将String存入HashSet,有可能两个内容相同的String字串用==判断为false,但是他们在HashSet中只存在了一份。这就是因为逻辑相同的String拥有这相同的hashCode。
普遍性的,如果你为你的类覆盖了equals方法,那么证明在某种情况下会有两个不同对象是逻辑相等的。此时如果与散列相关,那么这两个对象需要相同的hashCode。所以覆盖equals方法时总要覆盖hashCode方法。
不要让equals方法过于智能
如果我们只是简单的按照上面的实现流程来编写equals的方法,既符合规定也不会导致奇怪的错误。但是如果我们非要去追求各种各样、花哨的等价关系,而把代码搞得臃肿不堪,那么既违反了高内聚的初衷,也会让代码出一些莫名其妙的错误。
不要将equals方法的参数类型弄错
说出来可能感觉好笑,但这确实是会发生的情况。修改了参数类型之后的equals方法已完全于Object类没有了关系,编译器不会报错,留下的只是给程序员无尽的头痛。如果不能意识到参数类型是Object,很有可能花费几个小时也搞不清程序为什么不能正常工作。
以上是关于java覆盖equals更深层的方法概述的详细内容。更多信息请关注PHP中文网其他相关文章!