Object——所有类的超类

在Java中,有一个类的始祖,它就是Object类,在java中每个类都扩展了Object。所有类包括自定义的类吗?比如自己写的一个员工类:

class Employee
{
    ...
}

这里并没有写成

class Employee extends Object
{
    ...
}

Java规定,如果一个类没有明确指出超类(即没有使用extends),那么它的超类就是Object。

Object与其他派生类转换

因为所以类都派生自Object,故所有的类都可以转换为Object类。

Object obj = null;
String str = "java";
int[] arr = {3,23,4};

obj = str; 
obj = arr;

那么Object可以转换为其他类么,可以但必须使用强制类型转换,而且不一定能成功。下面看成功转换的案例:

Object obj1 = "java";
Object obj2 = new int[]{3,23,1};

String str = (String) obj1;
int[] arr = (int[]) obj2;

上述是没有问题的,可以正常编译及执行。但是看看下面的代码,它就不能编译成功了

Object obj1 = "java";
Object obj2 = new int[]{3,23,1};

String str = (String) obj2; // error
int[] arr = (int[]) obj1;  // error

结合上述两个例子,可以推断出,当Object变量存的数据内容符合强制转换后的变量类型,才可以转换成功。

下面介绍Object的几个常见方法:

equals方法

Object中有一个equals方法,用来测试两个对象是否相等。该方法判断两个对象相等的条件是,两个对象的引用是否相等。如果两个对象的引用相等的话,那么毋庸置疑这两个对象一定相等。

但是,我们经常需要对对象的字段进行比较,如果两个对象具有相同的字段值,就认为这两个对象相等。比如,有两个员工姓名、年龄以及薪水相等的话,就认为他们相等。

下面我们来重写父类的equals方法,不再比较引用,而是比较几个字段值。

package com.studyjava.demo;

import java.util.Objects;

class Employee
{
    private String name;
    private int age;
    private double salary;

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

    public String getName ()
    {
        return this.name;
    }

    public int getAge ()
    {
        return this.age;
    }

    public double getSalary ()
    {
        return this.salary;
    }

    public boolean equals (Object other)
    {
        if (this == other) {
            return true;
        }

        if (other == null) {
            return false;
        }

        if (this.getClass() != other.getClass()) {
            return false;
        }

        Employee otherE = (Employee) other;

        if (Objects.equals(this.name, otherE.name) 
             && this.age == otherE.age
             && this.salary == otherE.salary) {
            return true;
        } else {
            return false;
        }
    }
}

这里我们刚开始使用了“==”来比较两个对象。补充“==”知识点:

  • java中的基本数据类型判断是否相等,直接使用"=="就行了,相等返回true,否则,返回false。
  • 但是java中的引用类型的对象比较变态,假设有两个对象obj1,obj2, obj1==obj2 判断是obj1,obj2这两个变量的引用是否相等,即它们所指向的是否为同一个对象(和Object.equals判断方法一致)。
  • 在Java API中,有些类重写了equals()方法,它们的比较规则是:当且仅当该equals方法参数不是 null,两个变量的类型、内容都相同,则比较结果为true。这些类包括:String、Double、Float、Long、Integer、Short、Byte、、Boolean、BigDecimal、BigInteger等等,太多太多了,但是常见的就这些了,具体可以查看API中类的equals()方法,就知道了。

好了,现在我们来分析这个重写的equals方法。

  • 注意参数的类型必须是Object。如果写成equals(Employee other),则不会覆盖Object类的equals方法,而是定义了一个完全无关的方法。
  • 第一个if用了“==”来判断这两个变量是否引用同一对象。如果引用同一变量,那么肯定相等。
  • 第二个if用来判断other是否为null,如果为null,那么返回false。这是因为,this肯定不为null,如果为null的话就会抛出异常。
  • 第三个条件判断是否同属一个类,如果不是同属一个类,那么就需返回false。
  • 接着就需对字段值进行比较了。这里用了Objects.equals()来比较name属性,为什么不直接用this.name.equals(otherE.name)?这是防止this.name如果为null的话会抛出异常。

Objects类中的equals方法用于检测一个对象是否等于另一个对象。

if (a.equals(b)) {......}

这里,为了防止a可能为null的情况,可以换成Objects.equals方法。如果a和b都是null,则Objects(a,b)将返回true,若其中一个为null,则返回false,若都不是null,则返回a.equals(b)。

下面,我们把Employee的子类Manager的equals方法也重写下:

package com.studyjava.demo;

class Manager extends Employee
{
    private double bonus;

    public Manager (String name, int age, double salary) 
    {
        super(name, age, salary);
        this.bonus = 0;
    }

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

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

    public boolean equals (Object manager)
    {
        if (!super.equals(manager)) {
            return false;
        }

        Manager mng = (Manager) manager;
        return this.bonus == mng.bonus;
    }
}

相等测试与继承

如果a.equals(b),a和b不属于同一个类,但他们之间是派生关系,equals该如何处理呢?这是一个非常值得讨论的话题。争论的核心在于是用getClass()检测是否为同一个类,还是用intanceof检测是否为继承关系。

java官方对equals方法有一个对称性的要求,即a.equals(b)为true,那么b.equals(a)也应该为true。这是非常合理的,因为我们不想在使用equals时还需要考虑用a.equals(b)还是b.equals(a)。

假如在之前的例子中,将Employee中equals方法中的

this.getClass() != other.getClass()

修改为

other instanceof this

那么,如果e.equals(m)返回true,(e为Employee实例,m为Manager实例)。那么为满足对称性,要使m.equals(e)也返回true。这样就导致一个问题,Manager中的equals就收到了限制。该equals方法必须愿意将自己与任何一个Employee对象进行比较,而不考虑经理特有的那部分信息。

关于是使用intanceof还是使用getClass,下面给出最佳实践:

  • 如果子类可以有自己的相等性概念,则对称性需求将强制使用getClass检测。
  • 如果由超类决定相等性概念,那么就可以使用instanceof检测,这样就可以在不同子类的对象之间进行相等性比较。

在员工和经理的例子中,只要相应的字段相等,就认为两个对象相等。如果两个Manager对象的姓名、年龄以及薪水均相等,而奖金不相等,就认为它们是不同的,因此我们要使用getClass检测。

但如果假设使用员工的ID作为相等性检测标准,并且这个相等性概念适用于所有子类,就可以使用instanceof检测,而且应该将Employee.equals声明为final

hashCode方法

散列码(hash code)是由对象导出的一个整型值。散列码是没有规律的。如果重新定义了equals方法,就必须为用户可能插入散列表的对象重新定义hashCode方法。

equals与hashCode的定义必须相容,如果x.equals(y)返回true,那么x.hashCode就必须与y.hashCode返回相同的值。例如,如果定义Employee.equals比较员工的ID,那么hashCode方法就需要散列ID,而不是员工的姓名或存储地址。

下面,我们来完成Employee的hashCode方法。

public int hashCode ()
{
    return 3 * Objects.hashCode(this.name)
        + 5 * Objects.hashCode(this.age)
        + 7 * Objects.hashCode(this.salary);
}

这里使用Objects.hashCode而不是使用name.hashCode等是为了防止name为null情况。Objects.hashCode(a),若a为null则返回0;

还有更好的方法,需要组合多个散列值时,可以调用Objects.hash,并提供若干参数,该方法原型为

static int hash(Object ...objects)

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

最后,再完成下Manager类的hashCode方法。

public int hashCode ()
{
    return super.hashCode() + Objects.hashCode(this.bonus);
}

toString方法

toString是一个非常常用的调试方法。它返回一个表示对象值的一个字符串。

默认返回格式如下:对象的class名称 + @ + hashCode的十六进制字符串

clone

protected native Object clone() throws CloneNotSupportedException;

这个方法用于克隆对象。被克隆的对象必须实现java.lang.Cloneable接口,否则会抛出CloneNotSupportedException异常。

默认的clone方法是浅拷贝模式。所谓浅拷贝,指的是对象内属性引用的对象只会拷贝引用地址,而不会将引用的对象重新分配内存。深拷贝则是会连引用的对象也重新创建。

getClass

public final native Class<?> getClass();。获取对象的运行时class对象,class对象就是描述对象所属类的对象。这个方法通常是和Java反射机制搭配使用的。