Java问答:终极父类(下)

Equality

问:euqals()函数是用来做什么的?

答:equals()函数可以用来检查一个对象与调用这个equals()的这个对象是否相等。

问:为什么不用“==”运算符来判断两个对象是否相等呢?

答:虽然“==”运算符可以比较两个数据是否相等,但是要来比较对象的话,恐怕达不到预期的结果。就是说,“==”通过是否引用了同一个对象来判断两个对象是否相等,这被称为“引用相等”。这个运算符不能通过比较两个对象的内容来判断它们是不是逻辑上的相等。

问:使用Object类的equals()方法可以用来做什么样的对比?

答:Object类默认的eqauls()函数进行比较的依据是:调用它的对象和传入的对象的引用是否相等。也就是说,默认的equals()进行的是引用比较。如果两个引用是相同的,equals()函数返回true;否则,返回false.

问:覆盖equals()函数的时候要遵守那些规则?

答:覆盖equals()函数的时候需要遵守的规则在Oracle官方的文档中都有申明:

  • 自反性:对于任意非空的引用值x,x.equals(x)返回值为真。
  • 对称性:对于任意非空的引用值x和y,x.equals(y)必须和y.equals(x)返回相同的结果。
  • 传递性:对于任意的非空引用值x,y和z,如果x.equals(y)返回真,y.equals(z)返回真,那么x.equals(z)也必须返回真。
  • 一致性:对于任意非空的引用值x和y,无论调用x.equals(y)多少次,都要返回相同的结果。在比较的过程中,对象中的数据不能被修改。
  • 对于任意的非空引用值x,x.equals(null)必须返回假。

问:能提供一个正确覆盖equals()的示例吗?

答:当然,请看代码清单8。

代码清单8:对两个对象进行逻辑比较

class Employee
{
   private String name;
   private int age;

   Employee(String name, int age)
   {
      this.name = name;
      this.age = age;
   }

   @Override
   public boolean equals(Object o)
   {
      if (!(o instanceof Employee))
         return false;

      Employee e = (Employee) o;
      return e.getName().equals(name) && e.getAge() == age;
   }

   String getName()
   {
      return name;
   }

   int getAge()
   {
      return age;
   }
}

public class EqualityDemo
{
   public static void main(String[] args)
   {
      Employee e1 = new Employee("John Doe", 29);
      Employee e2 = new Employee("Jane Doe", 33);
      Employee e3 = new Employee("John Doe", 29);
      Employee e4 = new Employee("John Doe", 27+2);
      // 验证自反性。
      System.out.printf("Demonstrating reflexivity...%n%n");
      System.out.printf("e1.equals(e1): %b%n", e1.equals(e1));
      // 验证对称性。
      System.out.printf("%nDemonstrating symmetry...%n%n");
      System.out.printf("e1.equals(e2): %b%n", e1.equals(e2));
      System.out.printf("e2.equals(e1): %b%n", e2.equals(e1));
      System.out.printf("e1.equals(e3): %b%n", e1.equals(e3));
      System.out.printf("e3.equals(e1): %b%n", e3.equals(e1));
      System.out.printf("e2.equals(e3): %b%n", e2.equals(e3));
      System.out.printf("e3.equals(e2): %b%n", e3.equals(e2));
      // 验证传递性。
      System.out.printf("%nDemonstrating transitivity...%n%n");
      System.out.printf("e1.equals(e3): %b%n", e1.equals(e3));
      System.out.printf("e3.equals(e4): %b%n", e3.equals(e4));
      System.out.printf("e1.equals(e4): %b%n", e1.equals(e4));
      // 验证一致性。
      System.out.printf("%nDemonstrating consistency...%n%n");
      for (int i = 0; i < 5; i++)
      {
         System.out.printf("e1.equals(e2): %b%n", e1.equals(e2));
         System.out.printf("e1.equals(e3): %b%n", e1.equals(e3));
      }
      // 验证传入非空集合时,返回值为false。
      System.out.printf("%nDemonstrating null check...%n%n");
      System.out.printf("e1.equals(null): %b%n", e1.equals(null));
   }
}

代码清单8声明了一个包含名字、年龄成员变量的Employee对象。这个对象覆盖了equals()函数来对Employee对象进行适当的对比。

ps:覆盖hashCode()函数
当覆盖equals()函数的时候,就相当于覆盖了hashCode()函数,我将在下篇文章讨论hashCode()的时候详细说明。

equals()函数首先要检查传入的确实是一个Employee对象。如果不是,返回false。这个检查是靠instanceof运算来判断的,当传入null值的时候,同样也返回false。因此,遵守了“对于任意的非空引用值x,x.equals(null)必须返回假”这个规则。

这样,我们就保证了传入的对象是Employee类型。因为之前的instanceof判断保证了传入值必须是Employee类型的对象,所以在这里我们就不必担心抛出ClassCastException异常了。接下来,equals()方法对两个对象的name和age的值进行了比较。

编译(javac EqualityDemo.java)并运行(java EqualityDemo)代码清单8,你可以看到以下输出结果:

    Demonstrating reflexivity...
    
    e1.equals(e1): true
    
    Demonstrating symmetry...
    
    e1.equals(e2): false
    e2.equals(e1): false
    e1.equals(e3): true
    e3.equals(e1): true
    e2.equals(e3): false
    e3.equals(e2): false
    
    Demonstrating transitivity...
    
    e1.equals(e3): true
    e3.equals(e4): true
    e1.equals(e4): true
    
    Demonstrating consistency...
    
    e1.equals(e2): false
    e1.equals(e3): true
    e1.equals(e2): false
    e1.equals(e3): true
    e1.equals(e2): false
    e1.equals(e3): true
    e1.equals(e2): false
    e1.equals(e3): true
    e1.equals(e2): false
    e1.equals(e3): true
    
    Demonstrating null check...
    
    e1.equals(null): false

equals()和继承

当Employee类被继承的时候,代码清单8就存在一些问题。例如,SaleRep类继承了Employee类,这个类中也有基于字符串类型的变量,equals()可以对其进行比较。假设你创建的Employee对象和SaleRep对象都有相同的“名字”和“年龄”。但是,SaleRep中还是添加了一些内容。

假设你在Employee对象中调用equals()方法并且传入了一个SaleRep对象。由于SaleRep对象继承了Employee,也是一种Employee的对象,instanceof判断会通过,并且执行equals()方法来判断名字和年龄。因为这两个对象有完全相同的名字和年龄,所以equals()方法返回true。如果拿SaleRep对象中Employee的部分来和Employee比较的话,返回true值是正确的,但是,如果拿整个SaleRep对象来和Employee对象比较,返回true值就不妥了。

现在假设在SaleRep对象中调用equals()方法并将Employee传入。因为Employee不是SaleRep类型的对象(否则的话,你可以访问Employee对象中不存在的Region域,这会导致虚拟机崩溃),无法通过instanceof判断,equals()方法返回false。综上,equals()在一种判断中为true却在另一判断中为false,违背了“对称性原则”。

Joshua Bloch在《Effective Java Programming Language Guide》第七版中指出:我们无法扩展可被实例化的类(例如Employee)并向其中增加一个域(如Region域),而同时维持equals()方法的对称性。尽管也有办法来维持对称性,但代价是破坏传递性。Bloch指出解决这个问题需要在继承上支持组合:不是让SaleRep来扩展Employee,SaleRep应该引用一个私有的Employee值。获得更多信息可以参考Bloch的书。

问:可以使用equals()函数来判断两个数组是否相等吗?

答:可以调用equals()函数来比较数组的引用是否相等。但是,由于在数组对象中无法覆盖equals(),所以只能对数组的引用进行比较,因为不是很常用。参见代码清单9。

代码清单9:尝试通过equals()函数来比较两个数组

    public class EqualityDemo
    {
       public static void main(String[] args)
       {
      int x[] = { 1, 2, 3 };
      int y[] = { 1, 2, 3 };
    
      System.out.printf("x.equals(x): %b%n", x.equals(x));
      System.out.printf("x.equals(y): %b%n", x.equals(y));
       }
    }

代码清单9的main()函数中声明了一对类型与内容完全相等的数组。然后尝试对第一个数组和它自己、第一个数组和第二个数组分别进行比较。由于equals()对数组来说比较的仅仅是引用,而不比较内容,所以x.equals(x)返回true(因为自反性——一个对象与它自己相等),但是x.equals(y)返回false。

编译(javac EqualityDemo.java) 并运行(java EqualityDemo)代码清单9,你将会看到以下输出结果:

    x.equals(x): true
    x.equals(y): false

如果你想要比较的是两个数组的内容,也不要绝望。 可以使用java.util.Arrays 类中声明的 static boolean deepEquals(Object[] a1, Object[] a2) 方法来实现。代码清单10演示了这个方法。

代码清单10:通过deepEquals()函数来比较两个数组

    import java.util.Arrays;
    
    public class EqualityDemo
    {
       public static void main(String[] args)
       {
      Integer x[] = { 1, 2, 3 };
      Integer y[] = { 1, 2, 3 };
      Integer z[] = { 3, 2, 1 };
    
      System.out.printf("x.equals(x): %b%n", Arrays.deepEquals(x, x));
      System.out.printf("x.equals(y): %b%n", Arrays.deepEquals(x, y));
      System.out.printf("x.equals(z): %b%n", Arrays.deepEquals(x, z));
       }
    }

由于deepEquals()方法要求传入的数组元素必须是对象,所以之前在代码清单9中的元素类型要从int[]改为Integer[]。Java语言的自动封装特性会把integer常量转换成Integer对象存放在数组中。接下来要将数组传入到deepEquals()就是小事一桩了。

编译(javac EqualityDemo.java)并运行(java EqualityDemo)代码清单10,你将看到以下输出结果。

    x.equals(x): true
    x.equals(y): true
    x.equals(z): false

用deepEquals()方法比较的相等是“深度”的相等:这要求每个元素对象所包含的的成员、对象相等。成员对象如果还包含了对象,也要相等,以此类推,才算是“相等”(另外,两个空的数组引用也是“深度”的相等,因此Arrays.deepEquals(null, null)返回true)。

下期预告:

在第二部分,我将讨论finalize()方法和析构的话题。我也会讨论getClass()方法并指出获得java.lang.Class对象的几种方式,哈希码和hashCode()方法。

原文链接: Javaworld 翻译: ImportNew.com - 赖 信涛
译文链接: http://www.importnew.com/10433.html
[ 转载请保留原文出处、译者和译文链接。]

关于作者: 赖 信涛

(了解我更多,在:赖信涛的个人网站

查看赖 信涛的更多文章 >>



相关文章

发表评论

Comment form

(*) 表示必填项

7 条评论

  1. xmmxjy 说道:

    我是个新人,问问printf()中的%b%n什么意思了 %n换成\n貌似没区别啊,谢谢了

    Thumb up 1 Thumb down 0

    • 赖 信涛 说道:

      你好,在java中% 和 \ 都是转义字符吧,是一样的。都会把n转义成换行。
      我也是查的资料,不知道准确不准确。
      在查的时候,看到以下信息,但是我编译失败了。
      int slen;
      printf("hello world%n", &slen);
      //执行后变量被赋值为11。

      希望高手来解答~~
      我也新手

      Thumb up 0 Thumb down 0

    • xcccc 说道:

      printf()是java格式化输出,和C的printf()很类似,%b表示格式化为boolean值,%n就是换行。类似的例如%c代表字符,%d代表整数。更具体的请参考javaAPI中Formatter类。

      Thumb up 1 Thumb down 0

      • 赖 信涛 说道:

        int slen;
        printf(“hello world%n”, &slen);
        //执行后变量被赋值为11。

        能不能解释一下这段代码呢?为什么运行之后slen的值会被赋为11?
        还是这段代码是错误的? (我是从网上看到的

        谢谢

        Thumb up 0 Thumb down 0

        • xcccc 说道:

          我在网上搜了一下,网上的意思是在C语言中,%n可以将所输出字符串的长度值赋绐一个变量。但是我在我的机器上试过失败了,可能是编译环境的问题?我对C语言只是粗略的了解并不深入,看看其他高手怎么说。

          Thumb up 0 Thumb down 0

  2. 路人甲 说道:

    你好,我想请问一下,关于equals()方法覆盖的代码中,为什么判断了对象是类的实例,下面还需要强转为该类的类型呢?谢谢

    Thumb up 1 Thumb down 0

    • 赖 信涛 说道:

      你好,因为在equals函数中,我们是将对象以Object类的形式传进去的。涉及到多态。
      isinstanceof()方法可以判断这个对象是不是属于Employee类,但是在对其进行向下转型之前,这个对象中属于Employee的成员是不对外暴露的。
      且如果不对其进行转型,在Eclipse中会报错:
      Type mismatch: cannot convert from Object to Employee

      Thumb up 0 Thumb down 0

跳到底部
返回顶部