zoukankan      html  css  js  c++  java
  • java容器集合List、Map源码分析

    Map接口源码分析

    HashMap底层实现

    • JDK7中,HashMap使用数组+链表的结构,JDK8中,HashMap使用数组+链表+红黑树的结构。

    • HashMap添加数据的过程梳理,从JDK7为例梳理过程,并分析JDK8JDK7之间的区别:

      • JDK7为例,HashMap首先初始化,HashMap map = new HashMap();创建了一个长度为16的一维数组Entry[] table,之后使用map.put(key, value)不断向其添加数据

        1. 以某次执行map.put(key1, value1)为例:

          //计算key1的hash值,得到在数组的存放位置,若该位置为空,添加成功。
          int hash = hash(key1);
          
        2. 若该位置上有值,以链表形式存在,将hash(key1)循环与该位置存在的hash比较,

          //循环比较hash值,若有相同的,继续比较equals方法,若完全相同,则替换。
          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;
              }
          }
          
        3. 若循环比较时,与该链表中存在的所有数据的hash值均不相同,或者hash值相同但equals()方法不同,添加成功

          //添加成功
          addEntry(hash, key, value, i);
          
        4. 扩容问题,当超出临界值(且要存放的位置非空)时,扩容为原来容量的2倍,并将原有的数据复制过来。

      • JDK8JDK7区别:

        1. HashMap map = new HashMap();底层创建的数组为Node[],而非Entry[],默认为{}。在首次调用map.put(key1, value1)时,创建长度为16的数组。
        2. JDK7底层结构:数组+链表JDK8中底层结构:数组+链表+红黑树
        3. 当数组的某一个索引位置上的元素以链表形式存在的数据个数 > =8 且当前数组的长度 > =64时,此时此索引位置上的数据改为使用红黑树存储。

    HashMap源码分析

    • JDK8中的HashMap源码分析:
    //默认的装载因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    //底层数组
    transient Node<K,V>[] table;
    //默认构造方法
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // 0.75f
    }
    //调用put()方法
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    //传入key算出hash值
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    //putVal方法
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
      	//JDK8底层的数组为:Node<K,V>[] tab,   JDK7底层的数字为Entry[] table
        Node<K,V>[] tab; Node<K,V> p; int n, i;
      	//若数组为空 初始化
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
      	//该索引位置无元素,直接添加进去
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
      	//有元素 链表或者红黑树
        else {
            Node<K,V> e; K k;
          	//开始比较,如果跟第一个元素相同,赋值用于后面修改
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
          	//是树节点
            else if (p instanceof TreeNode)
              	//使用putTreeVal()
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
              	//循环遍历看是否相等  binCount 是链表中元素个数
                for (int binCount = 0; ; ++binCount) {
                  	//比较到最后发现没有相同的,直接在链表结尾添加进去元素
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                      	//新插入节点后链表长度大于等于8,判断需要使用红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                          	//数组长度<64时,直接扩容,>=64时,使用红黑树
                            treeifyBin(tab, hash);
                        break;
                    }
                  	//如果存在相等的元素 跳出循环
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
          	//如果有对应key的元素
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                  	//替换旧值为传入的新值
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
      	//没有找到元素  修改次数+1
        ++modCount;
      	//元素数量加1,判断是否需要扩容
        if (++size > threshold)
          	// 扩容
            resize();
        afterNodeInsertion(evict);
        return null;
    }
    
    • 扩容问题:扩容为原来两倍,并复制元素到新的数组中。
    // HashMap的默认容量,16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 
    //HashMap的默认加载因子:0.75
    static final float DEFAULT_LOAD_FACTOR = 0.75f;  
    //扩容的临界值,=容量*装载因子:16 * 0.75 => 12
    int threshold; 
    //Bucket中链表长度大于等于该默认值,转化为红黑树:8
    static final int TREEIFY_THRESHOLD = 8;
    //桶中的Node被树化时最小的hash表容量:64
    static final int MIN_TREEIFY_CAPACITY = 64;
    

    LinkedHashMap源码分析

    • LinkedHashMapHashMap多一个双向链表,可保证元素按插入的顺序访问。简单理解为LinkedHashMap=LinkedList + HashMap
    //多了before, after用来记录顺序
    static class Entry<K,V> extends HashMap.Node<K,V> {
        Entry<K,V> before, after;
        Entry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }
    

    注意

    • JDK7HashMap扩容时,多线程情况下复制数据时会出现环形链表的情况

      void transfer(Entry[] newTable, boolean rehash) {
         int newCapacity = newTable.length;
       	 //循环数组的每一个索引的数据
         for (Entry<K,V> e : table) {
           	//循环单个索引下的链表中的数据
             while(null != e) {
               	//next是一个局部变量
                 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;
             }
          }
      }
      

    Set接口源码分析

    HashSet底层实现

    • 两个重要的属性:

      //底层是hashMap
      private transient HashMap<E,Object> map;
      //所有的key指向的value  放到map中
      private static final Object PRESENT = new Object();
      
    • HashSet内部使用的是HashMap,存储的key无序不可重复。

      public HashSet() {
          map = new HashMap<>();
      }
      
    • 当向HashSet中添加数据时,调用的是map.put()方法,

      //添加key调用的是map的put方法
      public boolean add(E e) {
          return map.put(e, PRESENT)==null;
      }
      
    • HashSet的初始容量,初始化时指定容量是为了减少扩容的次数,提高效率。初始容量应设置为(int) (c.size()/.75f) + 1

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

    LinkedHashSet底层实现

    • LinkedHashSetHashSet的子类,底层使用的是LinkedHashMap,所以它可以按照插入的顺序进行排序。

      //构造函数使用父类构造函数
      HashSet(int initialCapacity, float loadFactor, boolean dummy) {
          map = new LinkedHashMap<>(initialCapacity, loadFactor);
      }
      

    List接口源码分析

    ArrayList底层实现

    • ArrayList的构造方法,底层Object[] elementData初始化为{}。

      //实例化后未添加元素时,空数组
      private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
      //底层数组
      transient Object[] elementData
      //构造方法
      public ArrayList() {
          this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
      }
      
    • 使用add()方法添加数据,如果是空数组,初始化为10,每次扩容为原来的1.5倍,并复制元素到扩容后的数组上。

      public boolean add(E e) {
      		//添加数据前检查是否需要扩容  空数组默认初始化为10
          ensureCapacityInternal(size + 1);  // Increments modCount!!
        	//添加数据到最后
          elementData[size++] = e;
          return true;
      }
      

    LinkedList底层实现

    • 定义双链表结构

       private static class Node<E> {
          E item;
          Node<E> next;
          Node<E> prev;
      
          Node(Node<E> prev, E element, Node<E> next) {
              this.item = element;
              this.next = next;
              this.prev = prev;
          }
      }
      

    Vector底层实现

    • 线程安全,每次扩容为原来的两倍。

    总结

    • 将集合中常见的实现类的源码粗略的做了分析,主要为理解这些接口的实现过程打下基础,源码中具体的细节问题有时间后详细整理,对于理解源码中的大致流程及重点,这篇文章就够了。

    原创不易,欢迎转载,转载时请注明出处,谢谢!
    作者:潇~萧下
    原文链接:https://www.cnblogs.com/manongxiao/p/13697961.html

  • 相关阅读:
    从零入门 Serverless | Knative 带来的极致 Serverless 体验
    SpringCloud 应用在 Kubernetes 上的最佳实践 — 高可用(熔断)
    阿里巴巴成立云原生技术委员会,云原生升级为阿里技术新战略
    解构云原生,从概念到落地:阿里云、声网、微博、好未来、CNCF 的专家们怎么看?
    人工智能训练云燧T10
    云计算应用场景分析
    昇腾全栈解决方案
    自动驾驶解决方案架构
    自动驾驶开发云平台业务分析
    ResNet-50模型图像分类示例
  • 原文地址:https://www.cnblogs.com/manongxiao/p/13697961.html
Copyright © 2011-2022 走看看