在java中,Object
处在类的最顶层,其他类都继承它,于是所有类都继承了 Object
类的方法, 最重要的几个如下:
在这部分教程,我们将看看 equals,
hashCode,
toString
和clone
方法, 记住它们的用法和重要的约束。
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.toStringHelper 和Apache Commons Lang
ToStringBuilder.
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()
方法实现。
如果你使用这些辅助类来为 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