原文链接: http://www.javacodegeeks.com/2015/09/using-methods-common-to-all-objects.html
本文是Java进阶课程的第二篇。
本课程的目标是帮你更有效的使用Java。其中讨论了一些高级主题,包括对象的创建、并发、序列化、反射以及其他高级特性。本课程将为你的精通Java的旅程提供帮助。
引言
equals和hashCode方法
toString方法
clone方法
equals方法与"=="操作符
有用的帮助类
源码下载
下章概要
从前面一篇对象的创建与销毁中,我们知道Java是一种面向对象编程语言(尽管不是纯粹的面向对象)。Java类层次结构的顶层是 Object
类,所有的其他类都隐式的继承于它。因此,所有的类也都从 Object
中继承了方法,其中最重要的几个方法如下表:
方法 | 描述 |
---|---|
protected Object clone() | 创建并返回当前对象的一份拷贝 |
protected void finalize() | 当垃圾回收器判断出该对象不再被引用时,就会调用finalize()方法。在对象的创建与销毁中有对finalizers的介绍。 |
boolean equals(Object obj) | 判断另外一个对象是否与当前对象相等 |
int hasCode() | 返回当前对象的哈希值 |
String toString() | 返回一个表示当前对象的字符串 |
void notify() | 唤醒一个等待当前对象的锁监视器的线程。我们将会在第9篇文章 并发最佳实践 中详细介绍此方法 |
void notifyAll() | 唤醒所有等待当前对象的锁监视器的线程。我们将会在第9篇文章 并发最佳实践 中详细介绍此方法 |
void wait() void wait(long timeout) void wait(long timeout, int nanos) | 使当前线程进入等待状态直到其他线程调用了当前对象的 notify() 或 notifyAll() 方法。我们将会在第9篇文章 并发最佳实践 中详细介绍此方法 |
在本篇文章中我们将重点介绍 equals
、 hashCode
、 toString
和 clone
方法。通过本章节的学习,需要对这几个方法的用法及重要的使用限制了然于胸。
默认情况下,Java 中任何两个对象引用(或类实例引用)只有指向相同的内存地址时才认为是相等的(引用相等)。但是Java允许通过重载 Object
的 equals()
方法给类自定义判等规则。听起来这是个很强大的概念,然而在适当的 equals()
方法实现需要满足以下几个规则限制:
自反性:对象 x 必须与其自身相等, equals(x) 返回 true
对称性:如果 equals(y) 为 true ,则 y.equals(x) 也要返回 true
传递性:如果 equals(y) 为 true ,并且 y.equals(z) 也为 true ,则 x.equals(z) 也要为 true
一致性:多次调用 equals() 方法应该返回相同值,除非对用于判等的任何一个属性进行了修改
与null判等: equals(null) 总是要返回 false
不幸的是Java编译器并不会在编译时对以上规则进行检查。然而,不遵守上述规则时可能会引入非常怪异并难以解决的问题。通用的建议是:如果需要重写 equals()
方法,请至少思考两次重写的必要性。遵循以上规则,我们为 Person
类重写一个简单的 equals()
实现。
package com.javacodegeeks.advanced.objects; 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()
方法返回 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()
方法是使用相同的字段,以确保在不可预期的字段调整时保证这两个方法行为的一致性。
toString()
是最让人感兴趣的方法,并且被重载的频率也更高。此方法的目的是提供对象(类实例)的字符串表现。如果对 toString()
方法重载恰当,能极大的简化debug难度和分析解决问题的过程。
默认情况下, toString()
的结果仅仅返回以 @
符分隔的全类名与对象哈希值串,然而这个结果在大多场景下并没什么用途。如下:
com.javacodegeeks.advanced.objects.Person@6104e2ee
我们来通过重写 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 的所有字段,然后执行下面的代码片段:
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 com.javacodegeeks.advanced.objects; 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
实例的字符串值):
Office{persons=[Person[email=john.smith@domain.com, first name=John, last name=Smith]]}
Java社区实例了大量有用的类库以简化 toString()
的实现。其中广泛使用的有 Google Guava
的 Objects.toStringHelper 和 Apache Commons Lang
的 ToStringBuilder
如果举出Java中最声名狼藉的方法,当属 clone()
无疑。 clone()
方法的目的很简单——返回对象实例的拷贝,然而有一堆理由可证明其使用并不像听起来那么轻而易举。
首先,实现自定义的 clone()
方法时需要遵守 Java文档 )中列出的一系列约定。其次,在 Object
类中 clone()
方法被声明为 protected
,所以为了提高方法的可见性,在重载时需要声明为 public
并把返回值类型调整为重载类自身类型。再次,重载类需要实现 Cloneable
接口(尽管该接口作为一种声明,并未提供任何方法定义),否则将会抛出 CloneNotSupportedException
异常。最后,在实现 clone()
方法时要先调用 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(); } }
上面的实现看起来简单直接,然而却隐藏着错误。当类实例的clone动作被执行时,未调用任何构造方法,后果将导致预料外的数据泄露。下面再看下 Office
类中的定义:
package com.javacodegeeks.advanced.objects; 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
实例克隆出来的所有对象都将共享相同的person数组,然而这并不是我们预期的行为。为了让 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() 方法有种奇怪的关系,却会引入大量的问题与困惑。大多数情况下(除比较基本数据类型), == 操作符执行的是引用相等:只要两个引用指向同一个对象时为 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
尽管两个字符串看起来完全一样,但事实上却是两个不同的 String
实例。作为建议,在处理对象引用时要使用 equals()
或 Objects.equals()
进行判等,除非你真的是要判断两个引用是否指向同一个实例。
从Java 7发布以来,一批有用的帮助类加入到了标准Java库中, Objects
便是其中之一。具体来说,以下三个方法可以简化你的 equals()
和 hashCode()
方法实现。
方法 | 描述 |
---|---|
static boolean equals(Object a, Object b) | 当参数中的两个对象相等时返回 true ,否则返回 false |
static int hash(Object...values) | 为参数列表生成哈希值 |
static int hashCode(Object o) | 为非null参数生成哈希值,如果参数为null返回0 |
如果使用上面的方法来重写 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 ); }
可以从这里下载本文中的源码: advanced-java-part-2
在本章中,我们学习了作为Java面向对象基础的 Object
类,以及自定义的类如何通过自己的判等规则重载 Object
的相关方法。下一章中,我们将会把视线暂时从代码实现上收起,转向去讨论如何设计合适的类和接口。