Java 集合之 Map

版权声明:本文为博主原创文章,未经博主允许不得转载。个人公众号:yujikui1115 https://blog.csdn.net/yujikui1/article/details/81947741

Map 就是另一个顶级接口了,总感觉 Map 是 Collection 的子接口呢。Map 主要用于表示那些含有映射关系的数据,存储的是一组一组的键值对。Map 是允许你将某些对象与其它一些对象关联起来的关联数组。

举个例子感受一下:我想通过学生的学号来找到对应的姓名就可以使用 Map 来存储 Map< Integer ,String > 。我想知道每个学生一共选了几门课可以这样存储 Map < Student ,List < Course > > 。这样我们就将 Student 这个类和课程的集合 List < Course > 关联起来了 。

下面来说说 Map 这个顶级的接口都有哪些具体的实现。
这里写图片描述

HashMap :它根据键的 hashCode 值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap 最多只允许一条记录的键为 null ,允许多条记录的值为 null 。HashMap 非线程安全,即任一时刻可以有多个线程同时写 HashMap ,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections 的 synchronizedMap 方法使HashMap 具有线程安全的能力,或者使用 ConcurrentHashMap 。

Hashtable :Hashtable 是遗留类,很多映射的常用功能与 HashMap 类似,不同的是它承自 Dictionary 类,并且是线程安全的,任一时间只有一个线程能写 Hashtable,并发性不如 ConcurrentHashMap,因为 ConcurrentHashMap 引入了分段锁。Hashtable 不建议在新代码中使用,不需要线程安全的场合可以用 HashMap 替换,需要线程安全的场合可以用 ConcurrentHashMap 替换。

LinkedHashMap :LinkedHashMap 是 HashMap 的一个子类,保存了记录的插入顺序,在用 Iterator 遍历 LinkedHashMap 时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序 。

TreeMap :TreeMap 实现 SortedMap 接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用 Iterator 遍历 TreeMap 时,得到的记录是排过序的。如果使用排序的映射,建议使用 TreeMap 。在使用 TreeMap 时,key 必须实现 Comparable 接口或者在构造 TreeMap 时传入自定义的 Comparator ,否则会在运行时抛出ClassCastException 类型的异常。

对于上述四种 Map 类型的类,要求映射中的 key 是不可变对象。不可变对象是该对象在创建后它的哈希值不会被改变 。可以参考这篇文章理解,String 与不可变对象。如果对象的哈希值发生变化,Map 对象很可能就定位不到映射的位置了

HashMap 的底层主要是基于 hash 表,首先来介绍一下 hash 相关的知识。hash 又名散列,hash 表也就是散列表,hash 表的出现是为了使得数据的查找变得简单,快速,原理是根据关键字的 hashCode 值来确定该关键字存储的位置。而计算出 hashCode 值的方法也就是 hash 算法,若是不同的关键字计算出同一个 hashCode 值,那么就会存储在同一个位置上,此时也就发生了冲突。

我们想要在空间有限的前提下,尽量减少冲突的发生,从而保证我们的查找效率不受影响,就需要设计一个好的 hash 算法,也要充分考虑到当发生冲突了应该怎么办。

那就来看看 HashMap 中是怎么来设计的,主要体现在 hash 算法的设计,使用链表结构和 resize 。首先看一下有哪些重要的属性

public class HashMap<K,V>
    extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable
{
    // 默认长度
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    // 最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30;
    // 默认负载因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    // 初始化为空
    static final Entry<?,?>[] EMPTY_TABLE = {};
    // 存放键值对的 entry 数组
    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
    // 实际长度
    transient int size;
    // rehash 之前的最大长度 等于 DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY    
    int threshold;

    final float loadFactor;

    /**
     * The number of times this HashMap has been structurally modified
     * the HashMap fail-fast.  (See ConcurrentModificationException).
     */
    transient int modCount;

    static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;

        /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }

HashMap 用一个数组来表示 Hash 表,而数组中的每一个元素就是一个entry,也就是一个键值对。当发生冲突的时候,我们可以在同一个位置中存放多个 entry ,此时的结构是链表,通过 entry 中的 next 指向下一个 entry 。以上源码均来自 JDK 1.7 ,在 JDK 1.8 中 entry 变成了 Node 节点,而且若是当同一位置中的元素数量大于 8 这个阀值的时候,链表结构会变成红黑树,这样做的原因是可以大大加快元素的查找速度。

说完了 HashMap 中的结构,我们再来看看具体的操作。主要是 entry 的 put 和 get 过程,put 的过程,我放一张图,过程可一目了然。
这里写图片描述

map.put(“name” , “YJK923”); 这个过程就是通过 hashCode 方法计算 name 这个 String 对象的 hashCode 值(说一句,hashCode 这个方法是 Object 对象的,且是一个 native 方法)得到这个 hashCode 值之后还不能直接进行映射数组下标存储数据,为了使数据尽量不散落在同一位置,还多了一步 hash 值和index 值转化的步骤。

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

    static int indexFor(int h, int length) {
        return h & (length-1);
    }

看完了 put 看 get ,get 方法通过传入的 key 值计算出 hash 值再得出索引值,若是同一位置有多个元素,则再使用 key 的 equals 方法找到指定的 entry 。最终取出相应的 entry ,但是返回我我们的就是一个 value 值而已。

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

最后说一下 rehash 的过程,为什么会出现 rehash ,是因为实际长度已经达到了 threshold ,也就是 loadFactor * capacity 。设置这个值的原因就是为了防止过多的元素落在同一个桶中,增加了冲突的发生,及时的增加长度。我们知道 HashMap 的默认长度是 16 ,而若是发生了 rehash ,长度直接翻倍。且 resize 的过程中会重新创建一个新的 entry 数组来存放原有的数据,且所有的 entry 都会重新计算 hash 值。resize 也就是扩容,在扩容的时候会 rehash 。

 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 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);
    }

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

通过改进 hash 算法,增加链表的存储结构,rehash 等操作,我们就可以将原始的 hash 表和 hash 算法应用到 JDK 1.7 中的 HashMap 中,然而在 JDK 1.8 中又对 HashMap 的功能进行了增强,主要体现在以下方面 1 当链表的长度大于 8 的时候,就将链表转化为红黑树 。2 改进了 hash 的过程,也就是 key 映射到 index 这个过程进行增强,降低了冲突发生的可能。 3 对 rehash 的增强,使其不用重新计算之前 entry 的 index 值。

最后还补充一点关于 Map 的遍历,有几下几种方式:

1 获取 key 的集合 keySet 。

2 获取 value 的集合 Collection 。

3 获取 entry 的集合 entrySet 。

Set<Integer> keySet = map.keySet();
Iterator<Integer> it2 = keySet.iterator();
while(it2.hasNext()){
    System.out.println(it2.next());
}

Collection<String> values = map.values();
Iterator<String> it = values.iterator();
while(it.hasNext()){
    System.out.println(it.next());
}

Set<Entry<Integer,String>> entrySet = map.entrySet();
Iterator<Entry<Integer, String>> iterator = entrySet.iterator();
while (iterator.hasNext()) {
    Entry<Integer, String> entry = iterator.next();
    System.out.println(entry.getKey() + " ----> "+ entry.getValue());
}

最后我想问,还有谁 ?还在使用 JDK1.7 。

我 ~

推荐阅读:Java 集合之 Collection
参考资料:Java8系列之重新认识HashMap

猜你喜欢

转载自blog.csdn.net/yujikui1/article/details/81947741
今日推荐