corejava11(5.2 Object:宇宙超类)

5.2 Object:宇宙超类

Object类是最终祖先——Java中每个类都扩展Object。但是,你不必写

public class Employee extends Object

如果没有明确提到超类,那么最终的超类Object被认为是理所当然的。由于Java中的每一个类都扩展Object,因此熟悉Object类提供的服务是很重要的。我们将详细介绍本章中的基本内容;查阅后面的章节或查看在线文档,了解此处未涉及的内容。(只有在处理并发性时才会出现几个Object方法,请参阅第12章。)

5.2.1 Object类型的变量

可以使用Object类型的变量引用任何类型的对象:

Object obj = new Employee("Harry Hacker", 35000);

当然,一个Object类型的变量只能作为任意值的通用容器使用。要对值执行任何特定操作,您需要了解原始类型并应用强制转换:

Employee e = (Employee) obj;

在Java中,只有基元类型(数字、字符和布尔值)的值不是对象。

所有数组类型,不管它们是对象数组还是基元类型数组,都是扩展Object类的类类型。

Employee[] staff = new Employee[10];
obj = staff; // OK
obj = new int[10]; // OK

C++注意

在C++中,没有宇宙根类。但是,每个指针都可以转换为void*指针。

5.2.2 equals方法

Object类中的equals方法测试一个对象是否被视为等同于另一个对象。在对象类中实现的equals方法确定两个对象引用是否相同。如果两个对象相同,那么这是一个相当合理的默认值,它们应该是相等的。对于相当多的类,不需要其他任何东西。例如,比较两个PrintStream对象是否相等没有什么意义。但是,您通常希望实现基于状态的平等测试,在这种测试中,当两个对象具有相同的状态时,它们被认为是相等的。

例如,如果两个员工的姓名、工资和雇用日期相同,那么让我们认为他们是相等的。(在实际的员工数据库中,比较ID更为明智。我们使用这个例子来演示实现equals方法的机制。)

public class Employee
{
    . . .
    public boolean equals(Object otherObject)
    {
        // a quick test to see if the objects are identical
        if (this == otherObject) return true;
        // must return false if the explicit parameter is null
        if (otherObject == null) return false;
        // if the classes don't match, they can't be equal
        if (getClass() != otherObject.getClass())
            return false;
        // now we know otherObject is a non-null Employee
        Employee other = (Employee) otherObject;
        // test whether the fields have identical values
        return name.equals(other.name)
            && salary == other.salary
            && hireDay.equals(other.hireDay);
    }
}

getClass方法返回一个对象的类,我们将在本章后面详细讨论这个方法。在我们的测试中,只有属于同一类的两个对象才能相等。

提示

要防止name或hireDay为空,请使用Objects.equals方法。调用Objects.equals(a, b)如果两个参数都为空则返回true,如果只有一个参数为空则返回false,否则调用a.equals(b)。使用该方法,Employee.equals方法的最后一条语句将变为

return Objects.equals(name, other.name)
    && salary == other.salary
    && Objects.equals(hireDay, other.hireDay);

5.2.3 相等测试与继承

如果隐式和显式参数不属于同一类,equals方法应该如何操作?这是一个有争议的领域。在前面的示例中,如果类不完全匹配,equals方法将返回false。但是许多程序员使用一个测试实例来代替:

if (!(otherObject instanceof Employee)) return false;

这使得otherObject可能属于子类。然而,这种方法会给你带来麻烦。这就是原因。Java语言规范要求equals方法具有以下属性:

  1. 它是自反的(reflexive):对于任何非空引用x,x.equals(x)都应返回true。
  2. 它是对称的(symmetric):对于任何引用x和y,当且仅当y.equals(x)返回true时,x.equals(y)应返回true。
  3. 它是可传递的(transitive):对于任何引用x、y和z,如果x.equals(y)返回true,y.equals(z)返回true,则x.equals(z)应返回true。
  4. 它是一致的(consistent):如果x和y引用的对象没有更改,那么对x.equals(y)的重复调用将返回相同的值。
  5. 对于任何非空引用x,x.equals(null)应返回false。

这些规则当然是合理的。在数据结构中定位元素时,您不希望库实现人员考虑是调用x.equals(y)还是y.equals(x)。

然而,当参数属于不同的类时,对称性规则会产生细微的影响。考虑调用

e.equals(m)

其中e是Employee对象,m是Manager对象,两者恰好具有相同的名称、薪金和雇用日期。如果Employee.equals使用instanceof测试,则调用返回true。但这意味着反向调用

m.equals(e)

也需要返回true——对称规则不允许返回false或抛出异常。

这使得Manager类处于绑定状态。它的equals方法必须愿意将自己与任何Employee进行比较,而不考虑Manager的具体信息!突然之间,测试的例子看起来不那么吸引人。

一些作者已经记录到getClass测试是错误的,因为它违反了替换原则。一个常见的例子是AbstractSet类中的equals方法,它测试两个集合是否具有相同的元素。AbstractSet类有两个具体的子类TreeSet和HashSet,它们使用不同的算法来定位集合元素。您真的希望能够比较任何两个集合,不管它们是如何实现的。

但是,集合示例相当专业。将AbstractSet.equals声明为final是有意义的,因为没有人应该重新定义set相等的语义。(该方法实际上不是final的。这允许子类为相等性测试实现更有效的算法。)

在我们看来,有两种不同的情况:

  • 如果子类可以有自己的相等概念,那么对称性要求强制您使用getClass测试。
  • 如果相等的概念在超类中是固定的,那么可以使用instanceof测试并允许不同子类的对象彼此相等。

在使用员工和经理的示例中,我们认为两个对象在具有匹配字段时是相等的。如果我们有两个Manager对象具有相同的名称、薪金和雇用日期,但奖金不同,我们希望它们不同。因此,我们使用getClass测试

但是假设我们使用员工ID进行相等测试。相等的概念对所有的子类都有意义。然后我们可以使用instanceof测试,我们应该声明Employee.equals为final。

注意

标准Java库包含了150种实现equals方法,其中使用了instanceof,调用GetClass,捕获ClassCastException,或者什么也不做。查看java.sql.Timestamp类的API文档,在该文档中,实现人员会尴尬地注意到他们已经将自己画在了一个角落里。Timestamp类继承自java.util.Date,其equals方法使用instanceof测试,因此不可能重写equals,使其既对称又准确。

以下是编写完美equals方法的方法:

  1. 将显式参数命名为otherObject,稍后您将需要将其强制转换为应调用other的另一个变量。

  2. 测试this是否与otherObject相同:

    if (this == otherObject) return true;
    
    

    这个语句只是一个优化。在实践中,这是一个常见的情况。检查身份要比比较字段便宜得多。

  3. 测试otherObject是否为空,如果为空则返回false。此测试是必需的。

    if (otherObject == null) return false;
    
    
  4. 比较this对象和otherObject的类。如果equals的语义可以在子类中更改,请使用getClass测试:

    if (getClass() != otherObject.getClass()) return false;
    
    

    如果所有子类都具有相同的语义,则可以使用instanceof测试:

    if (!(otherObject instanceof ClassName)) return false;
    
    
  5. 将otherObject强制转换为你的类类型的变量:

    ClassName other = (ClassName) otherObject
    
    
  6. 现在,根据您的相等概念,比较字段。对于基本类型字段使用==,对于对象字段使用Objects.equals。如果所有字段匹配,则返回“true”,否则返回“false”。

    return field1 == other.field1
        && Objects.equals(field2, other.field2)
        && . . .;
    
    

    如果在子类中重新定义equals,请包括对super.equals(other)的调用。

提示

如果有数组类型的字段,则可以使用静态的Array.equals方法检查相应的数组元素是否相等。

小心

这是实现equals方法时的一个常见错误。你能找出问题所在吗?

public class Employee
{
    public boolean equals(Employee other)
    {
        return other != null
            && getClass() == other.getClass()
            && Objects.equals(name, other.name)
            && salary == other.salary
            && Objects.equals(hireDay, other.hireDay);
    }
    . . .
}

此方法将显式参数类型声明为Employee。因此,它不会重写对象类的equals方法,而是定义一个完全不相关的方法。

您可以通过标记要用@Override重写超类方法的方法来防止此类错误:

@Override public boolean equals(Object other)

该方法报错,因为此方法不重写对象超类中的任何方法。

java.util.Array 1.2

  • static boolean equals(xxx[] a, xxx[] b) 5
    如果数组的长度相等,且元素在相应位置相等,则返回true。数组的组件类型xxx可以是Object、int、long、short、char、byte、boolean、float或double。

java.util.Objects 7

  • static boolean equals(Object a, Object b)
    返回true如果a和b都是null,false如果它们中其中一个是null,否则返回a.equals(b)

5.2.4 hashCode方法

hashCode是从一个Object继承的整数。如果x和y是两个不同的对象,那么应该对哈希代码进行打乱。很可能x.hashCode()和y.hashcode()是不同的。表5.1列出了由String类的hashCode方法产生的hashCode的一些示例。

表5.1 hashCode方法的Hash Code结果

String Hash Code
Hello 69609650
Harry 69496448
Hacker -2141031506

String类使用下面的算法来计算hash code

int hash = 0;
for (int i = 0; i < length(); i++)
    hash = 31 * hash + charAt(i);

hashCode方法是在Object类中定义的。因此,每个对象都有一个默认的哈希代码。该哈希代码是从对象的内存地址派生的。考虑这个例子:

var s = "Ok";
var sb = new StringBuilder(s);
System.out.println(s.hashCode() + " " + sb.hashCode());
var t = new String("Ok");
var tb = new StringBuilder(t);
System.out.println(t.hashCode() + " " + tb.hashCode());

表5.2显示了结果

表5.2 String和String Builders的Hash Codes

Object Hash Code Object Hash Code
s 2556 t 2556
sb 20526976 tb 20527144

请注意,字符串s和t具有相同的哈希代码,因为对于字符串,哈希代码是从其内容派生的。字符串生成器sb和tb具有不同的哈希代码,因为没有为StringBuilder类定义哈希代码方法,对象类中的默认哈希代码方法从对象的内存地址派生哈希代码。

如果重新定义equals方法,还需要为用户可能插入哈希表的对象重新定义hashCode方法。(我们在第9章讨论哈希表。)

hashCode方法应返回一个整数(可以是负数)。只需将实例字段的哈希代码组合起来,就可以使不同对象的哈希代码广泛分散。

例如,下面是Employee类的hashCode方法:

public class Employee
{
    public int hashCode()
    {
        return 7 * name.hashCode()
            + 11 * new Double(salary).hashCode()
            + 13 * hireDay.hashCode();
    }
    . . .
}

但是,你可以做得更好。首先,使用空安全方法Objects.hashCode。如果参数为null,则返回0,否则返回对该参数调用哈希代码的结果。另外,使用static Double.hashCode方法避免创建Double对象:

public int hashCode()
{
    return 7 * Objects.hashCode(name)
        + 11 * Double.hashCode(salary)
        + 13 * Objects.hashCode(hireDay);
}

更好的是,当您需要组合多个哈希值时,可以调用Objects.hash及其所有值。它将为每个参数调用Objects.hashCode并组合这些值。那么Employee.hashCode方法就是

public int hashCode()
{
	return Objects.hash(name, salary, hireDay);
}

equals和hashCode的定义必须兼容:如果x.equals(y)为true,则x.hashCode()必须返回与y.hashCode()相同的值。例如,如果定义Employee.equals来比较雇员ID,那么hashCode方法需要散列ID,而不是雇员名称或内存地址。

提示

如果有数组类型的字段,可以使用静态Array.hashCode方法计算由数组元素的哈希代码组成的哈希代码。

java.lang.Object 1.0

  • int hashCode()
    返回此对象的哈希代码。哈希代码可以是任何整数、正数或负数。相等的对象需要返回相同的哈希代码。

java.util.Objects 7

  • static int hash(Object… objects)
    返回从所有提供的对象的哈希代码组合而成的哈希代码。
  • static int hashCode(Object a)
    如果a为空,则返回0,否则返回a.hashCode()。

java.lang.(Integer|Long|Short|Byte|Double|Float|Character|Boolean) 1.0

  • static int hashCode(xxx value) 8
    返回给定值的哈希代码。这里,xxx是对应于给定包装器类型的原语类型。

java.util.Arrays 1.2

  • static int hashCode(xxx[] a) 5
    计算数组A的哈希代码。数组的组件类型xxx可以是Object、int、long、short、char、byte、boolean、float或double。

5.2.5 toString方法

Object中的另一个重要方法是toString方法,它返回一个表示此对象值的字符串。这是一个典型的例子。Point类的toString方法返回如下字符串:

java.awt.Point[x=10,y=20]

大多数(但不是全部)toString方法遵循以下格式:类的名称,然后是括在方括号中的字段值。下面是Employee类的toString方法的实现:

public String toString()
{
    return "Employee[name=" + name
        + ",salary=" + salary
        + ",hireDay=" + hireDay
        + "]";
}

实际上,你可以做得更好。不是将类名硬连接到toString方法中,而是调用getClass().getName()以获取具有类名的字符串。

public String toString()
{
    return getClass().getName()
        + "[name=" + name
        + ",salary=" + salary
        + ",hireDay=" + hireDay
        + "]";
}

这种toString方法也适用于子类。

当然,子类程序员应该定义自己的toString方法并添加子类字段。如果超类使用getClass().getName(),那么子类可以简单地调用super.toString()。例如,下面是Manager类的toString方法:

public class Manager extends Employee
{
    . . .
    public String toString()
    {
        return super.toString()
            + "[bonus=" + bonus
            + "]";
    }
}

现在,Manager对象打印为

Manager[name=. . .,salary=. . .,hireDay=. . .][bonus=. . .]

toString方法是普遍存在的一个重要原因:每当对象由“+”操作符与字符串连接时,Java编译器自动调用toString方法来获得对象的字符串表示形式。例如:

var p = new Point(10, 20);
String message = "The current position is " + p;
	// automatically invokes p.toString()

提示

您可以编写"" + x,而来代替编写x.toString()。此语句将空字符串与x的字符串表示形式连接起来,x的字符串表示形式正好是x.toString()。与toString不同,如果x是基元类型,则此语句还是有效。

如果x是任何对象,并且您调用

System.out.println(x);

然后println方法简单地调用x.toString()并打印结果字符串。

Object类定义toString方法以打印对象的类名和哈希代码。例如,调用

System.out.println(System.out)

生成如下所示的输出:

java.io.PrintStream@2f6684

原因是PrintStream类的实现程序不需要重写toString方法。

小心

令人讨厌的是,数组从对象继承了toString方法,增加了数组类型古怪的打印的格式。例如,

int[] luckyNumbers = { 2, 3, 5, 7, 11, 13 };
String s = "" + luckyNumbers;

生成字符串“[I@1A46E30”。(前缀[i表示整数数组。)补救方法是调用静态Array.toString方法。代码

String s = Arrays.toString(luckyNumbers);

生成字符串“[2,3,5,7,11,13]”。

要正确打印多维数组(即数组数组),请使用Array.deepToString。

toString方法是一种很好的日志工具。标准类库中的许多类都定义了toString方法,这样您就可以获得有关对象状态的有用信息。这在记录如下消息时特别有用:

System.out.println("Current position = " + position);

正如我们在第7章中所解释的,更好的解决方案是使用Logger类的对象并调用

Logger.global.info("Current position = " + position);

提示

我们强烈建议您向编写的每个类添加toString方法。您以及其他使用类的程序员将感谢日志支持。

清单5.8中的程序测试类Employee(清单5.9)和Manager(清单5.10)的equals、hashCode和toString方法。

清单5.8 equals/EqualsTest.java

package equals;

/**
 * This program demonstrates the equals method.
 * @version 1.12 2012-01-26
 * @author Cay Horstmann
 */
public class EqualsTest
{
   public static void main(String[] args)
   {
      var alice1 = new Employee("Alice Adams", 75000, 1987, 12, 15);
      var alice2 = alice1;
      var alice3 = new Employee("Alice Adams", 75000, 1987, 12, 15);
      var bob = new Employee("Bob Brandson", 50000, 1989, 10, 1);

      System.out.println("alice1 == alice2: " + (alice1 == alice2));

      System.out.println("alice1 == alice3: " + (alice1 == alice3));

      System.out.println("alice1.equals(alice3): " + alice1.equals(alice3));

      System.out.println("alice1.equals(bob): " + alice1.equals(bob));

      System.out.println("bob.toString(): " + bob);

      var carl = new Manager("Carl Cracker", 80000, 1987, 12, 15);
      var boss = new Manager("Carl Cracker", 80000, 1987, 12, 15);
      boss.setBonus(5000);
      System.out.println("boss.toString(): " + boss);
      System.out.println("carl.equals(boss): " + carl.equals(boss));
      System.out.println("alice1.hashCode(): " + alice1.hashCode());
      System.out.println("alice3.hashCode(): " + alice3.hashCode());
      System.out.println("bob.hashCode(): " + bob.hashCode());
      System.out.println("carl.hashCode(): " + carl.hashCode());
   }
}

清单5.9 equals/Employee.java

package equals;

import java.time.*;
import java.util.Objects;

public class Employee
{
   private String name;
   private double salary;
   private LocalDate hireDay;

   public Employee(String name, double salary, int year, int month, int day)
   {
      this.name = name;
      this.salary = salary;
      hireDay = LocalDate.of(year, month, day);
   }

   public String getName()
   {
      return name;
   }

   public double getSalary()
   {
      return salary;
   }

   public LocalDate getHireDay()
   {
      return hireDay;
   }

   public void raiseSalary(double byPercent)
   {
      double raise = salary * byPercent / 100;
      salary += raise;
   }

   public boolean equals(Object otherObject)
   {
      // a quick test to see if the objects are identical
      if (this == otherObject) return true;

      // must return false if the explicit parameter is null
      if (otherObject == null) return false;

      // if the classes don't match, they can't be equal
      if (getClass() != otherObject.getClass()) return false;

      // now we know otherObject is a non-null Employee
      var other = (Employee) otherObject;

      // test whether the fields have identical values
      return Objects.equals(name, other.name) 
         && salary == other.salary && Objects.equals(hireDay, other.hireDay);
   }

   public int hashCode()
   {
      return Objects.hash(name, salary, hireDay); 
   }

   public String toString()
   {
      return getClass().getName() + "[name=" + name + ",salary=" + salary + ",hireDay=" 
         + hireDay + "]";
   }
}

清单5.10 equals/Manager.java

package equals;

public class Manager extends Employee
{
   private double bonus;

   public Manager(String name, double salary, int year, int month, int day)
   {
      super(name, salary, year, month, day);
      bonus = 0;
   }

   public double getSalary()
   {
      double baseSalary = super.getSalary();
      return baseSalary + bonus;
   }

   public void setBonus(double bonus)
   {
      this.bonus = bonus;
   }

   public boolean equals(Object otherObject)
   {
      if (!super.equals(otherObject)) return false;
      var other = (Manager) otherObject;
      // super.equals checked that this and other belong to the same class
      return bonus == other.bonus;
   }

   public int hashCode()
   {
      return java.util.Objects.hash(super.hashCode(), bonus);
   }

   public String toString()
   {
      return super.toString() + "[bonus=" + bonus + "]";
   }
}

java.lang.Object 1.0

  • Class getClass()
    返回包含有关该对象的信息的类对象。正如您将在本章后面看到的,Java对类类中封装的类具有运行时表示形式。
  • boolean equals(Object otherObject)
    比较两个对象是否相等;如果对象指向同一内存区域,则返回true,否则返回false。您应该在自己的类中重写这个方法。
  • String toString()
    返回表示此对象值的字符串。您应该在自己的类中重写这个方法。

java.lang.Class 1.0

  • String getName()
    返回此类的名称。
  • Class getSuperclass()
    将此类的超类作为类对象返回。

猜你喜欢

转载自blog.csdn.net/nbda1121440/article/details/90665727