Java編程思想學習課時(四)第17章-容器深入探討

來源:互聯網
上載者:User

1 Set和儲存順序

  • 加入Set的元素必須定義equals()方法以確保對象的唯一性

  • hashCode()只有這個類被置於HashSet或者LinkedHashSet中時才是必需的。但是對於良好的編程風格而言,你應該在覆蓋equals()方法時,總是同時覆蓋hashCode()方法。

  • 如果一個對象被用於任何種類的排序容器中,例如SortedSetTreeSet是其唯一實現),那麼它必須實現Comparable介面。

  • 注意,SortedSet的意思是“按對象的比較函數對元素排序”,而不是指“元素插入的次序”。插入順序LinkedHashSet來儲存。

2 隊列

  • 隊了並發應用,Queue在Java SE5中僅有的兩個實現是LinkiedListPriorityQueue,它們僅有排序行為的差異,效能上沒有差異。

  • 優先順序隊列PriorityQueue的排列順序也是通過實現Comparable而進行控制的。

3 Map

  映射表(也稱為關聯陣列Associative Array)。

3.1 效能

  HashMap使用了特殊的值,稱作散列碼(hash code),來取代對鍵的緩慢搜尋。散列碼是“相對唯一”的、用以代表對象的int值,它是通過將該對象的某些資訊進行轉換而產生的。
hashCode()是根類Object中的方法,因此所有對象都能產生散列碼。

  對Map中使用的鍵的要求與對Set中的元素的要求一樣:

  • 任何鍵都必須具有一個equals()方法;

  • 如果鍵被用於散列Map,那麼它必須還具有恰當的hashCode()方法;

  • 如果鍵被用於TreeMap,那麼它必須實現Comparable

4 散列與散列碼

  HashMap使用equals()判斷當前的鍵是否與表中存在的鍵相同。
  預設的Object.equals()只是比較對象的地址如果要使用自己的類作為HashMap的鍵,必須同時重寫hashCode()equals()
  正確的equals()方法必須滿足下列5個條件:

  • 自反性。

  • 對稱性。

  • 傳遞性。

  • 一致性。

  • 對任何不是null的xx.equals(null)一定返回false

4.1 散列概念

  使用散列的目的在於:想要使用一個對象來尋找另一個對象
  Map的實作類別使用散列是為了提高查詢速度

散列的價值在於速度散列使得查詢得以快速進行。由於瓶頸位於查詢速度,因此解決方案之一就是保持鍵的排序狀態,然後使用Collections.binarySearch()進行查詢。

散列則更進一步,它將鍵儲存在某處,以便能夠很快找到。儲存一組元素最快的資料結構是數組,所以用它來表示鍵的資訊(請小心留意,我是說鍵的資訊,而不是鍵本身)。但是因為數組不能調整容量,因此就有一個問題:我們希望在Map中儲存數量不確定的值,但是如果鍵的數量被數組的容量限制了,該怎麼辦?

答案就是:數組並不儲存鍵本身。而是通過鍵對象產生一個數字,將其作為數組的下標。這個數字就是散列碼,由定義在Object中的、且可能由你的類覆蓋的hashCode()方法(在電腦科學的術語中稱為散列函數)產生。

為解決數組容量固定的問題,不同的鍵可以產生相同的下標。也就是說,可能會有衝突,即散列碼不必是獨一無二的。因此,數組多大就不重要了,任何鍵總能在數組中找到它的位置。

4.2 理解散列

  綜上,散列就是將一個對象產生一個數字儲存下來(作為數組的下標),然後在尋找這個對象時直接找到這個數字就可以了,所以散列的目的是為了提高尋找速度,而手段是將一個對象產生的數字與其關聯並儲存下來(通過數組,稱為散列表)。這個產生的數字就是散列碼。而產生這個散列碼的方法稱為散列函數hashCode())。

4.3 HashMap查詢過程(快速原因)

  因此,HashMap中查詢一個key的過程就是:

  • 首先計算散列碼

  • 然後使用散列碼查詢數組(散列碼作變數組下標)

  • 如果沒有衝突,即產生這個散列碼的對象只有一個,則散列碼對應的數組下標的位置就是這個要尋找的元素

  • 如果有衝突,則散列碼對應的下標所在數組元素儲存的是一個list,然後對list中的值使用equals()方法進行線性查詢。

  因此,不是查詢整個list,而是快速地跳到數組的某個位置,只對很少的元素進行比較。這便是HashMap會如此快速的原因。

4.4 簡單散列Map的實現

  • 散列表中的槽位(slot)通常稱為桶位(bucket)

  • 為使散列均勻,桶的數量通常使用質數(JDK5中是質數,JDK7中已經是2的整數次方了)。

    事實證明,質數實際上並不是散列桶的理想容量。近來,(通過廣泛的測試)Java的散列函數都使用2的整數次方。對現代處理器來說,除法與求餘數是最慢的操作。使用2的整數次方長度的散列表,可用 掩碼代替除法。因為get()是使用最多的操作,求餘數的%操作是其開銷最大的部分,而使用2的整數次方可以消除此開銷(也可能對hashCode()有些影響)。
  • get()方法按照與put()方法相同的方式計算在buckets數組中的索引,這很重要,因為這樣可以保證兩個方法可以計算出相同的位置。

package net.mrliuli.containers;import java.util.*;public class SimpleHashMap<K, V> extends AbstractMap<K, V> {    // Choose a prime number for the hash table size, to achieve a uniform distribution:    static final int SIZE = 997;    // You can't have a physical array of generics, but you can upcast to one:    @SuppressWarnings("unchecked")    LinkedList<MapEntry<K,V>>[] buckets = new LinkedList[SIZE];    @Override    public V put(K key, V value){        int index = Math.abs(key.hashCode()) % SIZE;        if(buckets[index] == null){            buckets[index] = new LinkedList<MapEntry<K,V>>();        }        LinkedList<MapEntry<K,V>> bucket = buckets[index];        MapEntry<K,V> pair = new MapEntry<K,V>(key, value);        boolean found = false;        V oldValue = null;        ListIterator<MapEntry<K,V>> it = bucket.listIterator();        while(it.hasNext()){            MapEntry<K,V> iPair = it.next();            if(iPair.equals(key)){                oldValue = iPair.getValue();                it.set(pair); // Replace old with new                found = true;                break;            }        }        if(!found){            buckets[index].add(pair);        }        return oldValue;    }    @Override    public V get(Object key){        int index = Math.abs(key.hashCode()) % SIZE;        if(buckets[index] == null) return null;        for(MapEntry<K,V> iPair : buckets[index]){            if(iPair.getKey().equals(key)){                return iPair.getValue();            }        }        return null;    }    @Override    public Set<Map.Entry<K,V>> entrySet(){        Set<Map.Entry<K,V>> set = new HashSet<Map.Entry<K, V>>();        for(LinkedList<MapEntry<K,V>> bucket : buckets){            if(bucket == null) continue;            for(MapEntry<K,V> mpair : bucket){                set.add(mpair);            }        }        return set;    }    public static void main(String[] args){        SimpleHashMap<String, String> m = new SimpleHashMap<String, String>();        for(String s : "to be or not to be is a question".split(" ")){            m.put(s, s);            System.out.println(m);        }        System.out.println(m);        System.out.println(m.get("be"));        System.out.println(m.entrySet());    }}

4.5 覆蓋hashCode()

  設計`hashCode()`時要考慮的因素:

  • 最重要的因素:無論何時,對同一相對象調用hashCode()都應該產生同樣的值

  • 此外,不應該使hashCode()依賴於具有唯一性的對象資訊,尤其是使用this的值,這隻能產生很糟糕的hashCode()。因為這樣做無法產生一個新的鍵,使之與put()中原始的索引值對中的鍵相同。即應該使用對象內有意義的識別資訊。也就是說,它必須基於對象的內容產生散列碼。

  • 但是,通過hashCode() equals()必須能夠完全確定對象的身份。

  • 因為在產生桶的下標前,hashCode()還需要進一步處理,所以散列碼的產生範圍並不重要,只要是int即可。

  • 好的hashCode()應該產生分布均勻的散列碼。

《Effective Java Programming Language Guide (Addison-Wesley, 2001)》為怎樣寫出一個像樣的hashCode()給出了一個基本的指導:

  1. int變數result賦予一個非零值常量,如17

  2. 為對象內每個有意義的域f(即每個可以做equals()操作的域)計算出一個int散列碼c

域類型 計算
boolean c=(f?0:1)
byte、char、short或int c=(int)f
long c=(int)(f^(f>>>32))
float c=Float.floatToIntBits(f);
double long l = Double.doubleToLongBits(f);
Object,其equals()調用這個域的equals() c=f.hashCode()
數組 對每個元素應用上述規則

3. 合并計算散列碼:result = 37 * result + c;
4. 返回result。
5. 檢查hashCode()最後產生的結果,確保相同的對象有相同的散列碼。

5 選擇不同介面的實現

5.1 微基準測試的危險(Microbenchmarking dangers)

已證明0.0是包含在Math.random()的輸出中的,按照數學術語,即其範圍是[0,1)

5.2 HashMap的效能因子

  HashMap中的一些術語:

  • 容量(Capacity):表中的桶位元(The number of buckets in the table)。

  • 初始容量(Initial capacity):表在建立時所擁有的桶位元。HashMapHashSet都具有允許你指定初始容量的構造器。

  • 尺寸(Size):表中當前儲存的項數。

  • 負載因子(Loadfactor):尺寸/容量。空表的負載因子是0,而半滿表的負載因子是0.5,依此類推。負載輕的表產生衝突的可能性小,因此對於插入和尋找都是最理想的(但是會減慢使用迭代器進行遍曆的過程)。HashMapHashSet都具有允許你指定負載因子的構造器,表示當負載情況達到該負載的水平時,容器將自動增加其容量(桶位元),實現方式是使容量大致加倍,並重新將現有對象分布到新的桶位集中(這被稱為再散列)。

HashMap使用的預設負載因子是0.75(只有當表達到四分之三滿時,才進行再散列),這個因子在時間和空間代價之間達到了平衡。更高的負載因子可以降低表所需的空間,但會增加尋找代價,這很重要,因為尋找是我們在大多數時間裡所做的操作(包括get()put())。

6 Collection或Map的同步控制

  Collections類有辦法能夠自動同步整個容器。其文法與“不可修改的”方法相似:

package net.mrliuli.containers;import java.util.*;public class Synchronization {    public static void main(String[] args){        Collection<String> c = Collections.synchronizedCollection(new ArrayList<String>());        List<String> list = Collections.synchronizedList(new ArrayList<String>());        Set<String> s = Collections.synchronizedSet(new HashSet<String>());        Set<String> ss = Collections.synchronizedSortedSet(new TreeSet<String>());        Map<String, String> m = Collections.synchronizedMap(new HashMap<String, String>());        Map<String, String> sm = Collections.synchronizedSortedMap(new TreeMap<String, String>());    }}

6.1 快速報錯(fail-fast)

  Java容器有一種保護機制能夠防止多個進行同時修改同一個容器的內容。Java容器類類庫採用快速報錯(fail-fast)機制。它會探查容器上的任何除了你的進程所進行的操作以外的所有變化,一旦它發現其他進程修改了容器,就會立刻拋出ConcurrentModificationException異常。這就是“快速報錯”的意思——即,不是使用複雜的演算法在事後來檢查問題。

package net.mrliuli.containers;import java.util.*;public class FailFast {    public static void main(String[] args){        Collection<String> c = new ArrayList<>();        Iterator<String> it = c.iterator();        c.add("An Object");        try{            String s = it.next();        }catch(ConcurrentModificationException e){            System.out.println(e);        }    }}

相關文章:

Java編程思想學習課時(三)第15章-泛型

Java編程思想學習課時(五)第18章-Java IO系統

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.