1. 程式人生 > >Java高階系列——使用所有物件的通用方法

Java高階系列——使用所有物件的通用方法

一、介紹
通過如何建立和銷燬物件一文,我們已經知道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", "[email protected]" );
System.out.println( person.toString() );

將會輸出如下結果:

Person[[email protected], 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", "[email protected]" );
    final Person personTwo = new Person( "Ron", "Zheng", "[email protected]" );
    Office office = new Office(person,personTwo);

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

將會出現如下結果:

Office{persons=[Person[[email protected], first name=John, last name=Smith],
Person[[email protected], 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類繼承的方法並強加自己的相等規則。在接下來的文章中我們將會扭轉我們的視角並且討論如何設計更適合我們的類和介面。

相關推薦

Java高階系列——使用所有物件通用方法

一、介紹 通過如何建立和銷燬物件一文,我們已經知道Java是一個面向物件的程式語言,Java類層次結構的頂部是Object類,Java中每個單獨的類都隱式的繼承於該類。因此,所有的類都繼承了在Object類中定義的一系列方法,這其中的一些非常重要的方法如下:

Effective java筆記-對於所有物件通用方法

對於所有物件都通用的方法 第8條 覆蓋equals時請遵守通用約定 滿足以下任何一個條件,可以不覆蓋equals: 類的每個例項本質上都是唯一的。(如Thread) 不關心是否提供了“邏輯相等”的測試功能(如Random) 超類已經覆蓋了equals,從

Java高階系列——不得不說的物件序列化(serialize)

1、什麼是Java物件序列化? Java的物件序列化是將那些實現了Serializable介面的物件轉化成一個位元組序列,並能夠在以後將這些位元組序列完全恢復成原來的物件。簡單來說序列化就是將物件轉化成位元組流,反序列化就是將位元組流轉化成物件。 物件必須在

java高階設計模式之工廠方法模式

一、定義 定義一個用於建立物件的介面,讓子類決定例項化哪一個類,FactoryMethod使一個類的例項化延遲到其子類。 二、結構和說明 三、簡單例項(直接複製了,不貼圖) public class ConcreteCreator extends Creator { &nb

java獲得內部類物件方法

1、使用靜態內部類 2、先new用外部類 3、用外部類的方法返回內部類物件 class OuterClass {          public static class InnerClassA          {                    public In

Java基礎【面向物件 構造方法 this super關鍵字】

第12天面向物件 今日內容介紹  構造方法  this  super 第1章 構造方法 我們對封裝已經有了基本的瞭解,接下來我們來看一個新的問題,依然以Person為例,由於Person中的屬性都被private了,外界無法直接訪問屬性,必須對外提供相應的

Java獲取json陣列物件方法

JSONArray jsonArray1 = jsonObject.getJSONArray("result"); for (int i = 0; i < jsonArray1.leng

Java中增刪改查通用方法

public class BaseDAO { private static final String DRIVER = "com.microsoft.sqlserver.jdbc.SQLServerDriver"; private static final String URL = "jdbc:sqlserv

Java基礎之面向物件方法引數和物件上轉型

 本篇部落格著重說明方法的引數傳遞機制和 物件上轉型,作為前幾篇部落格的續貂之作,當然面向物件我還沒有回顧完呢。言歸正傳。 一、方法的引數傳遞機制 1.1說明:java裡的方法不能單獨存在,呼叫方法必須使用類或者物件作為主調者。 如果宣告的方法有形式引數宣告,則在呼叫時必

通過Java反射機制獲取物件方法和成員變數

先定義一個JavaBean package com.jim.test.Test; public class User { private int id; private String name = "abc"; private Str

Java之把String物件作為方法的引數

看一段程式碼: public class StringTest { public void StringPassTest(String s, String[] ss) { s = "ab

Java高階系列——列舉(Enums)

一、介紹 本系列文章的這一部分我們將會介紹Java 5版本之後引入的除泛型之外的另外一個強大特性:列舉。可以將列舉看成一種特殊的類,並且可以將註解看成一種特殊的介面。 列舉的思想很簡單,也很方便:它代表了一組固定的常量值。實際上,列舉經常用來設計一些狀態常量

Java中類,物件方法的記憶體分配

以下針對引用資料型別: 在記憶體中,類是靜態的概念,它存在於記憶體中的CodeSegment中。 當我們使用new關鍵字生成物件時,JVM根據類的程式碼,去堆記憶體中開闢一塊控制元件,存放該物件,該物件擁有一些屬性,擁有一些方法。但是同一個類的物件和物件之間

effective java中文版第三章 對於所有物件通用方法

道一聲坑爹。。。。上週末剛把這章整理了。。。忘了儲存了。。。迫於強迫症。。。。不得不再寫一遍。。但是也一帶而過。。。。只是為了哥的強迫症 第8條 覆蓋equals時請遵守通用約定 1,自反性 2,對稱性 3,傳遞性 第9條 覆蓋equals時總要覆蓋hashCode(這條重點記住)

【Effective java 學習】第三章:對於所有物件通用方法

第八條:覆蓋equals是請遵守通用約定 滿足下列四個條件之一,就不需要覆蓋equals方法: 類的每個例項本質上都已唯一的。不包括代表值的類,如:Integer,String等,Object提供的equals方法就夠用了 不關心是否提供了“邏輯相等”的測試功能。對

Effective Java:對於所有物件通用方法

Java中所有的類都預設繼承Object類,而Object的設計主要是為了擴充套件。它的所有的非final方法(equals、hashCode、toString、clone和finalize)都有明確的通用約定(general contract),因為它們被設計

effective java-讀書筆記-第三章 對於所有物件通用方法

第三章 對於所有物件都通用的方法 所有非final方法(equals、hashCode、toString、clone、finalize)都有明確的通用約定,因為它們被設計成是要被覆蓋的,如果不遵守,基於雜湊的集合(HashMap、HashSet、HashTable)可

Effective Java學習筆記(二)對於所有物件通用方法

Object是一個具體類,但是設計他主要是為了擴充套件,他所有的非final方法(equals,toString,hashCode,clone,finalize)都是要被覆蓋的,並且任何一個類覆蓋非final方法時都要遵守通用原則,以便其他遵守這些預定的類能夠一同運作,

【讀書筆記】《Effective Java》(2)--對於所有物件通用方法

又讀了一章,之前一直覺得Java的體系很完善了,讀了這一章,發現原來Java平臺本身也有一些設計不周到的地方,而且有些地方因為已經成為公開API的一部分還不好改,相信繼續讀下去對Java的瞭解會更深一步的。 昨天下載了VS Code,嘗試了一下,感覺比subl

Effective Java 總結(二) 對於所有物件通用方法

在改寫equals的時候請遵守通用約定 不改寫equals方法時,每個例項只與自身相等,如果滿足一下任意一個條件,則不需要改寫equals方法: 一個類的例項本質上都是唯一的 不關心類是否支援“邏輯相等”功能 超類已經改寫了equals方法 類是私有的,並且可以確定