1. 程式人生 > >Java筆記(七)Map和Set

Java筆記(七)Map和Set

Map和Set

一)HashMap

1.Map介面

interface Map<K,V> {
    int size();//檢視Map中的鍵值對個數
    boolean isEmpty();//是否為空
    boolean containsKey(Object key);//是否包含某個鍵
    boolean containsValue(Object value);//是否包含某個值
    V get(Object key);//根據鍵獲取值,沒找到返回null
    V put(K key, V value);//儲存鍵值對,如果原來有key,則覆蓋並返回原來的值,原來沒有這個鍵返回null
V remove(Object key);//根據鍵刪除鍵值對,返回key原來的值,如果不存在,返回null void putAll(Map<? extends K,? extends V> m);//儲存m中所有的鍵值對到當前map void clear();//清空Map中的所有鍵值對 Set<K> keySet();//獲取Map中所有鍵的集合 Collection<V> values();//獲取Map中所有值的集合 Set<Map.Entry<K, V>> entrySet();//獲取Map中所有的鍵值對
interface Entry<K,V> { //巢狀介面表示一個鍵值對 K getKey();//獲取鍵 V getValue();//獲取值 V setValue(V value);//設定值 boolean equals(Object o); int hashCode(); } boolean equals(Object o); int hashCode(); }

keySet()、values()、entrySet()有一個共同特點,它們返回的都是檢視,

不是複製值,基於返回值的修改都會修改Map自身。例如:

map.keySet().clear();//會刪除所有鍵值對

2.HashMap

構造方法:

public HashMap(int initialCapacity)
public HashMap(int initialCapacity, float loadFactor)
public HashMap(Map<? extends K, ? extends V> m)

主要例項變數:

static final Entry<?,?>[] EMPTY_TABLE = {};
//table是一個Entry型別的陣列,稱為雜湊表或者雜湊桶,
//其中每個元素指向一個單向連結串列,連結串列中的每個節點表示一個鍵值對
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
transient int size; //實際鍵值對個數
int threshold;//表示閾值,當鍵值對個數size大等於該值時考慮擴充套件 threshold = table.length *  loadFactor
final float loadFactor;

Entry是一個內部類,構造方法:

    static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next; //指向下一個節點
        int hash; //key的hash值
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h; //儲存hash值是為了在比較的時候加快速度
        }
    }

當新增鍵值對後,table就不是空表了,它會隨著鍵值對的新增進行擴充套件,擴充套件的策略類似於ArrayList.

預設構造方法:

public HashMap() {
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
//其中DEFAULT_INITIAL_CAPACITY=16,DEFAULT_LOAD_FACTOR=0.75

呼叫了:

public HashMap(int initialCapacity, float loadFactor) {
    this.loadFactor = loadFactor;
    threshold = initialCapacity;
}

put方法:

    public V put(K key, V value) {
        if(table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if(key == null)
            return putForNullKey(value);
        int hash = hash(key); //計算key的hash值
        int i = indexFor(hash, table.length); //計算應該將這個鍵值對放入table中的哪個位置
//注意table是一個單向連結串列,現在在這個連結串列中查詢是否已經有這個鍵
for(Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if(e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++;
addEntry(hash, key, value, i);
return null; }

 

final int hash(Object k) {
    int h = 0
    h ^= k.hashCode();
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

 

static int indexFor(int h, int length) {
    return h & (length-1);//等同於h%length
}

 

如果是第一個儲存,先呼叫inflateTable方法給table分配空間:

    private void inflateTable(int toSize) {
        //Find a power of 2 >= toSize
        int capacity = roundUpToPowerOf2(toSize); //capacity的預設值為16
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); //預設為12
        table = new Entry[capacity];
    }
    void addEntry(int hash, K key, V value, int bucketIndex) {
        if((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); }
void createEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    size++;
}

總結儲存鍵值對過程:

1)計算鍵的雜湊值

2)根據雜湊值得到儲存位置

3)插到對應位置的連結串列頭部或更新已有值

4)根據需要擴充套件table大小

例子:

Map<String,Integer> countMap = new HashMap<>();
countMap.put("hello", 1);
countMap.put("world", 3);
countMap.put("position", 4);

 

 

 

3.查詢方法

    public V get(Object key) {
        if(key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);
        return null == entry ? null : entry.getValue();
    }
    final Entry<K,V> getEntry(Object key) {
        if(size == 0) {
            return null;
        }
        int hash = (key == null) ? 0 : hash(key);
        for(Entry<K,V> e = table[indexFor(hash, table.length)];e != null; e = e.next) {
            Object k;
            if(e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }

containsKey()方法邏輯與之類似:

public boolean containsKey(Object key) {
    return getEntry(key) != null;
}

HashMap可以方便高效地按鍵進行操作,但如果要按值進行操作,就要進行遍歷

    public boolean containsValue(Object value) {
        if(value == null)
            return containsNullValue();
        Entry[] tab = table;
        for(int i = 0; i < tab.length ; i++)
            for(Entry e = tab[i] ; e != null ; e = e.next)
                if(value.equals(e.value))
                    return true;
        return false;
    }

4.按鍵刪除鍵值對

public V remove(Object key) {
    Entry<K,V> e = removeEntryForKey(key);
    return(e == null ? null : e.value);
}
    final Entry<K,V> removeEntryForKey(Object key) {
        if(size == 0) {
            return null;
        }
        int hash = (key == null) ? 0 : hash(key);
        int i = indexFor(hash, table.length);
        Entry<K,V> prev = table[i];
        Entry<K,V> e = prev;
        while(e != null) {
            Entry<K,V> next = e.next;
            Object k;
            if(e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k)))) {
                modCount++;
                size--;
                if(prev == e)
                    table[i] = next;
                else
                    prev.next = next;
                e.recordRemoval(this);
                return e;
            }
            prev = e;
            e = next;
        }
        return e;
    }

5.實現原理總結

1)根據鍵獲取和儲存值的效率都很高,為O(1),單向連結串列往往只有一個或

少數幾個節點,根據hash值就可以快速直接定位;

2)HashMap中的鍵值對沒有順序,因為hash值是隨機的。

注意,HashMap不是執行緒安全的,Java中還有一個類HashTable,它是Java最早實現的

容器類之一,實現了Map介面,實現原理與HashMap類似,但沒有特別優化,它內部通過

synchronized實現了執行緒安全。另外,在HashMap中鍵和值都可以為null,而在HashTable

中不可以。在不需要併發安全的場景中推薦使用HashMap。高併發場景中,推薦使用ConcurrentHashMap。

根據雜湊值存取物件、比較物件是計算機程式中的一種重要思維方式,它使得存取物件主要依賴於自身

的雜湊值,而不是與其他物件進行比較,存取效率也與集合大小無關,高達O(1),即使是進行比較,也能

比較Hash值提高比較效率。

二)HashSet

1.概述

HashSet實現了Set介面。

Set介面表示的是沒有重複元素,且不保證順序的容器的介面,

它擴充套件自Collection,雖然沒有定義任何新方法,不過

對於其中的一些方法,它有自己的規範。

public interface Set<E> extends Collection<E> {
    int size();
    boolean isEmpty();
    boolean contains(Object o);
    //迭代遍歷時不強制要求元素之間有特別的順序
    //但某些Set實現可能有順序,比如ThreeSet
    Iterator<E> iterator();
    Object[] toArray();
    <T> T[] toArray(T[] a);
    //新增元素時,如果集合中已經存在相同元素了,則不會改變集合,直接返回false
    //只有不存在時才會新增,返回true
    boolean add(E e);
    boolean remove(Object o);
    boolean containsAll(Collection<?> c);
    //重複的元素不新增,不重複的元素才新增,如果集合有變化,返回true
    boolean addAll(Collection<? extends E> c);
    boolean retainAll(Collection<?> c);
    boolean removeAll(Collection<?> c);
    void clear();
    boolean equals(Object o);
    int hashCode();
}

構造方法與HashMap類似:

public HashSet()
public HashSet(int initialCapacity)
public HashSet(int initialCapacity, float loadFactor)
public HashSet(Collection<? extends E> c)

與HashMap類似,HashSet要求元素重寫hashCode與equals方法,且對於兩個物件,

如果equals相同,則hashCode也必須相同。如果元素是自定義類特別要注意這一點。

public class Dog {

    private String name;
    private int number;
    public Dog(String name, int number) {
        this.name = name;
        this.number = number;
    }
    @Override
    public String toString() {
        return "Name: " + name + "  Number: " + number;
    }
}
        Dog a = new Dog("King", 110);
        Dog b = new Dog("King", 110);
        HashSet<Dog> dogs = new HashSet<>();
        dogs.add(a);
        dogs.add(b);
        System.out.println("The set is " + dogs);
        //The set is [Name: King  Number: 110, Name: King  Number: 110]

2.實現原理

HashSet內部是用HashMap實現的,其內部有一個HashMap例項變數:

private transient HashMap<E,Object> map;

Map有鍵和值,HashSet相當於只有鍵,值都是相同的固定值,這個值定義為:

private static final Object PRESENT = new Object();

構造方法:

    public HashSet() {
        map = new HashMap<>();
    }
public HashSet(int initialCapacity, float loadFactor) {
    map = new HashMap<>(initialCapacity, loadFactor);
}
public HashSet(Collection<? extends E> c) {
    map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
    addAll(c);
}

add方法:

public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

檢查是否包含:

 

public boolean contains(Object o) {
    return map.containsKey(o);
}

 

刪除:

public boolean remove(Object o) {
    return map.remove(o)==PRESENT;
}

迭代器:

public Iterator<E> iterator() {
    return map.keySet().iterator();
}

3.HashSet特點總結

1)沒有重複元素

2)沒有順序

3)可以高效地新增,刪除,判斷元素是否存在