两种比较方法

  • ==
  • 基本类型
    对于基本类型,== 的功能是比较值。
  • Object比较对象在内存中的地址。
  • equals基本类型无equals方法。Object对象默认equals的实现如下:
/**
 * ...
 * @param   obj   the reference object with which to compare.
 * @return  {@code true} if this object is the same as the obj
 *          argument; {@code false} otherwise.
 * @see     #hashCode()
 * @see     java.util.HashMap
 */
public boolean equals(Object obj) {
    return (this == obj);
}

有很长一段注释,最终的实现我们可以看到还是用的 == 来比较两个对象在内存中的地址。

equals() & hashCode()

对于equals的默认实现——比较对象在内存中的地址——有时候可能并不是我们期望的,比如有一个Student类:

class Student{
   private int id;
   private String name;

   public Student(int id){
       this.id = id;
   }

   // ... getter and setter
}

当 Student.id 相同时,我们更愿意认为这是同一个学生,而下面这个测试是无法通过的:

@Test
public void equalsStudent() throws Exception {
   Student st1 = new Student(2333);
   Student st2 = new Student(2333);
   assertEquals(st1, st2);
   // assertNotEquals(st1, st2);
}

为了达到我们的目的——相同的学号就认为是同一个人——我们可以重写Student类的 equals 方法:

class Student {
   private int id;
   private String name;

   public Student(int id) {
       this.id = id;
   }

   @Override
   public boolean equals(Object o) {
       if (null == o) {
           return false;
       }
       Student std = (Student) o;
       if (this.id == std.id) {
           return true;
       }
       return super.equals(o);
   }

   // ... getter and setter
}

修改之后上面的测试就可以通过了。Demo毕竟是简单的,当我们在实际的使用中需要重写equals方法时还是需要遵守它的生成规则,这里贴出来供参考:

public boolean equals(Object obj)

    指示其他某个对象是否与此对象“相等”。

    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 方法的常规协定,该协定声明相等对象必须具有相等的哈希码。

    参数:
        obj - 要与之比较的引用对象。 
    返回:
        如果此对象与 obj 参数相同,则返回 true;否则返回 false。
    另请参见:
        hashCode(), Hashtable

事情还没有结束。看下面代码:

@Test
public void studentSet() throws Exception {
   Student st1 = new Student(2333);
   Student st2 = new Student(2333);

   Set<Student> stds = new HashSet<>();
   stds.add(st1);
   stds.add(st2);
   assertEquals(1, stds.size());
}

你会发现这段代码测试不通过。set不是重复对象只会保留一份吗,为什么不是 1 呢?

这里就要介绍 hashCode() 方法:

public int hashCode()

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

    hashCode 的常规协定是:

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

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

    返回:
        此对象的一个哈希码值。
    另请参见:
        equals(java.lang.Object), Hashtable

当我们使用用哈希表实现的工具类时,这个方法的价值就体现了。由于之前我们没有重写这个方法,把Student对象存入HashSet时还是按照之前系统默认的方式计算他们的hash值,导致Set中存在两个Student对象。

下面做一个简单修改:

class Student {
   private int id;
   private String name;

   public Student(int id) {
       this.id = id;
   }

   @Override
   public boolean equals(Object o) {
       if (null == o) {
           return false;
       }

       // 有时候instanceof不合适的时候可以考虑用getClass()方法
       if (getClass() != o.getClass()) {
           return false;
       }

       if (o instanceof Student) {
           Student std = (Student) o;
           if (this.id == std.id) {
               return true;
           }
       }

       return super.equals(o);
   }

   @Override
   public int hashCode() {
       return this.id; 
       // return super.hashCode();
   }

   // ... getter and setter
}

再运行下面的测试,你会发现和我们期待的一致了:

@Test
public void studentSet() throws Exception {
   Student st1 = new Student(2333);
   Student st2 = new Student(2333);

   Set<Student> stds = new HashSet<>();
   stds.add(st1);
   stds.add(st2);
   assertEquals(1, stds.size());
}

Demo中直接将id作为hashcode返回不是一种好的生成方式,具体的生成规则请参考上面的注释。

Demo

再来看一个demo:

@Test
public void stringTest() throws Exception {
   String str1 = "abc";
   String str2 = "abc";
   String str3 = new String("abc");

   System.out.println(str1 == str2);
   System.out.println(str1 == str3);

   assertEquals(str1, str2);
   assertEquals(str1, str3);
}

结果会怎样?没错,测试通过,输出:true,false。

首先我们来看下String类重写的equals()和hashCOde():

@Override 
public boolean equals(Object other) {
   if (other == this) {
     return true;
   }
   if (other instanceof String) {
       String s = (String)other;
       int count = this.count;
       if (s.count != count) {
           return false;
       }
       if (hashCode() != s.hashCode()) {
           return false;
       }
       for (int i = 0; i < count; ++i) {
           if (charAt(i) != s.charAt(i)) {
               return false;
           }
       }
       return true;
   } else {
       return false;
   }
}

@Override 
public int hashCode() {
   int hash = hashCode;
   if (hash == 0) {
       if (count == 0) {
           return 0;
       }
       for (int i = 0; i < count; ++i) {
           hash = 31 * hash + charAt(i);
       }
       hashCode = hash;
   }
   return hash;
}

从上面可以看出,String类重写了这两个方法,equals()中的逻辑是比较字符串中每个字符是否相同。因此 str1, str2, str3相同就可以理解了。
对于 str1 == str2str1 != str3 这涉及到不同字符串创建方法。

在开始这个例子之前,同学们需要知道JVM处理String的一些特性。Java的虚拟机在内存中开辟出一块单独的区域,用来存储字符串对象,这块内存区域被称为字符串缓冲池。当使用 String a = “abc”这样的语句进行定义一个引用的时候,首先会在字符串缓冲池中查找是否已经相同的对象,如果存在,那么就直接将这个对象的引用返回给a,如果不存在,则需要新建一个值为”abc”的对象,再将新的引用返回a。String a = new String(“abc”);这样的语句明确告诉JVM想要产生一个新的String对象,并且值为”abc”,于是就在堆内存中的某一个小角落开辟了一个新的String对象。