美文网首页
Java中的"=="和"equals()"

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

作者: sortinnauto | 来源:发表于2018-08-05 23:41 被阅读0次

    前言

    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

    相关文章

      网友评论

          本文标题:Java中的"=="和"equals()"

          本文链接:https://www.haomeiwen.com/subject/tovbvftx.html