Object類:又回到最初的起點

Object類大概是每一個JAVA程序員認識的第一個類,由於它是全部其餘類的祖先類。在JAVA單根繼承的體系下,這個類中的每一個方法都顯得尤其重要,由於每一個類都可以調用或者重寫這些方法。當你JAVA學到必定階段,尤爲是學到了反射機制、多線程和JVM以後,再回過頭看一眼這些方法,可能會有新的體會。java

Object根類方法

public final native Class<?> getClass()

public native int hashCode()

public boolean equals(Object obj)

protected native Object clone() throws CloneNotSupportedException

public String toString()

public final native void notify()

public final native void notifyAll()

public final native void wait(long timeout) throws InterruptedException

public final void wait(long timeout, int nanos) throws InterruptedException

public final void wait() throws InterruptedException

protected void finalize() throws Throwable {}

equals()

equals()的實現:程序員

  • 檢查是否爲同一個對象的引用,若是是直接返回 true;
  • 檢查是不是同一個類型,若是不是,直接返回 false;
  • 將 Object 對象進行轉型;
  • 判斷每一個關鍵域是否相等。

源碼以下:面試

public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String aString = (String)anObject;
            if (coder() == aString.coder()) {
                return isLatin1() ? StringLatin1.equals(value, aString.value)
                                  : StringUTF16.equals(value, aString.value);
            }
        }
        return false;
    }

equals() 與 ==的區別:算法

  • 對於基本類型,== 判斷兩個值是否相等,基本類型沒有 equals() 方法。
  • 對於引用類型,== 判斷兩個變量是否引用同一個對象,而 equals() 判斷引用的對象是否等價。

hashCode()

hashCode() 返回散列值,而 equals() 是用來判斷兩個對象是否等價。等價的兩個對象散列值必定相同,可是散列值相同的兩個對象不必定等價。這句話必定要想清楚,若是知道散列衝突的話,這句話也不難理解。在覆蓋 equals() 方法時應當老是覆蓋 hashCode() 方法,保證等價的兩個對象散列值也相等。數組

好比下面這個例子,因爲沒有覆蓋hashCode()方法,set會認爲是兩個不一樣的對象,去重失敗。多線程

EqualExample e1 = new EqualExample(1, 1, 1);
EqualExample e2 = new EqualExample(1, 1, 1);
System.out.println(e1.equals(e2)); // true
HashSet<EqualExample> set = new HashSet<>();
set.add(e1);
set.add(e2);
System.out.println(set.size());   // 2

簡單來講,hashCode() 方法經過哈希算法爲每一個對象生成一個整數值,稱爲散列值。ide

hashCode()方法的算法約定爲:函數

  • 在 Java 應用程序執行期間,在對同一對象屢次調用 hashCode 方法時,必須一致地返回相同的整數,前提是將對象進行 equals 比較時所用的信息沒有被修改。從某一應用程序的一次執行到同一應用程序的另外一次執行,該整數無需保持一致。
  • 兩個相等的對象(經過equals方法判斷)必須返回相同哈希值。
  • 兩個不相等的對象(經過equals方法判斷),調用hashCode()方法返回值不是必須不相等。

下面以一個例子演示hashCode方法的覆寫:性能

public class User {

    private long id;
    private String name;
    private String email;
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null) return false;
        if (this.getClass() != o.getClass()) return false;
        User user = (User) o;
        return id != user.id 
          && (!name.equals(user.name) 
          && !email.equals(user.email));
    }
}

上面的代碼中重寫了equals方法,hashCode方法的不一樣重現版本以下:優化

//實現1  
@Override
    public int hashCode() {
        return 1;
}

//實現2
@Override
public int hashCode() {
    return (int) id * name.hashCode() * email.hashCode();
}

//實現3(標準實現)
@Override
public int hashCode() {
    int hash = 7;
    hash = 31 * hash + (int) id;
    hash = 31 * hash + (name == null ? 0 : name.hashCode());
    hash = 31 * hash + (email == null ? 0 : email.hashCode());
    return hash;
}
  • 實現1:

    哈希表中全部對象保存在同一個位置,哈希表退化成了鏈表。

  • 實現2:

    這個比較好,這樣不一樣對象的哈希碼發生的碰撞的機率就比較小了

  • 實現3(標準實現):

    用到了素數31,至於爲何要用31,Effective Java中作了比較清楚的解答,這裏直接粘過來了:

    之因此選擇31,是由於它是個素數。若是乘數是偶數,而且乘法溢出的話,信息就會丟失,由於與2相乘等價於移位運算。使用素數的好處並不明顯。可是習慣上都使用素數來計算散列結果。31有很好的特性,即用移位和減法來代替乘法,能夠獲得更好的性能:31 * i == (i << 5) - i。如今 虛擬機能夠自動完成這種優化。

    這段話的最後講到了重點,即便用31仍是主要出於效率上的考慮。

toString()

toString()方法用於返回對象的字符串表示,默認實現以下:

public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

該字符串由類名 + 」@「 + 此對象散列值的無符號十六進制表示組成。輸出對象時會自動調用toString方法把對象轉化爲字符串,好比System.out.println(obj);

Effective JAVA第12條指出:始終要覆蓋toString方法。由於這種默認的輸出形式不太多是咱們想要的,每一個類應該實現本身的toString方法,好比下面這個例子:

public class User implements Serializable {
    private long id;
    private long phone;
    private String name;
    private String password;
    @Override
    public String toString() {
        return "User{" + "id=" + id + ", phone=" + phone + ", name=" + name + ", password=" + password + "}";
    }
}

clone()

clone() 是 Object 的 protected 方法,它不是 public,一個類不顯式去重寫 clone(),其它類就不能直接去調用該類實例的 clone() 方法。

一個類要重寫clone()方法必需要實現Cloneable接口,即:

public class CloneDemo implements Cloneable {
    private int a;
    private int b;
  
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

可是clone() 方法並非 Cloneable 接口的方法,而是 Object 的一個 protected 方法。實際上這個接口沒有包含任何的方法,Cloneable 接口只是規定,若是一個類沒有實現 Cloneable 接口又調用了 clone() 方法,就會拋出 CloneNotSupportedException。這種規定看上去的確很是奇怪,難怪Effective JAVA做者會說「Cloneable是接口的一種極端非典型的用法,也不值得效仿。一般狀況下,實現接口是爲了代表類能夠爲它的客戶作些什麼。」

clone又分爲淺拷貝和深拷貝,這二者的區別也在面試中常常被問到。

簡單說,二者的區別就是,在淺拷貝中拷貝對象和原始對象的引用類型引用同一個對象,在深拷貝中拷貝對象和原始對象的引用類型引用不一樣對象。

下面以一個例子說明,這個例子來自Effective JAVA。假如咱們要爲HashTable類實現Clone方法,它的內部維護了一個節點數組,部分代碼是這樣的:

class HashTable implements Cloneable {
      private Entry[] buckets;

      private static class Entry {
            final Object key;
            Object value;
            Entry next;

            public Entry(Object key, Object value, Entry next) {
                  super();
                  this.key = key;
                  this.value = value;
                  this.next = next;
	      }
      }
  // clone()...
}

淺拷貝以下,雖然拷貝對象也有本身的散列桶數組,但這個數組引用的鏈表與原始數組是同樣的,這樣就會引起諸多不肯定行爲。

@Override
protected Object clone() throws CloneNotSupportedException {
      HashTable result = (HashTable) super.clone();
      result.buckets = buckets.clone();
      return result;
      }

深拷貝以下,能夠看到深拷貝用遞歸的方式從新建立了一個新的散列桶數組,和原對象的不一樣。

@Override
protected Object clone() throws CloneNotSupportedException {
      HashTable result = (HashTable) super.clone();
      result.buckets = new Entry[buckets.length];
      for (int i = 0; i < buckets.length; i++)
            if (buckets[i] != null)
                  result.buckets[i] = buckets[i].deepCopy();
      return result;
      }
      private Entry deepCopy() {
            return new Entry(key, value, next == null ? null : next.deepCopy());
      }

Effective JAVA提到對象拷貝的更好的辦法是使用拷貝工廠而不是重寫clone()方法,除非拷貝的是數組。

getClass()

getClass方法利用反射機制獲取當前對象的Class對象。getClass方法是一個final方法,不容許子類重寫,而且也是一個native方法。

wait() / notify() / notifyAll()

這幾個方法用於java多線程之間的協做。

  • wait():調用此方法所在的當前線程等待,直到在其餘線程上調用notify() / notifyAll()方法喚醒該線程。
  • wait(long timeout):調用此方法所在的當前線程等待,直到在其餘線程上調用notify() / notifyAll()方法或者等待時間超過傳入參數表示的時間,該線程被喚醒。
  • notify() / notifyAll():喚醒在此對象監視器上等待的單個線程/全部線程。

下面舉個例子說明:

public class ThreadTest {
 
    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        MyRunnable r = new MyRunnable();
        Thread t = new Thread(r);
        t.start();
        synchronized (r) {
            try {
                System.out.println("main thread 等待t線程執行完");
                r.wait();
                System.out.println("被notity喚醒,得以繼續執行");
            } catch (InterruptedException e) {
                e.printStackTrace();
                System.out.println("main thread 本想等待,但被意外打斷了");
            }
            System.out.println("線程t執行相加結果" + r.getTotal());
        }
    }
}
 
class MyRunnable implements Runnable {
    private int total;
 
    @Override
    public void run() {
        synchronized (this) {
            System.out.println("Thread name is:" + Thread.currentThread().getName());
            for (int i = 0; i < 10; i++) {
                total += i;
            }
            notify();
            System.out.println("執行notify後同步代碼塊中依然能夠繼續執行直至完畢");
        }
        System.out.println("執行notify後且同步代碼塊外的代碼執行時機取決於線程調度");
    }
 
    public int getTotal() {
        return total;
    }
}

輸出結果:

main thread 等待t線程執行完
Thread name is:Thread-0
執行notif後同步代碼塊中依然能夠繼續執行直至完畢
執行notif後且同步代碼塊外的代碼執行時機取決於線程調度
被notity喚醒,得以繼續執行
線程t執行相加結果45

既然是做用於多線程中,爲何倒是Object這個基類所具備的方法?緣由在於理論上任何對象均可以視爲線程同步中的監聽器,且wait() / notify() / notifyAll()方法只能在同步代碼塊中才能使用。

從上述例子的輸出結果中能夠得出以下結論:

一、wait()方法調用後當前線程將當即阻塞,且適當其所持有的同步代碼塊中的鎖,直到被喚醒或超時或打斷後且從新獲取到鎖後才能繼續執行;

二、notify() / notifyAll()方法調用後,其所在線程不會當即釋放所持有的鎖,直到其所在同步代碼塊中的代碼執行完畢,此時釋放鎖,所以,若是其同步代碼塊後還有代碼,其執行則依賴於JVM的線程調度。

finalize()

finalize方法主要與Java垃圾回收機制有關,JVM準備對此對對象所佔用的內存空間進行垃圾回收前,將會調用該對象的finalize方法。

finalize()與C++中的析構函數不是對應的。C++中的析構函數調用的時機是肯定的(對象離開做用域或delete掉),但Java中的finalize的調用具備不肯定性。

關於finalize方法的Best Practice就是在大多數時候不須要手動去調用該方法,讓GC爲你工做吧!

相關文章
相關標籤/搜索