1. 程式人生 > >萬類之父——Object

萬類之父——Object

boolean sets 描述 cte equal etc jdk1.8 應用程序 析構函數

jdk1.8.0_144

  Object類作為Java中的頂級類,位於java.lang包中。所有的類直接或者間接都繼承自它。所以Object類中的方法在所有類中都可以直接調用。在深入介紹它的API時,先插一句它和泛型之間的關系。

  在JDK1.5之前是沒有泛型的,集合能夠裝下任意的類型,這就導致了一個潛在的問題——不能在編譯時做類型檢查,也就可能導致程序bug出現的概率。JDK1.5出現了泛型,在定義一個集合時指定一個泛型,這就能在編譯時能做類型檢查,避免了一些低級bug的出現。時至今日,實際上在JDK源碼中遺留了部分不是特別優美的代碼,從今天的角度來看是能夠將其泛型化的(例如Map的get方法),但在當時需要考慮向後兼容不得不放棄對某些方法和類的泛型化,才導致了一絲瑕疵。

  接下來將詳細的剖析Object類中的一些方法,其中某些方法會延伸到其他方面(例如:wait和notify等)。

public final native Class<?> getClass()

  返回Class對象實例。Class類有點“特殊”,因為它在我們的日常代碼邏輯中不常出現,它所出現的地方往往是一些基礎框架或者基礎工具。  

  Class類所處的包同樣是java.lang,毫無疑問它的父類還是Object。在學習面向對象編程時,我們知道類是對一個事物抽象的定義,對象實例是表示的是一個具體的事物。那麽Class這個名字有點含糊的類抽象的是什麽呢?它的實例有代表的是什麽呢?

  在程序中定義一個People類,我們將男人、女人抽象為了人類——People,它的實例表示的是男人或女人。程序中類似People這樣的類千千萬萬,Class類就是千千萬萬類和接口的抽象,Class類的對象實例就這千千萬萬中具體的某個類或接口。再繼續,男人和女人能被抽象為People類,這是因為男人和女人都有很多相同的特征,那千千萬萬類和接口都有名字、方法等也就意味著它們也能被抽象,故Class類就千千萬萬類和接口的抽象,Class類的對象實例就這千千萬萬中具體的某個類或接口。

  Class類作為類和接口的抽象,它存在的意義在哪裏呢?它是類和接口的抽象,它的實例是某個具體的類,那為何不直接通過People p = new People()來實例化一個People對象呢?而是麻煩的需要先獲取Class類,再獲取它的實例,再通過它的實例創造一個類的對象。

  通常情況下使用Class類來獲取某個類在實際編碼中確實不常見,但這是JVM的執行機制。每個類被創建編譯過後都對應一個.class文件,這個.class文件包含了它對應的Class對象,這個類的Class對象會被載入內存,它就會被JVM來創建這個類的所有實例對象。當然在實際運用中,Java的反射機制是離不開Class類的。 所以,回到Object類的getClass方法,提供的是該類的Class對象,每個類都可以通過這個方法獲取它對應的Class對象。

public native int hashCode()

  這個方法是一個native本地方法,它的具體實現是有C++實現的。在Java程序中,每個對象實例(註意是對象實例)都有一個唯一的hashCode值(哈希碼值),可以通過對比兩個對象實例是否相同來判斷是否指向同一個對象實例。

  它有這麽一個性質,例如判斷兩個String字符串是否相等,使用“==”表示的兩個對象的引用是否相等,而使用equals則表示兩個對象的值是否相等。equals相等,則hashCode值一定相等;hashCode值相等,而equals不一定相等。並且它被應用在我們熟悉的Map集合中。

public boolean equals(Object obj)

  該方法用於比較兩個對象是否“相等”。之所以相等有引號,是這個相等在代碼邏輯中分為兩種情況:對象引用相等;對象值相等。

  Object中equals方法有一個默認實現,它直接使用“==”進行比較,也就是說在Object中equals和“==”是等價的。但是在String和Integer等中, equals方法是被重寫的,它們的equals方法代表的是值相等,而不是引用相等。

  註意在重寫equals方法時,需要遵守以下幾個原則:

  1. 自反性。也就是說自己調用equals方法和自己比較時,必須返回true。(自己都不和自己相等,那誰才相等)

  2. 對稱性。我和你比較返回ture,你和我比較也要返回true,a.equals(b)返回true,b.equals(a)返回true。

  3. 傳遞性。這個根據名字就很好理解。a.equals(b)返回true,b.equals(c)返回true,a.equals(c)也需要返回true。

  4. 一致性。也就是在沒有修改兩個對象的情況下,多次調用返回的結果應該是一樣的。

  5. 非空性。非空對象與null值比較必須返回false。

e.g.

技術分享圖片
 1 package com.coderbuff.customequals;
 2 
 3 /**
 4  * Studen類
 5  * Created by Kevin on 2018/2/10.
 6  */
 7 public class Student {
 8     /**
 9      * 姓名
10      */
11     private String name;
12     /**
13      * 年齡
14      */
15     private int age;
16     /**
17      * 性別
18      */
19     private byte sex;
20 
21     public Student() {
22     }
23 
24     public Student(String name, int age, byte sex) {
25         this.name = name;
26         this.age = age;
27         this.sex = sex;
28     }
29 
30     public String getName() {
31         return name;
32     }
33 
34     public void setName(String name) {
35         this.name = name;
36     }
37 
38     public int getAge() {
39         return age;
40     }
41 
42     public void setAge(int age) {
43         this.age = age;
44     }
45 
46     public byte getSex() {
47         return sex;
48     }
49 
50     public void setSex(byte sex) {
51         this.sex = sex;
52     }
53 
54     @Override
55     public boolean equals(Object obj) {
56         if (!(obj instanceof Student)) {
57             return false;
58         }
59         Student other = (Student)obj;
60         if (other.getName().equals(this.name) && other.getAge() == this.age && other.getSex() == this.sex) {
61             return true;
62         }
63         return false;
64     }
65 }
View Code

測試代碼:

技術分享圖片
 1 package com.coderbuff.customequals;
 2 
 3 import org.junit.Before;
 4 import org.junit.Test;
 5 
 6 import static org.junit.Assert.assertEquals;
 7 
 8 /**
 9  * 測試Student類equals方法
10  * Created by Kevin on 2018/2/10.
11  */
12 public class StudentTest {
13     private Student a, b, c;
14 
15     @Before
16     public void setUp() {
17         a = new Student("Kevin", 23, (byte)0);
18         b = new Student("Kevin", 23, (byte)0);
19         c = new Student("Kevin", 23, (byte)0);
20     }
21 
22     /**
23      * 自反性
24      */
25     @Test
26     public void testReflexive() {
27         assertEquals(true, a.equals(a));
28     }
29 
30     /**
31      * 對稱性
32      */
33     @Test
34     public void testSymmetric() {
35         assertEquals(true, a.equals(b));
36         assertEquals(true, b.equals(a));
37     }
38 
39     /**
40      * 傳遞性
41      */
42     @Test
43     public void testTransitive() {
44         assertEquals(true, a.equals(b));
45         assertEquals(true, b.equals(c));
46         assertEquals(true, a.equals(c));
47     }
48 
49     /**
50      * 一致性
51      */
52     @Test
53     public void testConsistent() {
54         for (int i = 0; i < 100; i++) {
55             assertEquals(true, a.equals(b));
56         }
57     }
58 
59     /**
60      * 非空性
61      */
62     @Test
63     public void testNonNullity() {
64         assertEquals(false, a.equals(null));
65     }
66 }
View Code

  從上面重寫的equals的測試結果來看是通過的,但是實際上是錯誤的,如果在你的程序中只使用這個類的equals方法,而不會使用到集合,那沒問題,但是一旦使用Map集合,上面的錯誤立馬暴露。例如如果運行以下測試方法,返回的結果將會是null。

e.g.

技術分享圖片
1 /**
2  * 測試equals方法
3  */
4 @Test
5 public void testMap() {
6     Map<Student, String> map = new HashMap<>();
7     map.put(a, "this is map.");
8     assertEquals("this is map.", map.get(b));
9 }
View Code

  但明明邏輯中a和b是相等的,b也應該能取出值來,這就是沒有重寫hashCode方法a和b對象的hashCode值不一致導致的問題,這不是bug,這是沒有滿足JDK的規定。上面的hashCode方法末尾提到了equals相等,hashCode值也相等;hashCode值相等,equals不一定相等。上面的代碼3個對象的hashCode值是不相等的,所以導致b不能從Map中取出相應的值,相等的對象必須具有相等的hashCode值。

  這就涉及到如何設計一個良好運作的散列函數。一個好的散列函數,更能較為平均地散列到散列通中,而不是造成大量的散列沖突,大量的散列沖突會使得散列表退化成鏈表形式,這會使得效率大大降低。設計上要設計一個好的散列函數並不是一件容易的事,下面為Student類設計的散列函數是根據《Effective Java》中的解決辦法。

1 @Override
2 public int hashCode() {
3     int result = 17;    
4     result = name.hashCode() + result;
5     result = 31 * result + age;
6     result = 31 * result + (int)sex;
7     return result;
8 }

測試方法:

技術分享圖片
1 /**
2  * 測試hashCode值是否相等
3  */
4 @Test
5 public void testHashCode() {
6     assertEquals(true, a.hashCode() == b.hashCode());
7 }
View Code

protected native Object clone() throws CloneNotSupportedException

  “克隆”,也稱為“復制”。這個方法在訪問權限不同於其他方法,它在Object類中是protected修飾的方法。protected意味著只能在它的子類調用Object類中的clone方法,而不能直接在外部調用,想要使用對象的clone方法,需要在方法中調用父類的clone方法,並且需要實現Cloneable接口。

  這個方法如其名,復制一個相同的對象示例,而不是將引用拷貝給它,所以復制後的對象實例一個新的對象示例。

e.g.

技術分享圖片
1 //還是上面Student類,重寫clone方法,並且實現Cloneable接口
2 @Override
3 protected Student clone() throws CloneNotSupportedException {
4     return (Student) super.clone();
5 }
View Code

測試方法:

技術分享圖片
 1 /**
 2  * 測試clone方法
 3  * @throws CloneNotSupportedException
 4  */
 5 @Test
 6 public void testClone() throws CloneNotSupportedException {
 7     Student cloneA = (Student) a.clone();
 8     assertEquals(false, cloneA == a);
 9     assertEquals(true, cloneA.equals(a));
10 }
View Code

  這個方法呢,坑比較多。它有一個比較重要的地方——“深復制”和“淺復制”。

  一個復雜類的成員屬性有“基本數據類型”和“引用數據類型”。

  假設現在需要對對象A復制一個對象B。

  對於淺復制來講,對象B的基本數據類型和A的基本數據類型它們相等,且互相不會受到影響。但是如果修改了對象B中的引用數據類型,此時將會影響到A對應的引用數據類型。

  但對於深復制來講,對象B就是完完全全和A一樣的對象實例,不管是基本的還是引用的數據類型都不會相互影響。

  如果像上面的示例代碼重寫clone方法,它所實現的就是淺復制(當然在Student類中並沒有引用類型),如果在Student類中有一個Course引用類型的話,想要它實現深復制需要完成以下2點:

  1. Course本身也已經實現Cloneable接口,且重寫了clone方法。

  2. Student類中在調用了父類的clone方法後,還需要調用Course的clone方法。

  如下所示:

1 @Override
2 protected Student clone() throws CloneNotSupportedException {
3     Student s = (Student) super.clone();
4     s.course = (Course) this.course.clone();
5     return s;
6 }

  重寫實現clone方法時一定要仔細,切記需要調用父類的clone方法。

public String toString()

  返回類的一些描述信息。“最佳的編程實踐”是最好對每個類都重寫toString方法。在Object類中這個方法的實現是調用getClass返回類信息+@符號+16進制的hashCode值。

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

  這幾個方法拿到一塊來說是因為它們用於多線程並發編程當中。

  上面的5個方法實際上只有前3個核心方法,後兩個只是wait方法的重載而已。我們先了解前3個,後兩個也會迎刃而解。

  開頭提到這用於多線程並發編程中,眾所周知Java應用程序號稱“一次編譯,到處運行”的奧秘就在於Java應用程序是運行在Java虛擬機(JVM)之上的,而JVM的設計實際上是類同於一個操作系統的。在操作系統中談的更多是進程與進程之間的關系,例如進程間的同步與通信,進程和進程的並發等等。Java應用程序在操作系統中只是1個進程,在JVM中就蘊含了N個線程,就類似於操作系統的N個進程,所以在Java中提及更多的是線程間的同步與通信,線程和線程的並發等。

  Java中的線程用於自己的運行空間,稱之為虛擬機棧,這塊空間是線程所獨占的。如果Java應用程序中的N個線程相互孤立互不幹擾的運行,可以說這個應用程序並沒有多大的價值,最大的價值是N個線程之間相互配合完成工作。那自然就會涉及到多個線程間的通信問題。在本文只著重講解線程間的通信,而對於線程安全這個議題不做過多深究。

  在操作系統中,對於進程間同步有這麽一個定義:為完成某種任務而建立的兩個或多個進程,這些進程因為需要在某些位置上協調它們的工作次序而等待、傳遞信息所產生的制約關系。將定義中的進程換為線程,即可當做在Java中對同步的定義。

  例如:線程T1運行到某處時,需要線程T2完成另一項任務才能繼續運行下去,此時T1對CPU的占用就需要讓位給T2,而T1此時只能等待T2完成。當T2完成任務後通知T1重新獲取對CPU的占用繼續完成未完成的任務。這個例子就是簡單的同步示例,其中涉及到的等待、通知即表示線程間的通信,wait和notify、notifyAll所代表的就是線程間的通信。

  所以,Object類中的wait和notify、notifyAll方法是用於線程間的通信,且它們的調用需要在獲取對象鎖的情況下才可以(也就是說需要在線程安全的條件下調用),在具體一點是只有在synchronized關鍵字所修飾的同步方法或者同步代碼塊才可以使用,並不是任何地方都可以調用。這裏有一個有關線程間通信的經典示例——生產者消費者模型。通過仔細咀嚼這個模型我們能好的理解線程間的通信。

e.g.

技術分享圖片
 1 package com.coderbuff.communication;
 2 
 3 import java.util.Queue;
 4 
 5 /**
 6  * Producer
 7  * Created by Kevin on 2018/2/13.
 8  */
 9 public class Producer implements Runnable{
10     Queue<String> queue;
11 
12     public Producer(Queue<String> queue) {
13         this.queue = queue;
14     }
15 
16     @Override
17     public void run() {
18         synchronized (queue) {
19             try {
20                 while (queue.size() == 10) {
21                     System.out.println("生產線程" + Thread.currentThread().getId() + "執行,隊列為滿,生產者等待");
22                     queue.wait();
23                 }
24                 queue.add(String.valueOf(System.currentTimeMillis()));
25                 System.out.println("生產線程" + Thread.currentThread().getId() + "執行,隊列不為滿,生產者生產:" + String.valueOf(System.currentTimeMillis()) + ",容量" + queue.size());
26                 queue.notifyAll();
27             } catch (InterruptedException e) {
28                 e.printStackTrace();
29             }
30         }
31     }
32 }
View Code 技術分享圖片
 1 package com.coderbuff.communication;
 2 
 3 import java.util.Queue;
 4 
 5 /**
 6  * Consumer
 7  * Created by Kevin on 2018/2/13.
 8  */
 9 public class Consumer implements Runnable{
10     Queue<String> queue;
11 
12     public Consumer(Queue<String> queue) {
13         this.queue = queue;
14     }
15 
16     @Override
17     public void run() {
18         synchronized (queue) {
19             try {
20                 while (queue.isEmpty()) {
21                     System.out.println("消費線程" + Thread.currentThread().getId() + "執行,隊列為空,消費者等待");
22                     queue.wait();
23                 }
24                 System.out.println("消費線程" + Thread.currentThread().getId() + "執行,隊列不為空,消費者消費:" + queue.remove() + ",容量" + queue.size());
25                 queue.notifyAll();
26             } catch (InterruptedException e) {
27                 e.printStackTrace();
28             }
29         }
30     }
31 }
View Code

測試方法:

技術分享圖片
 1 package com.coderbuff.communication;
 2 
 3 import org.junit.Before;
 4 import org.junit.Test;
 5 
 6 import java.util.LinkedList;
 7 import java.util.Queue;
 8 
 9 /**
10  * Test Producer & Consumer
11  * Created by Kevin on 2018/2/13.
12  */
13 public class ProducerConsumerTest {
14     Queue<String> queue;
15 
16     @Before
17     public void setUp() {
18         queue = new LinkedList<>();
19     }
20 
21     @Test
22     public void test() {
23         new Thread(new Consumer(queue)).start();
24         new Thread(new Consumer(queue)).start();
25         new Thread(new Producer(queue)).start();
26         new Thread(new Producer(queue)).start();
27     }
28 }
View Code

  這個生產者消費者模型很好的演示了線程間是如何通過Object中的wait和notify、notifyAll方法進行通信的。在程序中使用的是notifyAll方法而不是notify方法,實際當中也多用notify方法。它們倆的區別就是notify方法只會喚醒等待隊列中的一個線程使之進入同步隊列進而使之有了爭奪CPU執行的權力,而notify方法是會喚醒等待隊列中的所有線程使之進入同步隊列。註意,它們都是讓等待線程從等待隊列進入同步隊列,它們僅僅是擁有了爭奪CPU的權力,調用這兩個方法不代表它們就會擁有CPU執行的權力。

  至於wait方法另外個重載方法:

  wait(long):等待N毫秒沒用收到通知就超時返回;

  wait(long, int):同樣也是超時等待指定的時間沒有收到通知超時返回,不同的是第二個參數可以達到更加細粒度的時間控制——納秒。

protected void finalize() throws Throwable { }

  這個方法在對象在被GC前會被調用,需要著重強調的是,千萬不要依賴此方法在其中做一些資源的關閉,因為我們不能保證JVM何時進行GC,所以我們也就無法判斷該方法何時會被執行,除非你不在意它執行的時間,否則千萬不要重寫它。它不能當做是C++中的析構函數。

這是一個能給程序員加buff的公眾號

技術分享圖片

萬類之父——Object