Collection和Map实现类的部分源码解析

目录

一、整体关系

二、Collection

List

1.ArrayList

效果展示①

效果展示②

源码解析①

源码解析②

2.Vector

效果展示①

效果展示②

源码解析

3.LinkedList

源码解析

Set

1.HashSet

源码解析

①resize()

②treeifyBin()

2.TreeSet

源码解析

3.LinkedHashSet

三、Map

1.HashMap

2.TreeMap

3.HashTable

4.Properties

总结


一、整体关系

二、Collection

List

1.ArrayList

(1)可以加入多个null值

 

(2)ArrayList底层维护了一个Object类型的可变数组,有transient关键字,表示该数组不会被序列化

其扩容机制为:

①当创建ArrayList对象时,如果使用的是无参构造器,则初始elementData容量为0,第1 次添加,则扩容elementData为10, 如需要再次扩容,则扩容elementData为1.5倍。

②如果使用的是指定大小的构造器,则初始elementData容量为指定大小,如果需要扩容,则直接扩容elementData为1.5倍。

效果展示①

从debug中可以知道arrayList无参构造器创建后是为空的。

 

 在加入一个元素之后数组扩容为大小为10的数组

在超过10容量时,数组会进行1.5倍扩容,成为大小为15的数组,如果再超过15,那么就是扩容为

大小为22的数组,再是33,49,73.....

 

效果展示②

这里可以看到,如果给出初始容量时,数组会初始化为该容量大小

 

当添加第6个数的时候,数组容量大小不够,会进行1.5倍扩容

容量5扩容后容量为7,当超过7之后,还是按照1.5倍扩容

 

源码解析①

从这里可以看出调用的无参构造器是会创建一个空数组

当加入第一个元素时,因为数组为空,会调用下面的方法来确定扩容大小(这里并没有对数组进行真正的扩容,只是确定了在数组中放入元素需要的最小容量,理论上放入一个元素只需要1个大小的容量,但是java默认空数组直接扩容为10)

因为前面传入的参数minCapacity是为1的(数组大小 + 1),所以返回较大的默认大小10

这个方法里面的grow方法才是进行真正的扩容(modCount只是用来统计修改次数的)

private void grow(int minCapacity) {
        int oldCapacity = elementData.length;   //原数组的容量大小,不是元素个数

        //(oldCapacity >> 1)是向右位移1位,简单来说就是除以2,
        //所以等价于 oldCapacity + (oldCapacity / 2) 约等于  1.5倍的oldCapacity
        //如果是空数组,则newCapacity还是0
        int newCapacity = oldCapacity + (oldCapacity >> 1);

        //如果1.5倍原数组大小比所需最小容量小,则数组直接扩容成所需最小容量
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        //如果新数组容量超过一定大小,则换用更大的扩容方法
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        //创建一个新数组,同时将原先的数据复制进去
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

最后把要加入的数据加入进去 

当加入第二个数据时,因为之前已经扩容为10,则会跳过grow方法,直接加入数据

源码解析②

如果使用的是指定大小的构造器,则初始elementData容量为指定大小,如果需要扩容,则直接扩容elementData为1.5倍。

当给定一个参数后,会调用下图有参构造器

后面的与源码解析①差不多一样,不过还有一个顺便说一下,就是当给定容量为1时,如果一直按照1.5倍扩容,那么则会一直保持为1,所以grow方法中的有if语句防止了这种情况。 

2.Vector

Vector和ArrayList基本上差不多,不过ArrayList适用于单线程,Vector是线程同步的,适用于多线程,还有两者扩容机制也有些不同,具体的比较如下图: 

(取自韩顺平的java视频)

(1)vector底层也是一个Object数组,不过它有protected关键字,而ArrayList的底组的数组关键字为trasnient。

①如果是无参,默认10,满后,按2倍扩容

②如果指定大小,则每次直接按2倍扩容

效果展示①

初始化

扩容

效果展示②

初始化

 

扩容

 

源码解析

注意:vector创建之后就会有一个容量为10的数组,但是ArrayList刚创建时,数组容量为0的,只有当加入元素时,检测到数组容量不够,才会进行扩容,扩容大小也为10。

当超过当前容量时,会扩容为原来容量的两倍

 private void grow(int minCapacity) {
        int oldCapacity = elementData.length;
        //当capacityIncrement > 0 时,那么按照capacityIncrement的值来扩容
        //否则,就再加上一个oldCapacity扩容
        //capacityIncrement默认为0
        int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                         capacityIncrement : oldCapacity);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

这里capacityIncrement是在调用下面有参构造器时会被赋值的,默认为0,当程序员自己定义这个值时,相当于按照这个capacityIncrement进行扩容,不会默认为两倍扩容。

 

3.LinkedList

LinkedList底层不同于ArrayList和Vector,底层维护一个双向链表进行数据存储的。

数组可以实现随机存取,而链表需要一个个遍历查找,所以数组改查效率更高

但数组进行增删时,要把原来元素进行增删后,再创建一个数组重新放入,而链表可以通过改变指向来实现一个元素的增删,所以链表的增删效率更高。

(取自韩顺平的java视频)

源码解析

增加结点

void linkLast(E e) {
        final Node<E> l = last;  //相当于一个临时变量,接收last指向的结点node1
        
        //因为要在链表后面再加入一个元素,那么原先链表的最后一个结点node1成为倒数第二个结点
        //新加入的结点node2成为最后一个结点
        //所以node2会指向node1,所以把l当做其前驱
        final Node<E> newNode = new Node<>(l, e, null);  
        
        //last总会指向最后一个结点
        //所以last指向新节点
        last = newNode;
        
        //如果l为空,那么这个新加入的结点为第一个结点
        //first总是指向第一个结点,所以会指向这个新结点
        if (l == null)
            first = newNode;
        else
        //前面node2已经指向node1,node也要指向node2
            l.next = newNode;
        size++;
        modCount++;
    }

删除结点

默认删除第一个结点 

private E unlinkFirst(Node<E> f) {
        final E element = f.item;
        final Node<E> next = f.next;
        
        //把f结点设为空,这样GC机制会将这个结点的空间回收
        f.item = null;
        f.next = null; 
        first = next;
        
        //如果next为空,说明把f结点已经是最后一个结点
        //删除f结点整个双向链表都没有结点了
        //所以把last置为空
        if (next == null)
            last = null;
        else
            //f结点删除后,f的下一个结点就是第一个结点
            next.prev = null;
        size--;
        modCount++;
        return element;
    }

Set

1.HashSet

维护了一个哈希表,即数组+链表+红黑树组成,且无序。

hashSet的数据存放模型如下图(没把红黑树包括进去):

(1)HashSet底层维护的是HashMap

(2)可以存放null值,但只能存放一个,数据存放不保证顺序

 

源码解析

添加数据

这里的PRESENT是一个Object对象,因为HashSet实际上是一个HashMap,所以加入的值(java)作为key,value就用一个Object对象代替,感觉无多少实际意义。

计算出加入的值(java)的hash值,这里的(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;
        //如果为空,进行扩容
        //扩容的数组初始大小为16
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //如果这个位置没有结点,则直接放入
        //如果有节点,此时p已经被赋值为第一个结点
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            //不同键值算出来的hash值可能相同
            //放入的结点与p的hash值是否相同,
            //如果相同,则再比较键值,键值的地址相同或者键值的类的equals方法比较相同
            //则认定为加入的数据与p相同,那么就无法加入
            if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                //将该位置第一个结点的值赋给e
                e = p;
            //如果p为TreeNode结点,那么是按照红黑树中插入结点的方法进行数据的插入
            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);
                        //TREEIFY_THRESHOLD的值为8,当链表中已经有8个结点了
                        //当要加入第9个结点时,会执行treeifyBin()方法
                        //注意这里并不一定会进行树化,要看数组的大小,具体的要看treefiBin()
                        //红黑树的搜索、添加,删除的效率为O(logn)
                        //链表的搜索、添加,删除的效率为O(n)
                        if (binCount >= TREEIFY_THRESHOLD - 1) 
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //如果要加入的数据不能加入,则e不会为空,则返回的值不为空
            //在add方法中会进行返回值是否为空的判断,
            //如果为空,add方法返回true,否则返回false
            if (e != null) {
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        //如果结点个数大于阈值(阈值默认为为数组大小的0.75)
        //如果数组大小为32,阈值为24,当加入第25个结点时
        //即使所有结点都在一个数组位置上,也会进行扩容操作
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
    

①resize()

对于为null的数组变量,会初始化为16,当超过阈值需要扩容时,会增大到原来的2倍同时会将数据重新放置。(重新放置的代码过长,自行查看)

②treeifyBin()

final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        //要对当前数组的大小进行判断,如果小于64,那么不会进行树化
        //只会对数据进行2倍的扩容
        //也就是树化的条件有两个
        //1.数组的大小需要大于64
        //2.某一个位置的结点数超过8个
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        //这里也不会进行树化,只是将这个位置的链表变成双向链表
        //因为把链表变成红黑树之后,根结点不一定在第一个
        //所以双向链表为后面方便把红黑树的根结点移到链表的第一个
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                //真正的进行树化
                hd.treeify(tab);
        }
    }

treeify()方法就是判断该结点该往左,往右,还是当做根结点,然后最后再做一次平衡

红黑树概念(转载自 小七mod)

treefiy方法的详解(转载自 老艮头)

2.TreeSet

底层基于红黑树实现,且有序。

(1)TreeSet底层维护的是TreeMap

(2)可以给TreeSet设置比较器

注意点:compareTo实现Comparable接口,compare实现Comparator接口,

Comparable位于java.util包下,Comparator位于java.lang包下,

Comparable是内比较器,一般在构建类时,就实现了Comparable

Comparator是外比较器,根据当时的需求再进行构造

Comparator的优先级较高

Comparator和Comparable的区别(转载自 想飞的yu)

底层把匿名内部类比较器传给TreeMap对象的comparator属性

由下图可知当根据comparator的compare方法进行是否相同的判断时,如果判断两个值是相同的,则不会加入相同的数据 

源码解析

 Comparator<? super K> cpr = comparator;
            if (cpr != null) {
                do {
                    parent = t;
                    //调用自己创建的比较器
                    cmp = cpr.compare(key, t.key);
                    //为什么当 第一个参数-第二个参数 < 0 时按从小到大
                    //因为当小于0时是往左子树那边插入
                    //而数据按红黑树(特殊的二叉搜索树)的中序遍历输出
                    //所以会从小到大输出,以上仅个人猜想
                    if (cmp < 0)
                        t = t.left;
                    else if (cmp > 0)
                        t = t.right;
                    //如果相等,则返回0,则插入不成功
                    //只是覆盖了这个key的值
                    //因为TreeSet中value=PRESENT,相当于没有插入
                    else
                        return t.setValue(value);
                } while (t != null);

 所以如果改变一下比较器,按照长度进行排列的话,会出现“wzy”加入不进去这种情况:

3.LinkedHashSet

底层维护的是数组+双向链表,初始容量与HashMap相同,为16

整体形状就是原来的HashSet加上了双向指针,构成双向链表

(1)LinkedHashSet底层维护的是LinkedHashMap(HashMap的一个子类),多了before和after属性来创建双向链表

(2)可以按添加的顺序进行存储

(3)数组是HashMap$Node[ ] ,存放的数据是 LinkedHashMap$Entry类型,

LinkedHashMap$Entry是HashMap$Node的子类

(4)添加数据

实际还是调用HashMap的add()方法把数据加入进去

三、Map

1.HashMap

因为前面HashSet底层是HashMap,这里就不再过多赘述

2.TreeMap

因为前面TreeSet底层是TreeMap,这里就不再过多赘述

3.HashTable

同HashMap差不多,不过它是线程安全的。

(1)键和值都不能为null,否则会抛出NullPointerException

(2)底层有数组Hashtable$Entry[ ] ,初始化大小为11,加载因子还是0.75

(3)扩容是  原先的容量 * 2 + 1

4.Properties

继承了HashTable类,同HashTbale类似,也是用键值对保存数据,可以用于从xxx.properties文件中,加载数据到Properties类对象中


总结

用于记录学习过程

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值