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

2018-08-05  本文已影响0人  sortinnauto

前言

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的源码中提到了字符串缓冲池(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 的地址不同

  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;
    }

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

上一篇下一篇

猜你喜欢

热点阅读