聊聊Java中的 " == "、equals以及hashCode

96
EakonZhao
0.2 2016.09.23 13:49* 字数 3620

关于 “ == ”

“ == ”操作符主要比较的是操作符两端对象的内存地址。如果两个对象的内存地址是一致的,那么就返回true。反之,则返回false。

话不多说,先来看看下面这段代码:

程序1-1

再来看看会输出什么:

上面程序的输出结果

怎么样,这个结果你想到了吗?

以下是关于上面结果的解释:

其实在Java中对于字符串的创建,有两种形式。

  • 一种是形如String a = "Hello"这样的字面量形式;
  • 另一种是形如String d = new String("Hello")这样的构造对象的方法。
字面量形式

在JVM中,为了减少对字符串变量的重复创建,其拥有一段特殊的内存空间,这段内存被称为字符串常量池(constant pool)或者是“字符串字面量池”

程序1-1中,我们首先使用 String a = "Hello" 创建了一个对象。此时默认字符串常量池中没有内容为“Hello”的对象存在。当JVM在字符串常量池中没有找到内容为"Hello"的对象时,就会创建一个内容为"Hello"的对象,并将它的引用返回给变量a。

那么当我们在后面使用 String b = "Hello" 的时候,会发生什么情况呢?
首先JVM会在字符串常量池中查询是否有内容为“Hello”的对象。因为此时字符串常量池中已经有内容为“Hello”的对象了,故JVM不会创建新的地址空间,而是将原有的“Hello”对象的引用返回给b------所以此时变量a和变量b拥有的是同一个对象引用,即指向的是同一块内存地址,因此使用 == 操作符对a和b进行比较,结果为true。

使用new来构造一个对象

然而当我们new一个字符串对象时,不管字符串常量池中是否有内容相同的对象,JVM都会创造一个新的对象,所以地址肯定不一样,因此使用 == 操作符对a和d进行比较,结果为false。

关于equals

在Object类中,equals方法源码如下:

Object类中的equals方法

我们可以看到,在Object类中的equals方法其实在内部也是使用了“ == ”操作符-----如果两个对象地址一样则返回true,反之则返回false。
既然equals方法里面也是使用“ == ”,那为什么还要设立一个equals方法,而不是直接用“ == ”操作符呢?

别急,这只是Object类里面的equals方法,其实绝大部分Object的子类都对equals方法进行了改写:

String类中的equals方法
HashMap中的equals方法

除了会比较内存地址,以上两个类中的equals方法还会比较对象所包含的内容。如果两个对象所包含的内容相同,equals方法也是返回true。

官方文档中对equals方法的描述:

equals方法在非空对象引用上实现相等关系:

  • 自反性:对于任何非空引用值x,x.equals(x)都应返回true。
  • 对称性:对于任何非空引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)才应返回true。
  • 传递性:对于任何非空引用值x、y和z,如果x.equals(y)返回true,并且y.equals(z)返回true,那么x.equals(z)应返回true。
  • 一致性:对于任何非空引用值x和y,多次调用x.equals(y)始终返回true
    或始终返回false,前提是对象上equals比较中所用的信息没有被修改。对于任何非空引用值x,x.equals(null)都应返回false。
    Object类的equals方法实现对象上差别可能性最大的相等关系;即,对于任何非空引用值x和y,当且仅当x和y引用同一个对象时,此方法才返回true(x == y具有值true)。
    注意:当此方法被重写时,通常有必要重写hashCode方法,以维护hashCode方法的常规协定,该协定声明相等对象必须具有相等的哈希码。

其实在很多时候,我们都需要改写equals方法以适应我们的实际情况:

下面是一个Person类,包含name和age两个属性。

Person类

现在我们创建两个Person对象,但是name和age分别都设置一样的值,同时使用equals方法对这两个对象进行比较:

创建两个Person对象并用equals方法对其进行比较
比较之后的结果

虽然这从JVM的角度来看这个程序是对的,可是结果并不是我们想要的:对象one和对象two虽然是两个不同的对象,但是它们包含的元素的值是相同的,也就是说one和two应该都表示的是同一个人-----name 为 EakonZhao,年龄为19。可是为什么调用equals方法之后输出的却是false呢?

原因:由于我们没有对equals方法进行改写,所以当我们在调用equals方法的时候实际上调用的是Object类的equals方法。从前面我们可以得知,Object的equals方法在内部是直接使用“ == ”操作符对对象进行比较,这样当然会返回false啦!

下面我将对equals方法进行改写,以满足我们的需求;

改写之后的equals方法
改写equals方法之后的输出结果

对于改写equals方法之后是否需要改写hashCode方法以维护hashCode方法的常规协定,我在介绍完hashCode方法之后会继续讲

关于在使用"=="操作符和equals方法时发生的自动装箱与自动拆箱现象

简单介绍一下自动拆箱和自动装箱

自动装箱:将Java的基本类型转换成包装器类型;
自动拆箱:将Java的包装器类型转换成基本类型。

基本数据类型及其包装器类型
自动装箱与自动拆箱

如上图所示,其实这是一个非常简单的程序,但是实际上Java自动帮我们完成了拆装箱的操作。

背景:
在JDK1.5之前,如果我们要生成一个数值为1的Integer对象,那么我们必须使用下面这行代码:

创建一个数值为1的Integer对象

可是引入了自动装箱功能之后,我们只需使用这样一行代码就能完成了:

自动装箱

这是如何实现的呢?下面我们将上面那段代码的class文件反编译一下:

反编译之后的代码

我们可以看到,其实Java是使用了 Integer的valueOf(int)方法来完成自动装箱的,而在拆箱过程当中调用的是Integer的intValue方法。对于其他的包装器类型来说,其实这两个过程也是类似的。

装箱过程是通过调用包装器类型的valueOf方法实现的,拆箱过程是通过调用包装器类型的xxValue方法实现的(其中xx代表的是对应的基本类型)。

让我们再来看看下面这段代码:

Integer缓存示例

为什么上面两条打印代码会输出不同的结果?
原因也很简单:与字符串常量池类似,这其实也是JVM节省内存的一个方法-----对于Integer类型的对象来说,如果我们要创建的Integer对象的数值在 [-128,127]的区间之内,那么JVM就会在缓存中查找,看看有没有已经存放在缓存中的数值一样的Integer对象。如果有,就返回已经存在的对象的引用。

关于使用equals方法时发生的自动拆装箱现象就不赘述了,其实很容易理解。

小结: “==”运算符其实比较的是地址相不相同,而equals方法比较的是值相不相同。

关于hashCode

官方文档中队hashCode方法的描述:

public int hashCode()
返回该对象的哈希码值。支持此方法是为了提高哈希表(例如java.util.Hashtable
提供的哈希表)的性能。
hashCode的常规协定是:

  • 在 Java 应用程序执行期间,在对同一对象多次调用hashCode方法时,必须一致地返回相同的整数,前提是将对象进行equals比较时所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。
  • 如果根据equals(Object)方法,两个对象是相等的,那么对这两个对象中的每个对象调用hashCode方法都必须生成相同的整数结果。
  • 如果根据equals(java.lang.Object)方法,两个对象不相等,那么对这两个对象中的任一对象上调用hashCode方法要求一定生成不同的整数结果。但是,程序员应该意识到,为不相等的对象生成不同整数结果可以提高哈希表的性能。

实际上,由Object类定义的 hashCode 方法确实会针对不同的对象返回不同的整数。(这一般是通过将该对象的内部地址转换成一个整数来实现的,但是 JavaTM
编程语言不需要这种实现技巧。)

上面这段话,简单的来说就是以下几点:

  • hashCode存在的意义主要是提供查找的快捷性,比如说在Hashtable、HashMap中等。hashCode是用来在散列存储结构中确定对象存储的位置的;
  • 如果两个对象相同,即调用equals方法返回的是true,那么它俩的hashCode值也要相同;
  • 如果equals方法被改写了,那么hashCode方法也尽量要改写,并且产生hashCode的对象也要和equals的对象保持一致;
  • 两个对象的hashCode相同并不代表两个对象就一定相同,也就是不一定适用于equals(java.lang.Object)方法,只能够说明这两个对象在散列存储对象中,如Hashtable中,是存放在同一个篮子里的。(关于Hashtable的介绍我在后面会开博客探究其源码)

下面这段话是从别人那里转过来的,我觉得能帮助理解hashCode:

  • hashcode是用来查找的,如果你学过数据结构就应该知道,在查找和排序这一章有
    例如内存中有这样的位置
    01234567
    而我有个类,这个类有个字段叫ID,我要把这个类存放在以上8个位置之一,如果不用hashcode而任意存放,那么当查找时就需要到这八个位置里挨个去找,或者用二分法一类的算法。
    但如果用hashcode那就会使效率提高很多。
    我们这个类中有个字段叫ID,那么我们就定义我们的hashcode为ID%8,然后把我们的类存放在取得得余数那个位置。比如我们的ID为9,9除8的余数为1,那么我们就把该类存在1这个位置,如果ID是13,求得的余数是5,那么我们就把该类放在5这个位置。这样,以后在查找该类时就可以通过ID除8求余数直接找到存放的位置了。
  • 但是如果两个类有相同的hashcode怎么办呢?(我们假设上面的类的ID不是唯一的),例如9除以8和17除以8的余数都是1,那么这是不是合法的,回答是:可以这样。那么如何判断呢?在这个时候就需要定义equals了。
    也就是说,我们先通过hashcode来判断两个类是否存放某个桶里,但这个桶里可能有很多类,那么我们就需要再通过equals来在这个桶里找到我们要的类。
    那么。重写了equals(),为什么还要重写hashCode()呢?
    想想,你要在一个桶里找东西,你必须先要找到这个桶啊,你不通过重写hashcode()来找到桶,光重写equals()有什么用啊

下面是代码示例:
我新建了一个HashExample类,里面定义了一个属性为id,并改写了hashCode方法:

HashExample类

现在我new两个对象,这两个对象的id我都赋予相同的值,并将它们两个存入一个Set(Set中的元素是不重复的)当中,然后分别输出它们两个的hashCode以及使用equals方法比较的结果以及将这个Set也输出:

改写equals方法之前

以上这个示例,我们只是重写了hashCode方法,从上面的结果可以看出,虽然两个对象的hashCode相等,但是实际上两个对象并不是相等;,我们没有重写equals方法,那么就会调用object默认的equals方法,是比较两个对象的引用是不是相同,显示这是两个不同的对象,两个对象的引用肯定是不定的。这里我们将生成的对象放到了hashSet中,而hashSet中只能够存放唯一的对象,也就是相同的(适用于equals方法)的对象只会存放一个,但是这里实际上是两个对象a,b都被放到了HashSet中,这样hashSet就失去了他本身的意义了。

现在把equals也改写一下:

改写之后的equals方法
改写equals方法之后的输出结果

现在我们可以看到,这两个对象已经完全相等了,并且hashSet中也只存放了一份对象。

Java知识