1. 程式人生 > >JavaSE中Collection集合框架學習筆記(2)——拒絕重復內容的Set和支持隊列操作的Queue

JavaSE中Collection集合框架學習筆記(2)——拒絕重復內容的Set和支持隊列操作的Queue

%d eof 是否 face 出錯 can 3.2 lean als

前言:俗話說“金三銀四銅五”,不知道我要在這段時間找工作會不會很艱難。不管了,工作三年之後就當給自己放個暑假。

面試當中Collection(集合)是基礎重點.我在網上看了幾篇講Collection的文章,大多都是以羅列記憶點的形式書寫的,沒有談論實現細節和邏輯原理。作為個人筆記無可厚非,但是並不利於他人學習。希望能通過這種比較“費勁”的講解,幫助我自己、也幫助讀者們更好地學習Java、掌握Java.

無論你跟我一樣需要應聘,還是說在校學生學習Java基礎,都對入門和進一步啟發學習有所幫助。(關於Collection已經寫過一篇文章,可以在本文最後點擊鏈接閱讀)。

1.3 拒絕重復內容的Set

Set,跟數學中的概念“集合”是一樣的,就是沒有重復的元素。在JavaSE的Cellection框架裏,Set是三大陣營之一。根據“核心框架圖”,我們可以看到它的位置。

技術分享

同樣一張訂單,已經支付過一次就不能再次支付,否則就是重復支付。反映在系統當中,就是收集對象時,如果有相同的對象,則不再重復收集。如果有這類需求,我們可以用使用實現Set接口的類。之前講過的List和之後會談到的Queue,都對是否重復沒有要求,這是Set的特性。

1.3.1 如何使用HashSet

除非已經是大小牛級別的人能做到觸類旁通,否則最好在學習API的時候做幾個簡單的實驗,不僅可以更實際地幫助理解,還可以加深印象有助於長期記憶。

輸入一段英文,經過處理後需要輸出所有不重復的單詞。HashSet實現了Set接口,我們不妨就用它來寫一段demo。

 1 import java.util.*;
 2 
 3 /**
 4  * HashSet的實驗用例
 5  */
 6 public class Words {
 7     public static void main(String[] args) {
 8         Scanner scanner = new Scanner(System.in);
 9         System.out.print("請輸入一段話:");
10         String line = scanner.nextLine();
11 String[] tokens = line.split(" ");//根據空格劃分單詞 12 Set words = new HashSet(); 13 for(String token : tokens) { 14 words.add(token);//使用HashSet收集單詞 15 } 16 System.out.printf("不重復的單詞有:%d 個: %s%n", words.size(), words); 17 } 18 }

英語中分詞沒有中文分詞那麽困難,基本上可以按照空格來劃分單詞。很明顯,輸出的結果是正確的。

技術分享

這時候不知道你是否也有這麽一個疑問:HashSet是如何判斷哪些單詞重復的呢?如果讓你來做,你會怎麽做?

1.3.2 Java中判斷重復對象的規範

如果對象是字符串,我們可以采用逐一比較的方式,比較即將收集的字符串和已有的字符串是否相同;如果對象是數值,那就更簡單了。可是,除此之外的對象怎麽辦?我們先來看一個沒那麽復雜的例子。

 1 import java.util.*;
 2 
 3 /**
 4  * Set測試用例
 5  */
 6     class Student {
 7     private String name;
 8     private String number;
 9 
10     Student(String name, String number) {
11         this.name = name;
12         this.number = number;
13     }
14 
15 
16     @Override
17     public String toString() {
18         return String.format("(%s, %s)", name, number);
19     }
20 }
21 
22     public class Students {
23         public static void main(String[] args) {
24             Set set = new HashSet();
25             set.add(new Student("Tom", "001"));
26             set.add(new Student("Sam", "002"));
27             set.add(new Student("Tom", "001"));
28             System.out.println(set);
29         }
30     }

上面這段demo是在模擬一個學生註冊系統,錄入姓名和學號,最後輸出已經註冊了的學生。由於同一個學生不能註冊兩次,所以使用了HashSet來收集對象。

技術分享

這樣的輸出結果是否出乎你的意料?顯然,在執行過程中Set並沒有把重復的學生數據排除。其實是我們太一廂情願了,因為在寫程序的時候並沒有告訴Set,什麽樣的Student實例才算是重復。要判斷對象是否重復,必須實現hashCode()和equals()方法。在之前的英文分詞例子中,對象是String,我們可以在源代碼中看到它已經實現了這兩個方法。

 1 /**
 2      * Compares this string to the specified object.  The result is {@code
 3      * true} if and only if the argument is not {@code null} and is a {@code
 4      * String} object that represents the same sequence of characters as this
 5      * object.
 6      *
 7      * @param  anObject
 8      *         The object to compare this {@code String} against
 9      *
10      * @return  {@code true} if the given object represents a {@code String}
11      *          equivalent to this string, {@code false} otherwise
12      *
13      * @see  #compareTo(String)
14      * @see  #equalsIgnoreCase(String)
15      */
16     public boolean equals(Object anObject) {
17         if (this == anObject) {
18             return true;
19         }
20         if (anObject instanceof String) {
21             String anotherString = (String)anObject;
22             int n = value.length;
23             if (n == anotherString.value.length) {
24                 char v1[] = value;
25                 char v2[] = anotherString.value;
26                 int i = 0;
27                 while (n-- != 0) {
28                     if (v1[i] != v2[i])
29                         return false;
30                     i++;
31                 }
32                 return true;
33             }
34         }
35         return false;
36     }
37 
38 /**
39      * Returns a hash code for this string. The hash code for a
40      * {@code String} object is computed as
41      * <blockquote><pre>
42      * s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
43      * </pre></blockquote>
44      * using {@code int} arithmetic, where {@code s[i]} is the
45      * <i>i</i>th character of the string, {@code n} is the length of
46      * the string, and {@code ^} indicates exponentiation.
47      * (The hash value of the empty string is zero.)
48      *
49      * @return  a hash code value for this object.
50      */
51     public int hashCode() {
52         int h = hash;
53         if (h == 0 && value.length > 0) {
54             char val[] = value;
55 
56             for (int i = 0; i < value.length; i++) {
57                 h = 31 * h + val[i];
58             }
59             hash = h;
60         }
61         return h;
62     }

事實上不只有HashSet,Java中許多要判斷對象是否重復時,都要求調用hashCode()與equals()方法,因此官方規範中建議這兩個方法必須同時實現。如果我們在之前學生註冊的例子中添加hashCode()與equals()方法的實現,重復的數據就不會出現。

 1 import java.util.*;
 2 
 3 /**
 4  * Set測試用例
 5  */
 6     class Student {
 7     private String name;
 8     private String number;
 9 
10     Student(String name, String number) {
11         this.name = name;
12         this.number = number;
13     }
14 
15     /**
16      * 重載equals()和hashcode()
17      */
18     @Override
19     public boolean equals(Object obj) {
20         if(obj == null) {
21             return false;
22         }
23         if(getClass() != obj.getClass()) {
24             return false;
25         }
26         final Student other = (Student) obj;
27         return true;
28     }
29 
30     @Override
31     public int hashCode() {
32         int hash = 5;
33         hash = 13 * hash + (this.name != null ? this.name.hashCode() : 0);
34         hash = 13 * hash + (this.number != null ? this.number.hashCode() : 0);
35         return hash;
36     }
37 
38 
39     @Override
40     public String toString() {
41         return String.format("(%s, %s)", name, number);
42     }
43 }
44 
45     public class Students {
46         public static void main(String[] args) {
47             Set set = new HashSet();
48             set.add(new Student("Tom", "001"));
49             set.add(new Student("Sam", "002"));
50             set.add(new Student("Tom", "001"));
51             System.out.println(set);
52         }
53     }

重載的hashCode()和equals()方法定義了“如果學生的姓名與學號相同,那就是重復的對象”。當然,你也可以根據自己的理解,改寫成“如果學號相同,即為重復”。

技術分享

1.3.3 Set小結

Set收集對象時,如果發現有重復的數據,會不再收集該對象。如果要實現這一點,必須告知符合什麽樣的條件才算是“重復”。

Java規範中通過重載hashCode()和equals()方法來判斷是否重復。如果你要收集的對象不屬於String或Integet之類API已經提供好的類,務必要記得實現這兩個方法。

通過學習Set和閱讀源代碼,不僅可能更好地掌握常用API的用法,同時也會Java規範有了意料之外情理之中的深入了解。無論是對Java的學習,還是日常的開發維護工作,都有不小的幫助。

1.4 支持隊列操作的Queue

什麽是隊列?它是最常用的數據結構之一,只允許在隊列的前端(front)進行刪除操作,而在表的後端(rear)進行插入操作。

顧名思義,只要是需要“排隊”的應用場景,都可以考慮使用隊列,例如餐廳的排隊系統,醫院的器官輪候系統等。

1.4.1 Queue的實現規範

在介紹完List、Set之後,我們來看看Collection的最後一大塊Queue.

Queue定義了自己特有的offer()、poll()和peek()等方法。建議優先使用offer()方法,而不是add()方法來收集對象。同樣地,pool()和peek()方法建議優先於remove()、element()方法使用。他們最主要的區別在於,add()、remove()、element()方法出錯時會拋出異常,offer()、poll()、peek()方法則會返回特定值。

前一篇介紹List的文章就介紹過LinkedList。從反復提及的核心架構圖中可以看出,其實它不僅實現了List,同時也是一種Queue。我們不妨就用LinkedList來寫一段demo,試著使用隊列。

 1 import java.util.*;
 2 
 3 /**
 4  * Queue實驗用例
 5  */
 6 interface Request {
 7     void execute();
 8 }
 9 
10 public class RequestQueue {
11 
12     public static void main(String[] args) {
13         Queue requests = new LinkedList();
14         // 模擬將請求加入隊列
15         for (int i = 1; i < 6; i++) {
16             requests.offer(new Request() {
17                public void execute() {
18                    System.out.printf("處理數據 %f%n", Math.random());
19                }
20             });
21         }
22         process(requests);
23     }
24 
25     // 處理隊列中的請求
26     private static void process(Queue requests) {
27         while(requests.peek() != null) {
28             Request request = (Request) requests.poll();
29             request.execute();
30         }
31     }
32 }

由於是隨機產生的數字,所以幾乎每一次實驗結果都會不一樣,不過這並不重要。

技術分享

1.4.2 既是隊列又是棧的Deque

有的時候,我們會想對隊列的前端與尾端進行操作,能在前端加入對象、取出對象,也能在尾端加入對象和取出對象。Queue的子接口Deque可以滿足這個需求,我們在核心框架圖上可以很容易找到它的位置。

Queue的行為和Deque的行為有所重復,有幾個方法是等義的,例如前者的add()等於後者的addLast()方法,建議感興趣的朋友自行查看源代碼或API說明文檔。

java.util.ArrayDeque實現了Deque接口,我們可以通過寫一段操作容量有限的堆棧的demo來看看如何使用它。

 1 import java.util.*;
 2 
 3 /**
 4  * Deque實驗用例
 5  */
 6 public class Stack {
 7     private Deque deque = new ArrayDeque();
 8     private int capacity;
 9 
10     public Stack(int capacity) {
11         this.capacity = capacity;
12     }
13 
14     public boolean push(Object o) {
15         if(deque.size() + 1 > capacity) {
16             return false;
17         }
18         return deque.offerLast(o);
19     }
20 
21     public Object pop() {
22         return deque.pollLast();
23     }
24 
25     public Object peek() {
26         return deque.peekLast();
27     }
28 
29     public int size() {
30         return deque.size();
31     }
32 
33     public static void main(String[] args) {
34         Stack stack = new Stack(5);
35         stack.push("小明");
36         stack.push("小花");
37         stack.push("小光");
38         System.out.println(stack.pop());
39         System.out.println(stack.pop());
40         System.out.println(stack.pop());
41     }
42 }

堆棧結構的特性是先進後出,所以運行結果是先顯示小光,最後顯示小明。

技術分享

一道思考題:從核心框架圖中可以看出LinkedList也實現了Deque接口,不過在這個demo裏面,使用ArrayDeque速度上要比LinkedList快。這是為什麽?

1.4.3 Quque小結

隊列是一種常見而且重要的數據結構,JavaSE中Collection的三大分支之一Quque提供了相應的實現。

Deque是一種雙向隊列,同時也是Queue的一個子接口。它們之間既有等義的方法,也有不同的實現,具體情況需要閱讀API說明文檔或直接查看源代碼。

我們在學習之前,不妨可以先試著用Java基本語法實現隊列、堆棧等數據結構和標準操作方法。在此基礎上再閱讀相應的源代碼(例如LinkedList),會格外發現代碼的邏輯之美和整潔之美。

相關文章推薦:

JavaSE中Collection集合框架學習筆記(1)——具有索引的List

如果你喜歡我的文章,可以掃描關註我的個人公眾號“李文業的思考筆記”。

不定期地會推送我的原創思考文章。

技術分享

JavaSE中Collection集合框架學習筆記(2)——拒絕重復內容的Set和支持隊列操作的Queue