Java集合之Map

来源:互联网 发布:c 语言编译器安卓版 编辑:程序博客网 时间:2024/06/05 03:30

存储双元素的容器Map

Map

Map Interface是存储双元素key-value容器接口,提供了对key-value的增删改查,注意其并没有继承Iterable,因此无法直接获取Map的Iterator。

public interface Map<K,V> {    // Query Operations    int size();    boolean isEmpty();    boolean containsKey(Object key);    boolean containsValue(Object value);    V get(Object key);    // Modification Operations    V put(K key, V value);    V remove(Object key);    // Bulk Operations    void putAll(Map<? extends K, ? extends V> m);    void clear();    //view    Set<K> keySet();    Collection<V> values();    Set<Map.Entry<K, V>> entrySet();    interface Entry<K,V> {        K getKey();        V getValue();        V setValue(V value);        boolean equals(Object o);        int hashCode();    }    boolean equals(Object o);    int hashCode();}

Map有个抽象实现AbstractMap,实现了Map的一些通用方法,实现Map接口的具体类一般都继承自AbstractMap.

HashMap

我们学习数据结构是就知道哈希表的实现就是内部一个数组作为桶,根据key计算hash值,将KeyValue存储到对应的位置,计算key的哈希函数应该尽量均匀分布,但是还是无法完全避免hash冲突,解决hash冲突的有拉链发、线性探测等等。

没错,java.util.HashMap就是完全这样实现的,它使用了拉链发解决hash冲突。

使用数组的问题就是bucket的个数是固定的,随着map中元素个数的增加,冲突会越来越严重,所以需要根据负载因子决定是否对数组进行动态扩容,动态扩容后所有元素也要rehash.

//默认bucket个数为16static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16//HashMap默认负载因子static final float DEFAULT_LOAD_FACTOR = 0.75f;

负载因子=HashMap中元素个数/Bucket个数, 默认值为0.75。

put

ReHash是由Put引起的,看看put的源码:

public V put(K key, V value) {    if (table == EMPTY_TABLE) {        inflateTable(threshold);    }    if (key == null)        return putForNullKey(value);    int hash = hash(key);    int i = indexFor(hash, table.length);    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;}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++;}

比较简单,就是先检查是否存在相同的key,若存在则覆盖,否则调用addEntry添加,添加前先检查是否需要reHash, threshold就是桶个数*LoadFactor, 同时也要判断需要插入的桶是否为非空,reHash是直接将桶的个数扩大为原来的两倍,重新计算bucketIndex后进行插入,插入的过程是直接添加到对应bucket头部。

ReHash

void resize(int newCapacity) {    Entry[] oldTable = table;    int oldCapacity = oldTable.length;    if (oldCapacity == MAXIMUM_CAPACITY) {        threshold = Integer.MAX_VALUE;        return;    }    Entry[] newTable = new Entry[newCapacity];    transfer(newTable, initHashSeedAsNeeded(newCapacity));    table = newTable;    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);}//Transfers all entries from current table to newTable.void transfer(Entry[] newTable, boolean rehash) {    int newCapacity = newTable.length;    for (Entry<K,V> e : table) {        while(null != e) {            Entry<K,V> next = e.next;            if (rehash) {                e.hash = null == e.key ? 0 : hash(e.key);            }            int i = indexFor(e.hash, newCapacity);            e.next = newTable[i];            newTable[i] = e;            e = next;        }    }}

transfer会将对依次对每个元素重新计算bucket index, 如果bucket index不变,同一个bucket中元素的顺序会颠倒。

了解了put过程,来看看HashMap中是如何计算Hash值的,又是如何通过Hash值计算buket index的。

put方法中对null值调用putForNullKey,非null则直接用hash(Object)计算哈希值。

private V putForNullKey(V value) {    for (Entry<K,V> e = table[0]; e != null; e = e.next) {        if (e.key == null) {            V oldValue = e.value;            e.value = value;            e.recordAccess(this);            return oldValue;        }    }    modCount++;    addEntry(0, null, value, 0);    return null;}

可以看出对于key为null的Entry总是存放在bucket index为0的桶中,通过addEntry中也可看出,对Entry的value没有任何检查,因此同样可以put value为null的Entry。

JDK7中实现:

final int hash(Object k) {    int h = hashSeed;    if (0 != h && k instanceof String) {        return sun.misc.Hashing.stringHash32((String) k);    }    h ^= k.hashCode();    // This function ensures that hashCodes that differ only by    // constant multiples at each bit position have a bounded    // number of collisions (approximately 8 at default load factor).    h ^= (h >>> 20) ^ (h >>> 12);    return h ^ (h >>> 7) ^ (h >>> 4);}

为啥不直接调用 Object.hashCode(),其实是HashMap为了防止用户实现的hashCode分布不均匀,产生碰撞比较频繁,所以又进行了hash二次加工。

JDK8实现:

static final int hash(Object key) {    int h;    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}

直接用hashCode的高16位与低16位进行异或,这样简单难度不担心碰撞,其实JDK8的设计对碰撞进行了优化,当桶数量达到64时,桶中拉链长度达到8,建一棵红黑树,解决严重冲突时的性能问题。

JDK7与JDK8中hashmap的差别以及高性能下场景下使用建议:

http://www.importnew.com/21429.html

indexFor

从上面代码可看出计算bucket index函数式indexFor, 按照我们一般的想法就是哈希值对桶的长度取模求余,JDK中使用与运算进行了优化,前提是bucket size总是2的n次幂,因此bucket capacity默认值、初始大小、扩容后都是2的n次幂。

 //Returns index for hash code h, length is size of bucket.static int indexFor(int h, int length) {    // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";    return h & (length-1);}

进行rehash后bucket size变成原来的两倍,通过indexFor的实现可以看出重新计算bucket index时只有两种情况,要么bucket index不变,要么后移原bucket size大小,可参考以下文章查看详细解析。

http://www.importnew.com/27043.html

遍历

我们知道Map遍历有两种方式,通过KeySet和EntrySet.

KeySet:

Map map=new HashMap();Iterator it=map.keySet().iterator();    //KeyIteratorObject key;Object value;while(it.hasNext()){    key=it.next();    value=map.get(key);    System.out.println(key+":"+value);}

EntrySet:

Map map=new HashMap();Iterator it=map.entrySet().iterator();  //EntryIteratorObject key;Object value;while(it.hasNext()){    Map.Entry entry = (Map.Entry)it.next();    key=entry.getKey();    value=entry.getValue();    System.out.println(key+"="+value);}

两种Iterator源码:

private final class KeyIterator extends HashIterator<K> {    public K next() {        return nextEntry().getKey();    }}private final class ValueIterator extends HashIterator<V> {    public V next() {        return nextEntry().value;    }}private final class EntryIterator extends HashIterator<Map.Entry<K,V>> {    public Map.Entry<K,V> next() {        return nextEntry();    }}final Entry<K,V> nextEntry() {    if (modCount != expectedModCount)        throw new ConcurrentModificationException();    Entry<K,V> e = next;    if (e == null)        throw new NoSuchElementException();    if ((next = e.next) == null) {        Entry[] t = table;        while (index < t.length && (next = t[index++]) == null)            ;    }    current = e;    return e;}

从源码可看出KeyIterator、ValueIterator、EntryIterator都是按照bucket index从大到小、bucket内部从前到后,依次遍历元素。

通过keySet()、valueSet()、entrySet()获取的都是原始HashMap的视图,共享底层数据结构。

然而,通过keySet遍历时,获取value时,需要调用get(key)方法,进行一次查找,因此效率更低。

ConcurrentHashMap

HashMap是非线程安全的,经常看见多线程下使用ConcurrentHashMap。
HashMap非线程安全主,在于同时进行put操作时可能发错数据混乱,主要体现在resize方法上,具体参考:

https://coolshell.cn/articles/9606.html

http://www.importnew.com/21396.html

从继承关系图可以看出CHM实现了ConcurrentMap接口

public interface ConcurrentMap<K, V> extends Map<K, V> {    V putIfAbsent(K key, V value);    boolean remove(Object key, Object value);    boolean replace(K key, V oldValue, V newValue);    V replace(K key, V value);}

ConcurrentMap主要是在Map接口上添加了修改操作,javadoc中对这几个方法含义有清晰说明。

ConcurrentHashMap是如何实现线程安全的了? 从 Java7 中源码可以看出使用了分段锁机制,每次需要加锁是在Segment上加锁,提高并发度,默认并发度为16.

static final int DEFAULT_CONCURRENCY_LEVEL = 16;//Segment继承自ReentrantLockfinal Segment<K,V>[] segments;

从Put实现可以看出ConcurrentHashMap不允许key或value为null,会抛出NullPointerException

使用ConcurrentHashMap时需要注意putifAbsent方法,参见

http://www.importnew.com/21388.html

CHM适用于做cache,在程序启动时初始化,之后可以被多个请求线程访问。

JDK8中实现与JDK7有差别,参考以下,目前我还没有研究源码:

http://www.jianshu.com/p/c0642afe03e0

HashTable && SynchronizedMap

HashTable也是线程安全的,但是同Vector一样,也是直接在方法上加synchronized,在这个对象上加锁,效率较低。同样,Collections.SynchronizedMap同Collections.SynchronizedList一样,只是换了一个加锁对象。

从HashTable的put方法看出HashTable同样不允许key或value为null.

LinkedHashMap


LinkedHashMap中继承自HashMap,但是其Entry中增加了两个指针before-after,在保持与HashMap相同的存储方式的同时,使用双向链表维持了插入元素的顺序,遍历时是按插入顺序来的。 成员变量accessOrder指定是按插入顺序有序还是按访问顺序有序,若按访问熟悉,每次访问后被访问的元素移动至双向链表头部。

private final boolean accessOrder;private static class Entry<K,V> extends HashMap.Entry<K,V> {    // These fields comprise the doubly linked list used for iteration.    Entry<K,V> before, after;    Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {        super(hash, key, value, next);    }    private void remove() {        before.after = after;        after.before = before;    }    private void addBefore(Entry<K,V> existingEntry) {        after  = existingEntry;        before = existingEntry.before;        before.after = this;        after.before = this;    }    void recordAccess(HashMap<K,V> m) {        LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;        if (lm.accessOrder) {            lm.modCount++;            remove();            addBefore(lm.header);        }    }    void recordRemoval(HashMap<K,V> m) {        remove();    }}

EnumMap

EnumMap也实现了Map接口,其限定了key类型为Enum,对于Key已知范围的,可以使用EnumMap.

public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V>    implements java.io.Serializable, Cloneable

TreeMap

HashMap是无序的,TreeMap是按Key有序的Map,其内部结构使用了红黑树,红黑树是一种二叉平衡树,相对AVL,查找效率较低,但是插入删除时自平衡需旋转次数要较少。

ConcurrentSkipListMap

ConcurrentSkipListMap是内部使用SkipList实现的Map,跳表是一种有序的链表,但是在List上建了索引,增删改查时间复杂度都是O(logN),而相比二叉平衡树,其不需要自平衡进行旋转,同时,在并发情况下,其实现同步加锁的区域较少,性能更好,因此并发有序,使用ConcurrentSkipListMap。

ConcurrentLinkedHashMap?

JDK没提供,也许是并发下加锁代价大,goolge貌似有个,用来做缓存。

IdentityHashMap

起内部结构跟HashMap相同,但是Key比较方式不是key1.equals(key2)来判断key是否相同,而是通过key1==key2来判断,违反Map设计原则和语义,因此只有很少特殊情况才会有用。

Properties

我们常用与读取配置文件的Properties,直接继承自HashTable,也是线程安全的,主要用于从.prop和xml中读取或存取为这两种格式,然后通过get(String)的方式可以获取具体的配置变量值。

    //加载    public synchronized void load(Reader reader) throws IOException;    public synchronized void load(InputStream inStream) throws IOException    public synchronized void loadFromXML(InputStream in) throws IOException, InvalidPropertiesFormatException;    //遍历查找    public Enumeration<?> propertyNames();    public String getProperty(String key);    public String getProperty(String key, String defaultValue);    //存取    public void store(OutputStream out, String comments) throws IOException    public void store(Writer writer, String comments) throws IOException    public void storeToXML(OutputStream os, String comment) throws IOException    public void storeToXML(OutputStream os, String comment, String encoding) throws IOException

.prop文件格式:

# 以下为数据库表信息dbTable = mytable# 以下为服务器信息ip = 192.168.0.9

.xml格式当然是要符合对应的dtd才能正确被解析了

<?xml version="1.0" encoding="UTF-8" standalone="no"?>  <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">  <properties>  <comment>test</comment>  <entry key="age">25</entry>  <entry key="name">tinyfun</entry>  <entry key="sex">man</entry>  <entry key="title">software developer</entry>  </properties>