Java Map浅谈(jdk1.7)


Map是非常常用的一种数据结构,最常用的一些Map实现如下图所示:


围绕着Map接口,最主要的实现类有HashTable、TreeMap、HashMap、LinkedHashMap和Properties。

首先对于HashTable和HashMap,两者都实现了Map接口,但HashTable的大部分方法做了同步,而HashMap没有,因此,HashMap不是线程安全的;而且,HashTable不允许key或者value使用null值,而HashMap可以,再者,在内部算法上,它们对key的hash算法和hash值内内存索引的映射算法不同。

1.HashMap的实现

 HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

HashMap底层就是一个数组结构,数组中的每一项又是一个链表。

    /**
     * The table, resized as necessary. Length MUST Always be a power of two.
     */
    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;//数组,就是这个数组

    static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;//下一个
        int hash;  //通过hash算法算出的hash值
        ......
table就是一个Entry<K,V>类型的数组,当新建一个HashMap的时候,就会初始化一个数组,长度必须是2的倍数,默认为16,而Entry<K,V>则又是一个链表,hash值相同而key不同的键值对就会以链表的形式存放在table的同一位置。

HashMap的存储实现:

    public V put(K key, V value) {
        if (table == EMPTY_TABLE) {//如果数组是空的,长度为0
        	//初始化数组,threshold=capacity * loadFactor,即数组大小*负载因子(默认折中0.75)
        	//负载因子越大,表示数组的利用率越高,数组中空的位置就越少,虽然这样增加了内存了利用率,
        	//但是,也增加了hash值冲突的几率,也就是增加了链表的长度,增大了取值的难度,所以,要在时间和空间上取一个平衡点,0.75
        	inflateTable(threshold); 
        } 
        if (key == null)
        		//如果key为空值,则放在数组的第一个位置
        		return putForNullKey(value); 
        int hash = hash(key);//根据key计算hash值 
        int i = indexFor(hash, table.length);//通过hash值确定存放在数组中的具体位置--类似于取模运算 
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {//根据下标i,遍历链表 
        	Object k; 
        	if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {//如果hash值相同且key也相同,则,覆盖原值value,key不变 
        		V oldValue = e.value; 
        		e.value = value; 
        		e.recordAccess(this); 
        		return oldValue; 
        	} 
        } 
        modCount++;//修改标记加1,即最新标记,如果此时,其他的线程修改此map,会快速失败抛出异常 
        addEntry(hash, key, value, i);//增加新的entry 
        return null; 
        }

简单地说,HashMap就是将key做hash算法,然后将hash值映射到内存地址,直接取得key所对应的数据。在HashMap中,底层数据结构使用的是数组,所谓的内存地址即数组的下标索引。HashMap的高性能需要保证以下几点:

a.hash算法必须是高效的;

b.hash值到内存地址(数组索引)的算法是快速的;

c.根据内存地址(数组索引)可以直接取得对应的值。

下面再对以上代码用到的几个方法做个解析:

    final int hash(Object k) {
        int h = hashSeed;//如果hashSeed==0,Hashing将会被禁用
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }

        h ^= k.hashCode();//获取响应的hashcode,可参照String类中的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).
              //将二进制中的"1"分布的尽量均匀,使得下面indexFor()得到的下标也相对均匀,减少hash碰撞的几率
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
    /**
     * Returns index for hash code h. 
     */
    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);//按位取与,相当于取模mod或者取余%
    }
从上面可以看到,不管是计算hash值还是取模,都是使用的位运算,还是非常高效的。而HashTable就不是这样了,也许是年久失修吧,有兴趣的同学可以看看。
    void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {//如果实际存储的个数大于可用的数量,扩容
            resize(2 * table.length);//扩容为原来的2倍,并重新计算在数组中的位置,有点耗费性能,如果已知大小,最好创建时候指定capacity
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
        createEntry(hash, key, value, bucketIndex);//增加一个新entry
    }
      //增加一个新entry,如果bucketIndex位置已经有链表则放在头部,如果没有,就新建链表并放头部
    void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];//在bucketIndex位置取出链表
        table[bucketIndex] = new Entry<>(hash, key, value, e);//将新entry放在链表头部
        size++;//实际大小加1
    }
HashMap的取值实现:
    public V get(Object key) {
        if (key == null)//如果key为空,在数组第一个位置遍历链表
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }
    private V getForNullKey() {
        if (size == 0) {
            return null;
        }
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
    }
    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 &&//如果hash值相同并且key也相同,读取value
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }

2.LinkedHashMap------有序的HashMap

LinkedHashMap继承自HashMap,因此,它具备了HashMap的优良特性--高性能。在HashMap的基础上,LinkedHashMap又在内部增加了一个链表,用以存放元素的顺序。因此,LinkedHashMap可以简单地理解为一个维护了元素次序表的HashMap。

LinkedHashMap可以提供两种类型的顺序:一是元素插入时的顺序;二是最近访问的顺序。可以通过以下构造函数类指定排序行为:

public LinkedHashMap(int initialCapacity,float loadFactor,boolean accessOrder)
当accessOrder为true时,按照元素最后访问时间排序;否则按照插入顺序排序。默认为false。

在内部实现中,LinkedHashMap通过继承HashMap.Entry类,实现了LinkedHashMap.Entry,为LinkedHashMap.Entry增加了before和after属性用以记录某一表项的前驱和后继,并构成循环链表。

    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;
。。。

如果将accessOrder设置为true,那么在迭代的过程中使用get()操作就会抛出java.util.ConcurrentModificationException。因为只要在迭代器中修改被迭代的集合,就会抛出ConcurrentModificationException异常,这个特性适用于所有的集合类,包括HashMap、Vector、ArrayList等。

因为如果LinkedHashMap工作在按照元素访问顺序排序的模式中,get()方法会修改LinkedHashMap中的链表结构,以便将最近访问的元素放置到链表的末尾,所以就有了这个异常错误。

3.TreeMap-----另一种Map的实现

TreeMap实现了SortMap接口,就意味着它可以对元素进行排序,同样的,它的性能也有略微低于HashMap。

与LinkedHashMap不同,LinkedHashMap是根据元素增加或者访问的先后顺序进行排序,而TreeMap则根据元素的key进行排序。为了确定key的排序算法,可以使用两种方式指定:

(1)在TreeMap的构造函数中注入一个Comparator:

public TreeMap(Comparator<? super K> comparator)
(2)使用一个 实现了Comparable接口的key。
对于TreeMap而言,排序是一个必须进行的过程,因此,要正常使用TreeMap,一定要通过其中一种方式将排序规则传递给TreeMap。如果既不指定Comparator,又不去实现Comparable接口,那么在put()操作时,就会抛出java.lang.ClassCastException异常。

TreeMap的内部实现是基于红黑树的。红黑树是一种平衡查找树,它的统计性能有优于平衡二叉树。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值