一、介绍
通过如何创建和销毁对象一文,我们已经知道Java是一个面向对象的编程语言,Java类层次结构的顶部是Object类,Java中每个单独的类都隐式的继承于该类。因此,所有的类都继承了在Object类中定义的一系列方法,这其中的一些非常重要的方法如下:

方法

描述

protected Object clone()

创建并返回对象的一个副本

protected void finalize()

当垃圾回收确定指定对象不可达时(没有引用),该方法就会被垃圾收集器调用

boolean equals(Object obj)

判定一个对象是否“等于”另外一个对象

int hashCode()

返回对象的hash code值

String toString()

返回对象的字符串表示

void notify()

唤醒正在等待该对象同步锁的单个线程(只唤醒一个,如果有多个在等待)

void notifyAll()

唤醒所有等待该对象同步锁的线程

void wait()

void wait(long timeout)

void wait(long timeout, int nanos)

调用该方法将会导致当前线程阻塞,直到其他的线程在该对象上调用notify()或者notifyAll()方法时当前线程才会被唤醒

本文我们只介绍equals,hashCode,toString和clone方法的使用,其他的方法我们将会在后续的多线程最佳实战的文章中细说。

二、equals和hashCode方法

默认情况下,Java中的任何两个对象引用(或类实例引用)只有在引用相同的内存位置(引用相等)的情况下才是相等的。但是Java允许类通过覆盖(overriding)Object类的equals()方法定义自己的相等规则。这听起来是很强大的概念,然而正确的equals()方法实现应该符合一些规则和满足以下的一些限制:

  • 自反性 Reflexive:对象x必须等于它自己并且x.equals(x)必须返回true;
  • 对称性 Symmetric:如果x.equals(y)返回true,那么y.equals(x)也必须返回true;
  • 传递性 Transtive:如果x.equals(y)返回true并且y.equals(z)返回true,那么x.equals(z)也必须返回true;
  • 一致性 Consistent:equals方法的多次调用必须返回相同的结果值,除非用于相等比较的属性被改变;
  • 和Null比较:任何非null x,x.equals(null)返回false;

不幸的是,Java编译器在编译处理过程中不能强制这些限制。然而,不遵守这些规则将会导致非常奇怪和难以解决的问题。一般我们建议:如果你需要写你自己的equals()方法实现,请再三思考是否你真的需要实现个性化的equals()方法;现在,根据这些规则,让我们为Person类写一个简单的equals()方法实现。

package javaTec;

public class Person {
    private final String firstName;
    private final String lastName;
    private final String email;

    public Person(final String firstName, final String lastName, final String email) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.email = email;
    }

    public String getEmail() {
        return email;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }

    /**
     * @Comment Step 0:添加@Override注解,这能够确保你的意图是修改默认实现
     * @Author Ron
     * @Date 2018年1月2日 下午4:41:39
     * @return
     */
    @Override
    public boolean equals(Object obj) {
        // Step 1: 检查obj是否为null
        if (obj == null) {
            return false;
        }

        // Step 2: 检查obj是否指向当前实例
        if (this == obj) {
            return true;
        }

        // Step 3: 检查类相等。请注意这里:不要使用'instanceof'关键字,除非类被定义为final。因为它可能会导致一些类层次上的问题。
        if (getClass() != obj.getClass()) {
            return false;
        }

        // Step 4: 检查单独的字段是否相等
        final Person other = (Person) obj;
        if (email == null) {
            if (other.email != null) {
                return false;
            }
        } else if (!email.equals(other.email)) {
            return false;
        }
        if (firstName == null) {
            if (other.firstName != null) {
                return false;
            }
        } else if (!firstName.equals(other.firstName)) {
            return false;
        }
        if (lastName == null) {
            if (other.lastName != null) {
                return false;
            }
        } else if (!lastName.equals(other.lastName)) {
            return false;
        }
        return true;
    }
}

在本节我们包含了hashCode()方法在我们的标题中,这并不是偶然。最后但同样重要且需要记住的规则就是:当你覆盖了equals()方法的时候,一般也是需要覆盖hashCode()方法。如果对于任意两个对象来说equals()方法返回true,然后这两个对象中任意一个对象的hashCode()方法所返回的是相同的整数值(然而相反的情况就并没有那么严格的要求:如果对于任意两个对象来说equals()方法返回false,然后这两个对象中任意一个对象的hashCode()方法所返回的整数值有可能相同也有可能不同)。让我们为Person类覆盖一个hashCode()方法。

/**
* @Comment 添加@Override注解,这能够确保你的意图是修改默认实现
* @Author Ron
* @Date 2018年1月2日 下午6:06:40
* @return
*/
@Override
public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + ( ( email == null ) ? 0 : email.hashCode() );
    result = prime * result + ( ( firstName == null ) ? 0 : firstName.hashCode() );
    result = prime * result + ( ( lastName == null ) ? 0 : lastName.hashCode() );
    return result;
}

为了避免一些意外情况的发生,在实现equals()方法和hashCode()方法时尽量使用final字段。这样会保证方法的操作不会受字段值变化的影响(然而,在现实项目中这几乎是不可能)。

最后,在实现equals()方法和hashCode()方法时一定要确认相同的字段已经被使用过,这样一旦遇到任何影响到字段的变化问题,才能保证两个方法的一致性操作。

三、toString方法

toString()方法相对其他方法来看可以说是最有趣的了,它正在被更频繁地覆盖。该方法的主要意图就是提供对象(类实例)的字符串表示方式。正确编写的toString()方法可以大大简化现实系统中调试和故障排除的过程。

在大多数情况下toString()的默认实现并没有太大的用处并且它仅仅只是返回一个全类名和对象的hash code,由’@’符号分割,例如:

com.javacodegeeks.advanced.objects.Person@6104e2ee

让我们尝试一下为Person类重写一个toString()方法改善toString()方法的实现。这是让toString()方法发挥作用的一种方式。

/**
* @Comment 添加@Override注解,这能够确保你的意图是修改默认实现
* @Author Ron
* @Date 2018年1月3日 上午11:49:16
* @return
*/
@Override
public String toString() {
    return String.format( "%s[email=%s, first name=%s, last name=%s]",
    getClass().getSimpleName(), email, firstName, lastName );
}

现在,Person类的toString()方法就提供了Person类实例的包含所有字段的字符串版本。例如,当执行如下代码段时:

final Person person = new Person( "John", "Smith", "john.smith@domain.com" );
System.out.println( person.toString() );

将会输出如下结果:

Person[email=john.smith@domain.com, first name=John, last name=Smith]

不幸的是,标准Java库对简化toString()方法实现的支持有限,很明显,大多数有用的方法都是Objects.toString()、Arrays.toString()/ Arrays.deepToString()。让我们来看一下Office类和它的一个可能的toString()方法实现。

package javaTec;

import java.util.Arrays;

public class Office {
    private Person[] persons;

    public Office(Person... persons) {
        this.persons = Arrays.copyOf(persons, persons.length);
    }

    @Override
    public String toString() {
        return String.format("%s{persons=%s}", getClass().getSimpleName(), Arrays.toString(persons));
    }

    public Person[] getPersons() {
        return persons;
    }
}

当执行如下代码段时:

public static void main(String[] args) {
    final Person person = new Person( "John", "Smith", "john.smith@domain.com" );
    final Person personTwo = new Person( "Ron", "Zheng", "john.smith@domain.com" );
    Office office = new Office(person,personTwo);

    System.out.println( office.toString() );
}

将会出现如下结果:

Office{persons=[Person[email=john.smith@domain.com, first name=John, last name=Smith],
Person[email=john.smith@domain.com, first name=Ron, last name=Zheng]]}

Java社区已经开发了一些非常全面的库,这些库有助于使toString()实现变得容易和轻松。其中就有 Google Guava’s Objects.toStringHelper和 Apache Commons Lang ToStringBuilder。

四、clone方法

如果说在Java中那个方法的名声最臭,那一定是clone()。此方法的意图非常清晰——返回调用该方法的实例的一个正确的副本,然而有很多的原因导致它并没有听起来那么容易。

  • 首先,假如你决定实现你自己的clone()方法,有很多在Java文档中规定的约定你需要遵守。
  • 其次,clone()方法在Object类中定义为protected类型,因此为了让该方法可见,那么它应该被重写为public,并且返回类型应该是重写类本身。
  • 第三、重写类应该实现Cloneable接口(这只是一个标记接口,接口中并没有定义方法),否则会报CloneNotSupportedException异常。
  • 最后、实现首先需要调用 super.clone()然后才执行其他额外所需的操作。让我们来看一下我们的Person类如何实现clone()方法。
public class Person implements Cloneable {
    /**
    * @Comment 添加@Override注解,这能够确保你的意图是修改默认实现
    * @Author Ron
    * @Date 2018年1月3日 下午2:40:10
    * @return
    */
    @Override
    public Person clone() throws CloneNotSupportedException {
        return ( Person )super.clone();
    }
}

这个实现看起来相当的简单和直接,那么这个地方哪儿容易出错呢?当正在执行类实例的克隆时,不会调用任何类构造函数。 这种行为的后果是可能会出现无意的数据共享。让我们考虑下一个在上一节介绍的Office类的例子:

public class Office implements Cloneable {
    private Person[] persons;

    public Office( Person ... persons ) {
        this.persons = Arrays.copyOf( persons, persons.length );
    }

    @Override
    public Office clone() throws CloneNotSupportedException {
        return ( Office )super.clone();
    }

    public Person[] getPersons() {
        return persons;
    }
}

在这个实现中,Office类实例的所有克隆将会共享相同的persons数组,这不太可能是我们克隆所期望的结果。为了让clone()能够完成正确的操作,我们还需要完成一些工作。

@Override
public Office clone() throws CloneNotSupportedException {
    final Office clone = ( Office )super.clone();
    clone.persons = persons.clone();
    return clone;
}

这样看起来就会稍微好一点,但是即使是这样的实现也是很脆弱的,因为如果persons字段的是final类型,那么同样会导致相同数据共享问题(final不能再分配);

总的来说,如果你想精确的拷贝类实例,可能最好的方法是避免使用clone()和Cloneable,选择其他更简单的方法实现(比如,拷贝构造函数,对具有C++背景的开发者来说这是相当熟悉的概念;或者使用工厂方法,我们在Java高级系列——如何创建和销毁对象一文中有讨论过这个非常有用的构造模式)。

五、equals方法和==操作符

Java的==操作符和equals()方法之间有一个非常有趣的关系,这个关系将会导致很多问题和困惑。在大多数情况下(除了比较原始基本类型),==操作符比较的是引用相等:如果两个引用指向相同的对象,那么他就返回true,否则就返回false。让我们通过实例的形式来阐明他们的不同:

final String str1 = new String( "bbb" );
System.out.println( "Using == operator: " + ( str1 == "bbb" ) );
System.out.println( "Using equals() method: " + str1.equals( "bbb" ) );

从我们人类的角度来看,str1==”bbb”和str1.equals(“bbb”)并没有什么不同:两种情况下的结果都应该是相同的,因为str1仅仅只是引用了’bbb’字符串。但是在Java中并不是这样,我们看上面代码段的执行结果:

Using == operator: false
Using equals() method: true

虽然两个字符串看起来完全一样,在这个特殊的例子中其实存在两个不同的字符串实例。根据经验,如果你处理对象引用,一般情况下都是使用equals()或者Objects.equals()去做相等比较,除非你真的是有意的去比较是否两个对象的引用是指向同一个实例。

六、有用的helper类

自Java 7发布以来,就有很多非常有用的helper类包含在标准类库中,其中之一就是Object类。尤其是下面的这三个方法能够大大的简化你自己的equals()和hashCode()方法实现。

Method

Description

static boolean equals(Object a, Object b)

如果两个参数相等则返回true,否则返回false

static int hash(Object…values)

为输入的值序列生成hash code

static int hashCode(Object o)

返回非null参数的hash code,如果参数为null,则返回0

如果我们使用helper方法为我们的Person类重写equals()和hashCode()方法,代码数量就会少很多,甚至代码可读性也能够得到提升。

/**
 * @Comment 添加@Override注解,这能够确保你的意图是修改默认实现
 * @Author Ron
 * @Date 2018年1月3日 上午11:49:16
 * @return
*/
@Override
public boolean equals(Object obj) {
    if (obj == null) {
        return false;
    }
    if (this == obj) {
        return true;
    }
    if (getClass() != obj.getClass()) {
        return false;
    }
    final Person other = (Person) obj;
    if (!Objects.equals(email, other.email)) {
        return false;
    } else if (!Objects.equals(firstName, other.firstName)) {
        return false;
    } else if (!Objects.equals(lastName, other.lastName)) {
        return false;
    }
    return true;
}

/**
 * @Comment 添加@Override注解,这能够确保你的意图是修改默认实现
 * @Author Ron
 * @Date 2018年1月3日 上午11:49:16
 * @return
*/
@Override
public int hashCode() {
    return Objects.hash(email, firstName, lastName);
}

在本文中,我们讨论了Object类,它是Java中面向对象编程的基础。我们已经看到每个类如何覆盖从Object类继承的方法并强加自己的相等规则。在接下来的文章中我们将会扭转我们的视角并且讨论如何设计更适合我们的类和接口。