工学1号馆

home

« | 返回首页 | »

java高级教程系列2--使用类的公共方法

By Wu Yudong on November 04, 2015

在java中,Object 处在类的最顶层,其他类都继承它,于是所有类都继承了 Object类的方法, 最重要的几个如下:

Method Description
protected Object clone() Creates and returns a copy of this object.
protected void finalize() Called by the garbage collector on an object when garbage collection determines that there are no more references to the object.
boolean equals(Object obj) Indicates whether some other object is “equal to” this one.
int hashCode() Returns a hash code value for the object.
String toString() Returns a string representation of the object.
void notify() Wakes up a single thread that is waiting on this object’s monitor.
void notifyAll() Wakes up all threads that are waiting on this object’s monitor.
void wait()void wait(long timeout)void wait(long timeout, int nanos) Causes the current thread to wait until another thread invokes the notify() method or the notifyAll() method for this object.

Table 1

在这部分教程,我们将看看 equalshashCodetoStringclone 方法, 记住它们的用法和重要的约束。

1. 方法的 equals 和 hashCode

默认情况下,在Java中任意两个对象引用(或类实例的引用)只有当他们指向相同的内存位置(引用相等)是相等的。但是Java允许类通过覆盖Object类的equals()方法来定义自己的相等规则。这听起来像一个强大的概念,然而正确的equals()方法的实现应遵循一套规则,满足以下约束条件:

  • Reflexive. 对象x必须和自己相等,equals(x) 必须返回 true.
  • Symmetric. 如果equals(y) 返回 true 那么 y.equals(x) 必须返回 true.
  • Transitive. 如果equals(y) 返回 true 并且 y.equals(z) 返回 true, 那么 x.equals(z) 必须返回 true.
  • Consistent. 多个调用equals()方法的结果必须是相同的值
  • Equals To Null.  equals(null) 返回 false.

不幸的是,Java编译器在编译过程中无法执行这些约束。然而,不遵守这些规则可能会导致非常奇怪和难以解决的问题。一般建议是这样的:如果你要自己编写equals()方法实现,三思而后行,如果你真的需要它。现在带着所有这些规则,让我们编写一个简单的实现Person类的equals()方法。

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;
    }

    // Step 0: Please add the @Override annotation, it will ensure that your
    // intention is to change the default implementation.
    @Override
    public boolean equals( Object obj ) {
        // Step 1: Check if the 'obj' is null
        if ( obj == null ) {
            return false;
        }
        
        // Step 2: Check if the 'obj' is pointing to the this instance
        if ( this == obj ) {
            return true;
        }
        
        // Step 3: Check classes equality. Note of caution here: please do not use the 
        // 'instanceof' operator unless class is declared as final. It may cause 
        // an issues within class hierarchies.
        if ( getClass() != obj.getClass() ) {
            return false;
        }
        
        // Step 4: Check individual fields equality
        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()方法returns false,每两对象 的hashcode()方法可能会或可能不会返回相同的整数值)。让我们来看看person类的hashcode()方法。

// Please add the @Override annotation, it will ensure that your
// intention is to change the default implementation.
@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()方法使用相同的域。这将在任何时候域改变的情况下,保证这两种方法的一致性。

3. toString方法

toString()可以说是最有趣的方法,更频繁地被覆盖。其目的是提供对象的字符串表示(类实例)。正确写toString()方法可以极大地简化现实系统的调试和故障排除问题。

在大多数情况下,默认的 toString() 实现不是很有用,只是返回类名和对象的hashcode,让我们使用实现和覆盖 toString()方法来改进上面的Person类,这是一个让toString()更有用的方法。

// Please add the @Override annotation, it will ensure that your
// intention is to change the default implementation.
@Override
public String toString() {
    return String.format( "%s[email=%s, first name=%s, last name=%s]", 
        getClass().getSimpleName(), email, firstName, lastName );
}

现在,toString() 方法提供了包含 Person 类实例所有域的string版本. 例如, 执行下面的代码段:

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() 实现.

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;
    }
}

下面的结果将输出到 控制台 (可以看到 Person 类实例转化为string):

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

在Java社区已经开发了几个有很大的帮助的比较全面的库,使tostring()实现不再痛苦并且变得轻松。在这些中有比如 Google Guava's Objects.toStringHelperApache Commons LangToStringBuilder.

4. clone方法

首先,如果你已经决定要实现自己的clone()方法,在Java documentation中有很多规范。其次,该方法在 Object 中声明为protected,为了使其可见,它应该被覆盖为public。再次,覆盖类应该实现Cloneable接口,否则将引发CloneNotSupportedException异常。最后,实现应首先调用super.clone() ,然后如果需要,执行其他操作。让我们看看如何为我们的Person类实现。

public class Person implements Cloneable {
    // Please add the @Override annotation, it will ensure that your
    // intention is to change the default implementation.
    @Override
    public Person clone() throws CloneNotSupportedException {
        return ( Person )super.clone();
    }
}

实现看起来很简单,于是这里会出什么问题?相当多问题,其实。当正在执行的类实例的克隆,没有类构造函数被调用。这样的行为的后果是,意外的数据共享可能会出现。让我们考虑下面的Office类的例子

import java.util.Arrays;

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会导致数据共享问题。

总的来说,如果你想让你的类的精确副本,可能最好是避免 clone() / Cloneable ,使用更简单的替代品(例如,拷贝构造函数,或工厂方法,一个有用的建设模式)。

5. equals方法和== 操作

java中的==运算符和equals()方法有一个有趣的关系并造成许多问题和困惑。在大多数情况下(除了比较原始类型),==操作符执行引用的相等:如果两个引用指向同一个对象,则返回真。让我们来看看一个简单的例子,它说明差异:

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

即使两个字符串看起来完全一样,在这个特定的例子中他们作为两种不同的string实例存在。作为一个经验法则,如果你处理的对象引用,总是用 equals() 或 Objects.equals()来比较相等,除非你真的有意向比较对象引用指向同一个实例。

6. 有用的辅助类

自从发布了java7,在java标准库中有几个非常有用的辅助类。其中一个是Objects类。特别,以下三种方法可以大大简化你的equals()hashcode()方法实现。

Method Description
static boolean equals(Object a, Object b) Returns true if the arguments are equal to each other and false otherwise.
static int hash(Object... values) Generates a hash code for a sequence of input values.
static int hashCode(Object o) Returns the hash code of a non-null argument and 0 for a null argument.

Table 2

如果你使用这些辅助类来为 Person 类写 equals()hashCode() 方法 ,代码的量将明显变小,代码可读性增强。

@Override
public boolean equals( Object obj ) {
    if ( obj == null ) {
        return false;
    }
        
    if ( this == obj ) {
        return true;
    }
        
    if ( getClass() != obj.getClass() ) {
        return false;
    }
        
    final PersonObjects other = (PersonObjects) 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;
}
        
@Override
public int hashCode() {
    return Objects.hash( email, firstName, lastName );
}

如果文章对您有帮助,欢迎点击下方按钮打赏作者

Comments

No comments yet.
To verify that you are human, please fill in "七"(required)