• Java集合-Map


    Java集合-Map

    一、简介

    Map是以键值对来存储数据元素的。键值对之间存在映射关系,通过key可以查找value。需要注意的是key是不允许重复的,可以认为Map的key组成的集合是一个Set,上篇文章我们介绍Set时也发现Java中Set的实现大多数最后都是采用Map来存储数据。

    二、Map子类

    HashMap

    底层使用哈希表实现,需要实现hashcode和equals方法。面试中涉及最多的还是和HashTable的区别。首先前者是线程不安全的但效率较高key或vale均可为空,后者是线程安全的但线程安全是通过synchronized关键字实现的,key/value不能为空。

    需要掌握的内容:

    • 底层实现

    java1.8版本为数组+链表+红黑树实现,1.7为数组+链表。

        //内部定义了一个table数组来存放键值对,
        transient Node<K,V>[] table;
        
        //内部类Node表示一个键值对
        static class Node<K,V> implements Map.Entry<K,V> {
            final int hash;
            final K key;
            V value;
            Node<K,V> next;
    
            Node(int hash, K key, V value, Node<K,V> next) {
                this.hash = hash;
                this.key = key;
                this.value = value;
                this.next = next;
            }
    

    从上面可知HashMap使用内部类Node表示一个键值对,且定义了一个数组来存储键值对,存储过程是根据hash值来确定存储位置的,当发生hash冲突时会把后到的元素链接到上一个Node,当同一index的链表长度超过一定值后会把链表调整为红黑树。

    • 存取元素过程

          public V put(K key, V value) {
            //通过hash(key)计算key的hash值
              return putVal(hash(key), key, value, false, true);
          }
          
           final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                         boolean evict) {
              Node<K,V>[] tab; Node<K,V> p; int n, i;
              //初始化table数组大小
              if ((tab = table) == null || (n = tab.length) == 0)
                  n = (tab = resize()).length;
                  //根据hash值查找在数组中的index,如果找到的index位置上没有元素那么直接放置在该位置
              if ((p = tab[i = (n - 1) & hash]) == null)
                  tab[i] = newNode(hash, key, value, null);
              else {
                  //找到的index已存在元素即发生了hash碰撞
                  Node<K,V> e; K k;
                   //如果当前index的元素与要存入元素hash一样且key相等则将p赋值给e
                  if (p.hash == hash &&
                      ((k = p.key) == key || (key != null && key.equals(k))))
                      e = p;
                     //如果是树则插入树中
                  else if (p instanceof TreeNode)
                      e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
                  else {
                     //插入到链表中
                      for (int binCount = 0; ; ++binCount) {
                          if ((e = p.next) == null) {
                              p.next = newNode(hash, key, value, null);
                              if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                                  treeifyBin(tab, hash);
                              break;
                          }
                          if (e.hash == hash &&
                              ((k = e.key) == key || (key != null && key.equals(k))))
                              break;
                          p = e;
                      }
                  }
                  if (e != null) { // existing mapping for key
                      V oldValue = e.value;
                      if (!onlyIfAbsent || oldValue == null)
                          e.value = value;
                      afterNodeAccess(e);
                      return oldValue;
                  }
              }
              ++modCount;
              //插入后需要扩容
              if (++size > threshold)
                  resize();
              afterNodeInsertion(evict);
              return null;
          }
      

      HashMap存储过程使用put,put流程如下:

      1. 先通过hash函数定位桶位置,如果桶位置上元素为空则直接插入该位置
      2. 如果桶位置上元素不为空,判断key是否存在,如果key为空则直接覆盖
      3. key如果不为空那么判断当前元素是否是TreeNode类型,如果是那么红黑树直接插入
      4. 如果不是TreeNode类型那么遍历链表,如果key在链表中存在那么直接覆盖value
      5. 如果key在链表中不存在那么插入链表,插入后判断链表长度如果大于8那么调整链表为红黑树。
      6. 最后插入完要判断当前size是否大于临界值,如果大于要进行扩容
    • 扩容过程

          final Node<K,V>[] resize() {
              Node<K,V>[] oldTab = table;
              int oldCap = (oldTab == null) ? 0 : oldTab.length;
              int oldThr = threshold;
              int newCap, newThr = 0;
              if (oldCap > 0) {
                  if (oldCap >= MAXIMUM_CAPACITY) {
                      threshold = Integer.MAX_VALUE;
                      return oldTab;
                  }
                  else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                           oldCap >= DEFAULT_INITIAL_CAPACITY)
                           //扩容至原来两倍
                      newThr = oldThr << 1; // double threshold
              }
              else if (oldThr > 0) // initial capacity was placed in threshold
                  newCap = oldThr;
              else {               // zero initial threshold signifies using defaults
                  newCap = DEFAULT_INITIAL_CAPACITY;
                  newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
              }
              if (newThr == 0) {
                  float ft = (float)newCap * loadFactor;
                  newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                            (int)ft : Integer.MAX_VALUE);
              }
              threshold = newThr;
              @SuppressWarnings({"rawtypes","unchecked"})
                  Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
              table = newTab;
              if (oldTab != null) {
                  for (int j = 0; j < oldCap; ++j) {
                      Node<K,V> e;
                      if ((e = oldTab[j]) != null) {
                          oldTab[j] = null;
                          if (e.next == null)
                              newTab[e.hash & (newCap - 1)] = e;
                          else if (e instanceof TreeNode)
                              ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                          else { // preserve order
                              Node<K,V> loHead = null, loTail = null;
                              Node<K,V> hiHead = null, hiTail = null;
                              Node<K,V> next;
                              do {
                                  next = e.next;
                                  if ((e.hash & oldCap) == 0) {
                                      if (loTail == null)
                                          loHead = e;
                                      else
                                          loTail.next = e;
                                      loTail = e;
                                  }
                                  else {
                                      if (hiTail == null)
                                          hiHead = e;
                                      else
                                          hiTail.next = e;
                                      hiTail = e;
                                  }
                              } while ((e = next) != null);
                              if (loTail != null) {
                                  loTail.next = null;
                                  newTab[j] = loHead;
                              }
                              if (hiTail != null) {
                                  hiTail.next = null;
                                  newTab[j + oldCap] = hiHead;
                              }
                          }
                      }
                  }
              }
              return newTab;
          }
      

      扩容是重新计算容量,向HashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。当然Java里的数组是无法自动扩容的,方法是使用一个新的数组代替已有的容量小的数组。

      1. resize首先会检查当前容量,如果等于0那么表示初始化
      2. 如果大于0那么会检查当前容量是否达到最大容量阈值,如果是那么新阈值设置为Integer.MAX_VALUE
      3. 如果未达到最大容量阈值,那么会扩容为当前容量的两倍。
      4. 扩容后需要对原数组中的元素进行重hash,Java 1.8版本重hash时不会重新计算hash只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”
    • hash的实现

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

      HashMap的Hash算法本质上就是三步:取key的hashCode值、高位运算、取模运算

      1. 首先会获取key的hashCode值
      2. 然后hashCode值会跟hashCode值的高16为做异或运算
      3. 最后拿上两步获取的值对底层数组长度取模,取模操作使用的是h & (table.length -1)

      通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在数组table的length比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销。

    参考链接:

    https://blog.csdn.net/u013132758/article/details/89181005

    Java8系列之重新认识HashMap

    准备用HashMap存1w条数据,构造时传10000还会触发扩容吗

    面试必问的HashMap,你真的了解吗?

    TreeMap

    TreeMap实现SortedMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。如果使用排序的映射,建议使用TreeMap。在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常。底层是二叉树(红黑树)实现。containsKey、get, put、 remove等操作的时间复杂度为log(n)

    Hashtable

    很多的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable,并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段机制(jdk1.7是分段锁 jdk1.8采用CAS+synchronized实现)。当不需要线程安全的场合可以用HashMap替换,需要线程安全且高并发的场合可以用ConcurrentHashMap替换

    LinkedHashMap

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

    在内部定义了LinkedHashMapEntry,它继承自HashMap.Node,增加了before, after指针。

        static class LinkedHashMapEntry<K,V> extends HashMap.Node<K,V> {
            LinkedHashMapEntry<K,V> before, after;
            LinkedHashMapEntry(int hash, K key, V value, Node<K,V> next) {
                super(hash, key, value, next);
            }
        }
    

    LinkedHashMap是 LruCache核心。

    LinkedHashMap都有哪些小秘密?同时给LruCache 提个建议

    LinkedHashMap源码解析(JDK8)

    IdentityHashMap

    HashMap内对key比较时是使用hash和equals,IdentityHashMap则是使用= =实现的

        public boolean containsKey(Object key) {
            Object k = maskNull(key);
            Object[] tab = table;
            int len = tab.length;
            int i = hash(k, len);
            while (true) {
                Object item = tab[i]
                //k和数组中某个已存在key通过“= =”比较,判断是否存在
                if (item == k)
                    return true;
                if (item == null)
                    return false;
                i = nextKeyIndex(i, len);
            }
        }
    

    WeakHashMap

    如果其中key所引用对象没有被其他对象以强引用所引用那么key就会被回收。如果key所对应的对象被回收那么其表示的键值对会被删除。

    ConcurrentHashMap

    因为hashtable同步效率较低所以引入ConcurrentHashMap用于处理并发情况的 HashMap。大概的实现机制是分段,每段有自己的锁,这样就有多把锁可以在一定程度提高同步效率。感兴趣的可以自行去了解具体实现。

    https://www.jianshu.com/p/d0b37b927c48

    ArrayMap

    它的内部实现是基于两个数组。
    一个int[]数组,用于保存每个item的hashCode.
    一个Object[]数组,保存key/value键值对。容量是上一个数组的两倍。
    它可以避免在将数据插入Map中时额外的空间消耗(对比HashMap)。
    而且它扩容的更合适,扩容时只需要数组拷贝工作,不需要重建哈希表。
    和HashMap相比,它不仅有扩容功能,在删除时,如果集合剩余元素少于一定阈值,还有收缩(shrunk)功能。减少空间占用。

    深度解读ArrayMap优势与缺陷

    SparseArray

    SparseArray是Android提供的key为int类型的map,底层使用数组实现。其内部有两个数据int[] mKeys和Object[] mValues。

    mKeys和mValues通过如下方式对应起来:

    • 假设要向SparseArray存入key为 10,value为200的键值对,则先将10存到mKeys中,假设 10 在mKeys中对应的索引值是index ,则将value存入 mValues[index]中
    • mKeys中的元素值按照递增的形式存放,每次存放新的键值对时都通过二分查找方法来对mKeys进行排序

    SparseArray避免了基本数据类型的拆装箱操作,且在数据量不大的情况下查找效率高(内部使用二分查找),还有就是其移除数据时仅做数据标记并不实际删除,实际删除操作在垃圾回收时才进行。

    参考链接

    三、小结

    至此Java集合相关类基本分析完毕
    这里贴一个在网上看到的简单规律总结:
    ArrayXxx:底层数据结构是数组,查询快,增删慢
    LinkedXxx:底层数据结构是链表,查询慢,增删快
    HashXxx:底层数据结构是哈希表。依赖两个方法:hashCode()和equals()
    TreeXxx:底层数据结构是二叉树。两种方式排序:自然排序和比较器排序

    参考:

    面试中的HashMap、ConcurrentHashMap和Hashtable

  • 相关阅读:
    编码的使用,编码与解码
    小数据池,is和==的区别,id()
    python的内置函数
    python基本数据类型
    组合模式学习
    访问者模式
    C++ primer Plus回顾
    命令模式学习
    策略模式学习(设计模式)
    python_xlrd模块学习
  • 原文地址:https://www.cnblogs.com/Robin132929/p/13701528.html
Copyright © 2020-2023  润新知