转载

Java进阶2 —— 使用Object的通用方法

原文链接: http://www.javacodegeeks.com/2015/09/using-methods-common-to-all-objects.html

本文是Java进阶课程的第二篇。

本课程的目标是帮你更有效的使用Java。其中讨论了一些高级主题,包括对象的创建、并发、序列化、反射以及其他高级特性。本课程将为你的精通Java的旅程提供帮助。

内容提纲

  1. 引言

  2. equals和hashCode方法

  3. toString方法

  4. clone方法

  5. equals方法与"=="操作符

  6. 有用的帮助类

  7. 源码下载

  8. 下章概要

1. 引言

从前面一篇对象的创建与销毁中,我们知道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篇文章 并发最佳实践 中详细介绍此方法

表1

在本篇文章中我们将重点介绍 equalshashCodetoStringclone 方法。通过本章节的学习,需要对这几个方法的用法及重要的使用限制了然于胸。

2. equlas和hashCode方法

默认情况下,Java 中任何两个对象引用(或类实例引用)只有指向相同的内存地址时才认为是相等的(引用相等)。但是Java允许通过重载 Objectequals() 方法给类自定义判等规则。听起来这是个很强大的概念,然而在适当的 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() 方法是使用相同的字段,以确保在不可预期的字段调整时保证这两个方法行为的一致性。

3. toString方法

toString() 是最让人感兴趣的方法,并且被重载的频率也更高。此方法的目的是提供对象(类实例)的字符串表现。如果对 toString() 方法重载恰当,能极大的简化debug难度和分析解决问题的过程。

默认情况下, toString() 的结果仅仅返回以 @ 符分隔的全类名与对象哈希值串,然而这个结果在大多场景下并没什么用途。如下:

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

我们来通过重写 PersontoString() 方法以使其输出更有用,下面是其中一种实例:

// 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

4. clone方法

如果举出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++程序员熟悉的复制构造方法,或者工厂方法——在对象的创建与销毁中讨论过的一种有用的构造模式)。

5. 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

尽管两个字符串看起来完全一样,但事实上却是两个不同的 String 实例。作为建议,在处理对象引用时要使用 equals()Objects.equals() 进行判等,除非你真的是要判断两个引用是否指向同一个实例。

6. 有用的帮助类

从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

如果使用上面的方法来重写 Personequals()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 ); }      

7. 源码下载

可以从这里下载本文中的源码: advanced-java-part-2

8. 下章概要

在本章中,我们学习了作为Java面向对象基础的 Object 类,以及自定义的类如何通过自己的判等规则重载 Object 的相关方法。下一章中,我们将会把视线暂时从代码实现上收起,转向去讨论如何设计合适的类和接口。

原文  https://segmentfault.com/a/1190000004420059
正文到此结束
Loading...