Java中的"=="和"equals()"

前言

equals() 和 hashCode() 都是 Object 对象中的非 final 方法,它们设计的目的就是被用来覆盖(override)的,所以在程序设计中还是经常需要处理这两个方法的。而掌握这两个方法的覆盖准则以及它们的区别还是很必要的,相关问题也不少。

首先看看==和 equals() 的不同。

对于基本数据类型==比较的是它们的值。

Example

        int num1 = 1, num2 = 1;
        char ch1 = 'a', ch2 = 'a';
        if (num1 == num2 && ch1 == ch2) {
            System.out.println(num1 == num2);
            System.out.println(ch1 == ch2);
        } else {
            System.out.println(num1 == num2);
            System.out.println(ch1 == ch2);
        }

输出结果:

true
true

对于引用类型==比较的是它们的内存地址。以 String 为例:

        String str1 = "Hello World";
        String str2 = "Hello World";
        
        if (str1 == str2) {
            System.out.println("str1和str2的地址相同");
        } else {
            System.out.println("str1和str2的地址不同");
        }

输出结果:

str1和str2的地址相同

根据String的源码解释:

The String class represents character strings. All string literals in Java programs, such as "abc", are implemented as instances of this class.Strings are constant; their values cannot be changed after they are created.

可见,直接这样初始化的字符串属于 String 类的实现,并且 String 类型的字符串自被创建后就是不可变的了。并且 Java 也推荐这样初始化 String ,这是为什么呢?

在这一构造函数的注释中这样写道:

 public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }

Initializes a newly created String object so that it represents the same sequence of characters as the argument; in other words, the newly created string is a copy of the argument string. Unless an explicit copy of original is needed, use of this constructor is unnecessary since Strings are immutable.

大意就是说这样初始化 String 对象,其字符序列是与参数相同的;也就是说新创建的字符串是参数字符串的副本。 因此除非需要显式的原始副本,否则不必使用此构造函数,因为字符串是不可变的。

那为什么字符串是不可变的呢?

在 String 源码中可以看到,其实字符串是被存放到了一个 char 类型的数组中,且该数组被 final 关键字修饰,因此创建好的字符串是不可变的也就可以想通了。

/** The value is used for character storage. */
    private final char value[];

因此下面的语句是等价的:

String str = "abc";
//str等价于:
char [] data = {'a', 'b', 'c'};
String str = new String(data);

这样理解了一番之后,就可以得出一些结论:

  • 如果要初始化字符串,直接给 String 类型的变量赋值即可;
  • 但如果要转换数组为字符串, String 类提供了不少相应的构造函数,可以查阅文档选择合适的使用。
  • 用 String 类创建的字符串是不可变的(constant)。

回到最开始的问题,又有一个新的问题:String是怎么判断两个字符串引用地址的呢?

在String的源码中提到了字符串缓冲池(A pool of strings):

A pool of strings, initially empty, is maintained privately by the class String.
...
if the pool already contains a string equal to this String object as determined by the equals(Object) method, then the string from the pool is returned. Otherwise, this String object is added to the pool and a reference to this String object is returned.

这个缓冲池是由 String 类维护的,当缓冲池中已经存在一个和传入字符串相同的字符串(通过调用 equals() 方法来确定是否相同),那么就返回缓冲池中的字符串。否则,就把传入的新字符串添加到缓冲池中并返回这个字符串的引用。

结合之前提到的使用构造函数来创建新字符串的方式,这种方式新创建的字符串是传入字符串的副本,现在对这句话进行解释。看下面的例子:

        String str1 = "Hello";
        String str2 = new String("Hello");

        if (str1 == str2) {
            System.out.println("str1和str2的地址相同");
        } else {
            System.out.println("str1和str2的地址不同");
        }

输出结果为:

str1 和 str2 的地址不同

  • str1 创建的字符串被加入到字符串缓冲池中去, str1 指向缓冲池中的 "Hello" ;
  • str2 调用构造函数传入 "Hello" ,实际上进行了两步:
  1. 先去缓冲池中找有没有相同的字符串(通过调用 equals() 方法来确定是否相同);
  2. 发现缓冲池中有相同的字符串,那就不需要新创建一遍了;而如果没有,就新创建一个字符串。这里的情况显然是第一种。但是由于 str2 中调用了构造函数,因此要在内存的堆空间上新建一个对象,而这个 str2 正是指向内存堆上的一个地址空间。

因此 str1 和 str2 通过 == 判断到的地址空间是不同的。


所有类中的 equals() 方法都是继承自或重写 Object 类中 equals() 方法的。Object类提供的 equals() 方法如下:

 public boolean equals(Object obj) {
       return (this == obj);
   }

可见最原始的 equals() 方法其实就是调用的==。对于任何非空(non-null)的引用变量 x 和 y ,当且仅当 x 和 y 指向同一个对象的时候,equals() 方法就返回 true 。

重写 equals() 的准则,这个在 Object 类中有提到过:

  • 自反性:
    It is reflexive: x.equals(x) should return true;
  • 对称性:
    It is symmetric: x.equals(y) should return true if and only if y.equals(x) returns true;
  • 传递性:
    It is transitive: if x.equals(y) returns true and y.equals(z) returns true, then x.equals(z) should return true.
  • 一致性:
    It is consistent: for any non-null reference values x and y, multiple invocations of x.equals(y) consistently return true or consistently return false, provided no information used in equals comparisons on the objects is modified.
  • 非空性:
    x.equals(null) should return false.

那么问题来了,哪些情况下会违反对称性和传递性?

  • 违反对称性

对称性就是x.equals(y)时,y也得equals x,很多时候,我们自己覆写equals时,让自己的类可以兼容等于一个已知类,比如下面的例子:

public final class CaseInsensitiveString {
    private final String s;
    public CaseInsensitiveString(String s) {
        if (s == null)
            throw new NullPointerException();
        this.s = s;
    }
    
    @Override
    public boolean equals(Object o) {
        if (o instanceof CaseInsensiticeString)
            return s.equalsIgnoreCase(((CaseInsensitiveString)o).s);
        if (o instanceof String)
            return s.equalsIgnoreCase((String) o);
        return false;
    }
}

这个想法很好,想创建一个无视大小写的String,并且还能够兼容String作为参数,假设我们创建一个CaseInsensitiveString:

CaseInsensitiveString cis = new CaseInsensitiveString("Case");

那么肯定有 cis.equals("case"),问题来了,"case".equals(cis)吗? String 并没有兼容 CaseInsensiticeString ,所以 String 的 equals() 也不接受 CaseInsensiticeString 作为参数。

所以有个准则,一般在覆写 equals() 只兼容同类型的变量。

  • 违反传递性

传递性就是A等于B,B等于C,那么A也应该等于C。

假设我们定义一个类Cat。

public class Cat(){
    private int height;
    private int weight;
    public Cat(int h, int w)
    {
        this.height = h;
        this.weight = w;
    }
    
    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Cat))
            return false;
        Cat c = (Cat) o;
        return c.height == height && c.weight == weight; 
    }
}

名人有言,不管黑猫白猫抓住老鼠就是好猫,我们又定义一个类ColorCat:

public class ColorCat extends Cat{
    private String color;
    public ColorCat(int h, int w, String color)
    {
        super(h, w);
        this.color = color;
    }

我们在实现 equals 方法时,可以加上颜色比较,但是加上颜色就不兼容和普通猫作对比了,这里我们忘记上面要求只兼容同类型变量的建议,定义一个兼容普通猫的 equals 方法,在“混合比较”时忽略颜色。

@Override
public boolean equals(Object o) {
    if (! (o instanceof Cat))
        return false; //不是Cat或者ColorCat,直接false
    if (! (o instanceof ColorCat))
        return o.equals(this);//不是彩猫,那一定是普通猫,忽略颜色对比
    return super.equals(o)&&((ColorCat)o).color.equals(color); //这时候才比较颜色
}

假设我们定义了猫:

ColorCat whiteCat = new ColorCat(1,2,"white");
Cat cat = new Cat(1,2);
ColorCat blackCat = new ColorCat(1,2,"black");

此时有whiteCat等于catcat等于blackCat,但是whiteCat不等于blackCat,所以不满足传递性要求。


源码注释中还提到,无论何时当 equals() 方法被重写的时候,都有必要去重写一下 hashCode() 方法以便维持 hashCode() 方法的通用契约(general contract),这个契约就是相同的对象必须具有相同的哈希值

类库提供的 equals() 方法,如果已经重写的话,那么比较的也许就不止是地址空间了,这就看具体类库是怎么实现的了。以 String 类为例,它提供的 equals() 方法如下:

public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }
  • 首先会判断当前对象和传入对象是否指向相同的地址空间,如果是就直接返回 true ;
  • 如果指向的地址空间不同,检测传入对象是否为 String 类的实例,如果是就比较两个对象中值是否相同,如果相同,返回 true ;否则返回 false ;
  • 如果传入对象不是 String 类的实例,返回 false 。

Example

        String str1 = "Hello";
        String str2 = new String("Hello");
        if (str2.equals(str1)) {
            System.out.println("str1 equals to str2");
        } else {
            System.out.println("str1 doesn't equals to str2");
        }

输出结果为:

str1 equals to str2

原因显而易见。


参考:
面试官爱问的equals与hashCode

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容