Java集合类

Java集合类

1. 前言

Java集合类是学习Java时一个很重要的知识点,同时也是面试时考核的重点。另外在实际工作中,如果不是很理解各个集合类的特点,很容易陷入到只会机械使用ArrayList,HashMap等集合类的怪圈中。所以通过debug源码的方式,来学习这些类之间的相互关系,以及他们各自的特点。

2. 类关系

在学习他们之前,需要先了解这些常用类之间的相互关系。

2.1 单列集合类

1)首先单列集合类的祖先接口是Iterable,这个接口类为所有单列集合类提供了遍历的方法,这个后面再说。
2)接下来是Collection,他继承了Iterable接口,规定了一些通用的方法,如size(),add(),remove(),clear()等方法。
3)之后又有两个接口继承了Collction接口,一个是List接口(有序可重复),一个是Set接口(无序不可重复)。
4)常用的实现List接口的集合类有三个,Vector(线程安全),LinkedList(双链表结构),ArrayList(数组结构)。
常用的实现Set接口的集合类也有三个,HashSet(底层是HashMap),它的子类LinkedHashSet(底层是LinkedHashMap);TreeSet(底层是TreeMap)。
在这里插入图片描述

2.2 双列集合类

1)祖先接口是Map,与Collection一样,规定了所有双列集合类要实现的基本方法,如put(),get(),remove()等常用方法。
2)常用的实现Map接口的集合类有四个,一个是Hashtable(线程安全),HashMap(jdk8为数组+链表+红黑树,jdk7为数组+链表,考虑到jdk已经发展到15,所以7就不做分析了),以及他们分别的子类Properties(常用于解析配置文件.properties),LinkedHashMap(在HashMap的基础上,实现了双链表)。
在这里插入图片描述

3. List集合

3.1 ArrayList

1.ArrayList底层维护的是一个数组,我将利用add方法,来展示它的底层结构以及相应代码。
在开始之前,将用到的属性先标注一下。在这里插入图片描述
2.用下面的代码进行测试

public class ListMethod {
    public static void main(String[] args) {
        ArrayList arrayList = new ArrayList();
        for(int i = 0; i < 100; i++){
            arrayList.add(i);
        }
    }
}

3.进入debug环境
1)先经过构造函数,elementData被赋空数组作为初始值。
在这里插入图片描述
2)进入到add方法,有三个步骤,ensureCapacityInternal()方法确认数组容量,之后添加数据到数组中,最后返回true。
在这里插入图片描述
3)进入ensureCapacityInternal()方法,内部调用ensureExplicitCapacity方法,再debug,进入calculateCapacity方法。
在这里插入图片描述
4)进入calculateCapacity方法后,通过判断默认数组大小与添加元素后数组大小,决定后面是否需要扩充底层数组。
在这里插入图片描述
5)回到ensureExplicitCapacity方法,通过上一步计算出需要的数组容量,来判断是否需要扩充elementData数组。
在这里插入图片描述
6)第一次肯定需要数组扩容,进入grow方法
在这里插入图片描述
7)ensureCapacityInternal方法结束,数组扩容完毕,将对象放置到数组中,完成一次add操作。
在这里插入图片描述
在这里插入图片描述
8)看第11次add操作,体会数组扩容
在这里插入图片描述
在这里插入图片描述
4.小结
1)ArrayList底层是利用数组保存存入的对象,在创建一个ArrayList对象的时候,并没有给数组赋初始大小,而是在第一次add操作时,才给数组赋初始大小(如果addAll批量添加的时候,需要的容量大于默认初始值10,那么直接使用最大值作为初始容量值)。
2)由于底层为数组,具备索引,所以ArrayList在查询操作中效率很高,因此若在使用中需要频繁查询,首选ArrayList。
3)没有synchronized关键字,所以若程序涉及到多线程问题,不建议使用。

3.2 LinkedList

1.LinkedList底层双链表结构,由自定义的内部类Node形成。我还是通过add方法来展现底层结构。
同样,先说下用到的属性和静态内部类Node
在这里插入图片描述
在这里插入图片描述
2.用下面的代码进行测试

public class LinkedListMethod{

    public static void main(String[] args) {
        LinkedList<Integer> list = new LinkedList<>();
        list.add(5);
        list.remove(new Integer(5));
    }
}

3.进入debug环境
1)进入空构造函数,可以看到LinkedList除创建一个对象外,没有做任何操作。
在这里插入图片描述
2)进入add方法,里面分成两步。第一步,由于调用的是无指定位置的add方法,因此默认将结点插入到链表末尾,即调用linklast方法;第二步,插入成功后返回true。
在这里插入图片描述
3)进入linklast方法,将新结点添加到链表末端。
在这里插入图片描述
4)至此,完成一次add操作。
在这里插入图片描述5)remove方法就比较简单,找到需要删除的结点x,将其前驱结点的next指针指向结点x的后驱结点,将x后驱结点指向x的前驱结点。解链完成后,该结点失去对象指针,由jvm的GC处理掉。
在这里插入图片描述
在这里插入图片描述
4.小结
与ArrayList不同,LinkedList底层是Node形成的双向链表,所以在批量增加的时候,并不会涉及到扩容的问题。
同样由于双向链表的结构,LinkedList在删除操作上比ArrayList的数组结构具有更高的效率,因此如果在更新操作较多的环境下,前者是更好的选择。

3.3 Vector

1.Vector底层是数组,与ArrayList在设计思路上基本类似,但由于方法上使用了synchronized关键字,实现了线程安全。
先说使用到的类属性,capacityIncrement这个参数的作用是在数组扩容的时候,可以由用户自定义扩容大小
在这里插入图片描述
2.用下面的代码进行测试

public class VectorMethod {
    public static void main(String[] args) {
        List vector = new Vector();
        for(int i = 0; i < 100; i++){
            vector.add(0);
        }
    }
}

3.进入debug环境
1)进入构造函数,这里要注意,Vector与ArrayList不同。前者在创建对象时,直接指定数组大小为10,同时capacityIncrement为0(没有自定义扩容基数,使用默认值0);而后者是在创建对象时,并没有给数组赋初始长度,是在第一次add操作时,才指定长度。
在这里插入图片描述
在这里插入图片描述
2)进入add方法,一共有四步。首先modCount自增,记录这是第几次操作该Vector对象;第二步判断数组是否需要扩容,与ArrayList的ensureCapacityInternal()方法作用相同;第三步将元素按顺序插入到数组里;第四步返回true。
在这里插入图片描述
3)进入ensureCapacityHelper()方法,因为数组长度为10,且只插入一个值,所以数组不需要扩容。
在这里插入图片描述
4)确认不需要扩容,将元素添加到数组中,返回true。一次add操作结束。
在这里插入图片描述
5)运行第11次,体会数组扩容。
在这里插入图片描述
6)进入grow方法,与ArrayList只能1.5倍扩容不同,Vector允许自定义扩容基数,若未指定基数,则按2倍扩容。
在这里插入图片描述
7)至此,add操作结束,底层数组完成扩容。
在这里插入图片描述
4.小结
Vector与ArrayList比较,需要保证线程安全的时候,可以考虑Vector。
Vector底层也是数组,但是默认扩容基数与ArrayList不同,并且可以根据实际情况自定义基数大小。

3.4 List小结

介绍了List的三种实现类,底层结构有不同,有数组,也有自定义的内部类。不过都能保证插入数据的顺序性,以及能够允许重复数据的插入。

4. Set集合

4.1 介绍

在学习Set集合三个实现类之前,需要提前说一下,HashSet的底层是HashMap,LinkedHashSet的底层是LinkedHashMap,TreeSet的底层是TreeMap,所以学Set其实就是学Map。

4.1 HashSet

1.HashSet底层是HashMap,还是先看下需要用到的类属性。
在这里插入图片描述
2.使用下面代码进行测试

public class HashSetMethod {
    public static void main(String[] args) {
        Set hashSet = new HashSet();
        for(int i = 0; i < 100; i++){
            hashSet.add(i);
        }
    }
}

3.进入debug源码
1)进入构造函数,内部创建一个空HashMap对象。
在这里插入图片描述
2)进入add方法,调用map.put()方法。注意这个时候map的table数组为null。
在这里插入图片描述
3)内部调用putVal方法。
在这里插入图片描述
4)进入hash方法。使用高低位异或解决hash碰撞的原因,我在网上查了一下:
1.当数组长度很短时,只有低位的hash值参与运算(这个地方是在HashMap源码第630行,属于putVal()方法),因此为了减少hash碰撞结果,让高16位hash值参与运算。
2.采用异或运算而不采用& ,| 运算的原因是 异或运算能更好的保留各部分的特征,如果采用&运算计算出来的值会向1靠拢,采用|运算计算出来的值会向0靠拢。

详细的可以看下面这个链接

https://blog.csdn.net/weixin_43842753/article/details/105927912

在这里插入图片描述
5)进入putVal方法,完成数据插入。里面涉及到两个方法,resize()完成链表扩容,treeifyBin()方法完成链表树化。

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        /**
         * 创建临时数组tab,Node结点p
         */
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        /**
         * 先将底层table赋给tab,判断是否为空
         * 如果为空,说明table还没有初始化,进入resize()方法
         */
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        /**
         *  数组长度与hash值&运算,确定元素将放到的数组索引位置,判断数组当前索引位置是否已经存在元素
         *  若没有值,则将元素放到数组当前位置
         *  若数组该位置已经存在值,则进入后续判断
         */
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            /**
             * 判断数组当前位置是否重复插入相同值
             * 如果相同,只会将原值替换掉,保留新值,这也就是为什么Set会保证数据唯一性
             */
            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 {
                /**
                 * 循环遍历数组该位置的链表
                 * 没有跳出循环的临界条件,而是在程序中使用break结束循环
                 */
                for (int binCount = 0; ; ++binCount) {
                    /**
                     * 在这里一定要注意开始遍历的位置!!!p是数组当前位置元素,p.next是p所形成的链表的第二个元素!!!
                     * 判断是否已经到达了链表末端,如果已经到了末端,就先创建一个Node结点,保存对象数据
                     */
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        /**
                         * 1.判断循环次数是否大于树化临界值
                         *   binCount >= TREEIFY_THRESHOLD - 1 判断 binCount是否大于等于7
                         *   注意循环的起始位置是链表第二位,所以树化的阈值是8,这个地方一定要理解!!
                         * 2.但是树化还有另外一个条件,还需判断数组长度是否到达64
                         *   数组长度到达64,该链表树化
                         *   数组长度没到达64,数组扩容,但还是按照链表结构新添元素
                         */
                        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;
                }
            }
            /**
             * 如果元素重复,则替换value值
             */
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        /**
         * 如果添加后size大于临界值,则调用rezise()方法
         * 要注意只要新增数据,不管添加到哪个位置,只要成功添加,size都要自增
         */
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

6)resize()方法,完成数组扩容,以及数据从老数组到新数组的迁移。

final Node<K,V>[] resize() {
        /**
         * 创建临时数组oldTab保存table数据
         * oldCap用于记录当前数组容量
         * oldThr用于记录当前数组扩容临界值
         */
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        /**
         * 先对容量进行判断
         * 若当前数组容量大于0
         *  判断其是否已经大于数组最大容量MAXIMUM_CAPACITY(1 << 30)
         *    如果已经满足,则将临界值修改成Integer.MAX_VALUE,直接返回
         *    如果不满足,将旧容量扩容一倍与最大容量比较且旧容量是否大于默认初始容量,都满足的话,将旧临界值扩容一倍
         */
        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;
        /**
         * 只针对第一次扩容,即容量和临界值都为0
         */
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        /**
         * 判断新临界值
         *   若临界值为0,即初次加载
         *     则用新容量*加载因子计算新临界值
         */
        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;
    }

4.小结
HashSet底层是HashMap,在创建的时候,数据保存在Map的Key中,Value用Object对象占位。因此就能解释Set为什么能保证数据的唯一性。

由于时间原因,后续有空再慢慢补。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值