集合底层源码list、map、set


List

ArrayList

  • list可以存储空值。
  • arraylist是线程不安全的,而且基本等同于vector,在多线程的情况下,一般不建议使用arraylist,可以考虑一下使用vector
  • 每次向list添加元素时,都会进行扩容判断,看看是否需要扩容。
  • arraylist底层维护了一个Object的数组elementData
  • 使用无参构造器实例化时,list的初始容量为10,当容量不够时,每次扩容以当前容量的1.5被进行扩容
  • 使用有参构造器时,list的初始容量为传递进去的值,当容量不够时,每次扩容以当前容量的1.5被进行扩容
//list底层实际上维护了一个 elementData[] 的数组
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  //判断是否需要对数组扩容
    elementData[size++] = e; //添加元素
    return true;
}
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) { //该方法使用于计算容量
   if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { //判断是否是第一次添加元素
   		//DEFAULT_CAPACITY默认容量是10个,minCapacity默认是1,第一次添加元素会将数组容量初始化为10
       return Math.max(DEFAULT_CAPACITY, minCapacity); 
   }
   return minCapacity; //如果不是第一次添加,则返回数组至少所需minCapacity容量
}
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // overflow-conscious code
    //判断当前的数组的长度是否满足minCapacity最小容量,minCapacity其实就是已经加入到数组中的元素的个数+1
    //第一次添加元素minCapacity=10,elementData.length=0,一定会进行扩容
    if (minCapacity - elementData.length > 0) 
        grow(minCapacity); //如果数组容量不够,则需进行扩容
}	
private void grow(int minCapacity) {//已经加入到数组中的元素的个数+1,第一次添加元素是minCapacity=10
    // overflow-conscious code
    //获取当前数组长度,第一次长度=0
    int oldCapacity = elementData.length;  
    //扩容获取新的容量,扩容的大小就是当前数组长度的1.5倍
    //第一次添加元素的长度=0,因此newCapacity=0*1.5=0
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    //如果扩容后的容量还是没有比旧容量大,则证明是第一次添加元素,因此默认将数组扩容10
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity; //
    //如果扩容后的容量比最大容量大,则需进一步处理
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    //为保证数组扩容后不丢失数据,使用copyOf方法在以添加的数据后面追加容量
    elementData = Arrays.copyOf(elementData, newCapacity);
}

Vector

  • vector是线程安全的
  • vector底层维护了一个Object的数组elementData
  • vector如果没有设置初始容量,那么每次扩容都是添加10的容量,如果设置了初始容量x,那么每次都是扩容x

LinkList

  • 将新的结点加入到双向链表的最后
  • LinkList底层是一个双向链表,first指向链表的开头,last指向链表的结尾
  • remove方法默认删除第一个节点
  • remove使用根据对象删除时,底层使用equals判断进行删除,因此,只要内容相同就能够进行删除
public boolean add(E e) {
    linkLast(e); //将节点挂到最后一个结点的后面
    return true;
}
void linkLast(E e) {
    final Node<E> l = last;//获取最后一个节点
    final Node<E> newNode = new Node<>(l, e, null); //建立新的结点
    last = newNode; //将last指向当前结点
    if (l == null) //如果是第一个结点
        first = newNode;//将first指向当前结点
    else
        l.next = newNode;//最后一个结点的next指向当前结点
    size++;
    modCount++;
}

List的选择

  • 如果是改查的操作比较多,使用ArrayList
  • 如果是删除插入的操作多,使用LinkList
  • 如果是需要考虑线程安全,使用Vector

Map

说明

  • Map与Collection并列存在。用于保存具有映射关系的数据:Key-Value(双列元素)
  • Map 中的 key 和 value 可以是任何引用类型的数据,会封装到HashMap$Node 对象中
  • Map 中的 value 可以重复
  • 常用String类作为Map的 key
  • key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到对应的 value

HashMap

  • hashmap线程是不安全的
  • hashmap的key 可以为 null, value 也可以为null ,注意 key 为null
  • hashmap中的 key 不允许重复,原因和HashSet 一样
  • k-v 最后是 HashMap$Node node = newNode(hash, key, value, null)
  • k-v 为了方便程序员的遍历,还会 创建 EntrySet 集合 ,该集合存放的元素的类型 Entry, 而一个Entry对象就有k,v EntrySet<Entry<K,V>> 即: transient Set<Map.Entry<K,V>> entrySet
  • entrySet 中, 定义的类型是 Map.Entry ,但是实际上存放的还是 HashMap$Node,这是因为 static class Node<K,V> implements Map.Entry<K,V>,向上转型
  • 当把 HashMap$Node 对象 存放到 entrySet 就方便我们的遍历, 因为 Map.Entry 提供了重要方法 K getKey(); V getValue();
  • 由于hashset的底层是hashmap,因此,源码分析放在hashset

HashTable

  • 底层有数组 Hashtable$Entry[] 初始化大小为 11
  • 临界值 threshold 8 = 11 * 0.75
  • 扩容: 按照自己的扩容机制来进行即可,每次扩容就是当前容量的2倍+1
  • 执行 方法 addEntry(hash, key, value, index),添加K-V 封装到Entry
  • 当 if (count >= threshold) 满足时,就进行扩容
  • 按照 int newCapacity = (oldCapacity << 1) + 1; 的大小扩容.
  • hashtable的键和值都不能为空,否则会抛出空指针异常
  • hashtable的使用方法基本和hashmap一样
  • hashtable是线程安全的
public Hashtable() {
    this(11, 0.75f); //初始化容量,默认11
}
public synchronized V put(K key, V value) {
    // Make sure the value is not null
    if (value == null) { //value值不能为空
        throw new NullPointerException();
    }

    // Makes sure the key is not already in the hashtable.
    Entry<?,?> tab[] = table; //辅助变量
    int hash = key.hashCode(); //获得key的hash值
    int index = (hash & 0x7FFFFFFF) % tab.length; //计算索引
    @SuppressWarnings("unchecked")
    Entry<K,V> entry = (Entry<K,V>)tab[index]; 
    for(; entry != null ; entry = entry.next) {
        if ((entry.hash == hash) && entry.key.equals(key)) { //判断是否有重复的key值
            V old = entry.value; //更新当前key对应的value
            entry.value = value;
            return old;
        }
    }

    addEntry(hash, key, value, index);//如果当前的key唯一,则添加到entry数组中
    return null;
}
private void addEntry(int hash, K key, V value, int index) {
    modCount++;
    Entry<?,?> tab[] = table; //辅助变量
    if (count >= threshold) { //如果当前的数量已经到达threshold临界值
        // Rehash the table if the threshold is exceeded
        rehash();//扩容

        tab = table;//复制数组
        hash = key.hashCode(); //获取hashcode
        index = (hash & 0x7FFFFFFF) % tab.length; //计算索引
    }

    // Creates the new entry.
    @SuppressWarnings("unchecked")
    Entry<K,V> e = (Entry<K,V>) tab[index];
    tab[index] = new Entry<>(hash, key, value, e);//建立新结点并插入
    count++;
}

Set

HashSet

  • hashset的底层实际上是一个hashmap,hashset的hash值并不是key对应的hashcode
  • hashmap的底层就是维护了一个数组链表table,也就是hash表
  • 添加一个元素时,先得到hashcode,再通过计算得到索引值
  • 找到存储的数据表table,看这个索引位置是否已经存放的有元素,如果没有,直接加入,如果有,调用equals比较,如果相同,就放弃添加,如果不相同,则添加到最后
  • 在Java8中,如果一条链表的元素个数到达8,并且table数组的大小>=64,就会进行树化(红黑树)
  • hashset的扩容并不是达到当前容量的峰值才进行扩容,默认table有16个,当使用达到容量的0.75,也就是16*0.75=12,就会进行扩容,每次扩容到当前容量的两倍,另外,当任意一条链表达到8时,如果table的大小未达到64,也会进行扩容
  • hashset加入后的数据是没有顺序的,因为数据达到一定量的时候会进行树化
  • hashset在树化之前其实维护了一个单链表数组table
public boolean add(E e) {
	//transient关键字的作用是:被transient修饰的变量不参与序列化和反序列化。
	//当一个对象被序列化的时候,transient型变量的值不包括在序列化的表示中,然而非transient型的变量是被包括进去的。
	private transient HashMap<E,Object> map;
 	return map.put(e, PRESENT)==null; //使用set.add(e),实际上调用了hashmap的put方法
}
public V put(K key, V value) { //等同与hashmap的put方法
	//这里的hash(key)是计算key对应的hash值,key就是我们传递的参数(set.add(key)),value是默认值,只有hashmap才会用到
     return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                boolean evict) {
     Node<K,V>[] tab; Node<K,V> p; int n, i;
     if ((tab = table) == null || (n = tab.length) == 0) //首先判断table数组是否为空
         n = (tab = resize()).length; //如果数组为空,使用resize方法进行扩容,初始的容量是16个数组,每次扩容以两倍扩容
     //i = (n - 1) & hash该公式是根据当前key的hash值计算出所在table的索引,并判断该索引所在的链表是否为空
     if ((p = tab[i = (n - 1) & hash]) == null) 
         tab[i] = newNode(hash, key, value, null); //假设为空,则创建新的结点,并作为索引 i 下的第一个结点,也就是头节点
     else { //如果索引下的链表不为空,则需判断是否该索引中是否已经有key
         Node<K,V> e; K k;
         //满足key值相等的条件:
         //1. hash必须相等
         //2. 链表中有跟key一样的对象,也就是两个对象完全相同 (k = p.key) == key
         //3. 链表中有一个对象跟key不相同,但是对象的内容跟key相同 key != null && key.equals(k)
         if (p.hash == hash &&
             ((k = p.key) == key || (key != null && key.equals(k))))  //这里只是判断索引所在链表p的第一个节点是否与key相同
             e = p; //相同的话,记录这个p,不对key做任何处理
         //如果p不是链表,是一颗红黑树,则在红黑树中寻找与key相等的元素
         //注意:在Java8中,如果有一条链表的元素个数到达8,并且table数组的大小>=64,就会进行树化(红黑树)
         else if (p instanceof TreeNode) 
、             e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
         else {
             for (int binCount = 0; ; ++binCount) { //如果p是链表,并且第一个结点不等于key,往后继续查找
                 if ((e = p.next) == null) { //这里说明在整个链表中找不到与key相等的结点
                     p.next = newNode(hash, key, value, null); //将key直接插入到p链表的后面
                     if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st //如果插入后p的结点数>=8,则进行树化
                         treeifyBin(tab, hash); //树化,具体算法参考红黑树
                     break;
                 }
                 if (e.hash == hash && //找到与key相等的结点
                     ((k = e.key) == key || (key != null && key.equals(k))))
                     break;
                 p = e; //记录这个结点
             }
         }
         if (e != null) { // 在table数组中存在与key相等的值
             V oldValue = e.value; //记录key对应的value,由于这里是hashset,我们只是传key,value是默认值
             if (!onlyIfAbsent || oldValue == null) 
             	//更新key对应的value,这里的操作只能在map键值对才能看出效果,hashset所有的value都是相等的
                 e.value = value; 
             afterNodeAccess(e);
             return oldValue;
         }
     }
     ++modCount;
     //这里并不是table数组中被占领满了才会扩容,而是只要添加的数据超过了threshold临界值,table数组就会进行扩容
     //size表示已经加入到table数组中的元素,也就是hashset已经包含的元素
     if (++size > threshold)
         resize();
     //该方法主要给hashset的子类重写用,便于子类的拓展,这里的afterNodeInsertion是一个空方法
     afterNodeInsertion(evict);
     return null;
}
public static void test1() {
    HashSet<Object> set = new HashSet<>();
    set.add("abc");
    set.add("abc");
    set.add(new Node(1, 1));
    set.add(new Node(1, 1));
    set.add(new String("bcd"));
    set.add(new String("bcd"));
    System.out.println(set);//[bcd, abc, Node{x=1, y=1}, Node{x=1, y=1}]
}
class Node {
    int x;
    int y;
    public Node(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

LinkedHashSet

  • LinkedHashSet 加入顺序和取出元素/数据的顺序一致
  • LinkedHashSet 底层维护的是一个LinkedHashMap(是HashMap的子类)
  • LinkedHashSet 底层结构 (数组table+双向链表)
  • 添加第一次时,直接将 数组table 扩容到 16 ,存放的结点类型是 LinkedHashMap$Entry
  • 数组是 HashMap$Node[] 存放的元素/数据是 LinkedHashMap $ Entry类型
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值