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

posted @ 2020-09-19 22:27  潇~萧下  阅读(110)  评论(0编辑  收藏  举报