Java集合学习(含ArrayList、LinkedList、HashSet(HashMap)、TreeSet(TreeMap)等等)

简介

数组的不足

  1. 在数组初始化时长度必须定义,一旦定义就不能修改

  2. 存储的是同一类型的元素

  3. 数组扩容和裁剪比较麻烦

    • 扩容的第一步先创建满足大小的数组,第二步拷贝原数组到新数组中,第三步改变数组对象的引用

集合的好处

  1. 可以动态保存任意多个对象
  2. 提供了许多对应的增删改查的方法

集合框架体系

Collection:单列集合
  1. Collection接口继承自Iterable接口,有两个重要的子接口:List和Set

  2. List的重要实现类:ArrayList Vector LinkedList

  3. Set的重要实现类:HashSet TreeSet

  4. HashSet的重要子类:LinkedHashSet

    image-20210713130801097

Map:双列集合
  1. Map接口的重要实现类:HashMap Hashtable TreeMap

  2. Hashtable的重要子类:Properties

  3. HashMap的重要子类:LinkedHashMap

    image-20210713131020500

集合使用选择
image-20210717162652364

Collection接口体系

常用方法

image-20210713132330472

迭代器遍历
  1. Iterable接口中有一个iterator方法,返回值是Iterator对象。因此只要是实现了Collection的接口都实现了Iterable接口,因此他们都可以获取一个Iterator对象:用来遍历集合
  2. 把Iterator看作游标,hasNext()表示当前游标之后是否还有元素,next()表示游标下移,并返回下移后的上个元素
增强for循环
  1. 增强for可以直接用在集合上,也可以直接用在数组上
  2. 增强for底层仍然是迭代器,获取Iterator对象,调用hasNext()和next()方法

List

List接口体系

  1. List体系下的集合元素是有序的(存取顺序一致)
  2. List体系下的集合元素可以重复,可以存储null值
  3. List体系下的集合元素都有对应的索引
常用方法

image-20210714121323461

遍历方式
  1. 普通for循环
  2. 迭代器遍历
  3. 增强for循环

ArrayList底层结构和源码分析

一些结论
  1. ArrayList可以存放null值
  2. ArrayList底层是由数组实现的
  3. ArrayList基本等同于Vector,但是ArrayList为了高执行效率,没有做线程安全方面的控制,所以它是线程不安全的,因此多线程情况下,不建议使用ArrayList。具体表现在:ArrayList的方法都没有使用synchronized关键字
源码分析
  1. ArrayList中维护了一个Object类型的数组elementData:transient Object[] elementData
  2. 当创建ArrayList对象时,如果使用的是无参构造器,则初始化elementData的大小为0,第一次添加,大小扩容至10,再次扩容,则大小扩容至1.5倍
  3. 如果使用的是指定参数大小的构造器,则指定elementData的大小,若需扩容,则大小扩容至1.5倍

使用无参构造方法创建ArrayList时:

image-20210714140124166

向空的ArrayList中添加数据时:

image-20210714140520677

确定是否要扩容:

image-20210714141429981

image-20210714141529017

真实的扩容过程:

image-20210714142550394


使用有参构造创建ArrayList时:

image-20210714150247844

Vector底层结构和源码分析

一些结论
  1. Vector底层也是一个elementData数组:protected Object[] elementData
  2. Vector是线程安全的,所以效率会低。具体表现在Vector类中的方法都有synchronized关键字
源码分析

我这里是JDK15的源码,与JDK1.8略有不同

  1. 创建Vector对象时,如果使用的是无参构造器,则初始化elementData的大小为0,第一次添加,大小扩容至10,再次扩容,则大小扩容至2倍
  2. 如果使用的是指定参数大小的构造器,则指定elementData的大小。若需扩容,则大小扩容至2倍

使用无参构造方法创建Vector时:

image-20210714153333433

扩容的过程与ArrayList很相似

LinkedList底层结构和源码分析

  1. LinkedList底层实现了双向链表双端队列
  2. 线程不安全
  3. LinkedList中维护了两个属性first和last分别指向首节点和尾节点
  4. 每个结点(Node对象)里面又维护了三个属性:prev(前一个) next(后一个) item
  5. 这样的双向链表增删很快,效率也高,只需要改变prev和next的指向
模拟一个LinkedList
class Node{
    public Object item;		//真正存放数据的地方
    public Node prev;		//指向上一个节点
    public Node next;		//指向下一个节点
    
    public Node(Object name){
        this.item = name;
    }
    
}

public class Mock{
    public static void main(String[] args){
        
        Node jack = new Node("jack");
    	Node tom = new Node("tom");
    	Node zxl = new Node("zxl");
        
        //连接这三个几点,形成双向链表
        //jack --> tom --> zxl
        jack.next = tom;
        tom.next = zxl;
        
        //jack <-- tom <-- zxl
        zxl.prev = tom;
        tom.prev = jack;
        
        //维护首尾
        Node first = jack;
        Node last = zxl;
        
        //从头到尾进行遍历
        while(true){
            if(first == null){
                break;
            }
            System.out.println(first);
            first = first.next;
        }
        
        //在jack和tom之间插入hsp
        Node hsp = new Node("hsp");
        jack.next = hsp;
        hsp.next = tom;
        tom.prev = hsp;
        hsp.prev = jack;
    }  
}
源码分析

当使用无参构造创建LinkedList:

image-20210715100251104

image-20210715100340053

添加数据时,add的无参方法会添加数据到最后面

image-20210715100420802

image-20210715100800796

当第一次添加数据时,l=last=null:

image-20210715101625816

第一次添加数据成功后:

image-20210715101915271

删除数据时,remove的无参方法会删除最前面

image-20210715104250475

进入

image-20210715104353486

真正的删除方法:

image-20210715104707645

修改某个节点对象的数据:

image-20210715105748108

image-20210715105933172

检查机制:

image-20210715110049887

获得某个节点对象的数据:

image-20210715110503934

索引查找:

image-20210715110644789

索引查找需要经过很多次遍历:

image-20210715111056433

查找头尾节点:

image-20210715111449647

可以看出,LinkedList查找头尾的速度很快,但是索引查找的速度很慢。

Set

Set接口体系

  1. Set体系下的集合是无序的(存取顺序不一致)
  2. Set体系下的集合取出数据的顺序虽然与存放顺序不一致,但是取出的顺序是固定的
  3. Set体系下的集合元素不可以重复,最多只有一个null值
  4. Set体系下的集合没有索引
常用方法

参考Collection接口体系的常用方法

遍历方式
  1. 可以使用迭代器
  2. 可以使用增强for循环

HashSet的底层结构和源码分析

一些结论
  1. HashSet底层是HashMap
  2. HashSet取出元素的顺序取决于hash后,再确定索引的结果
模拟一个HashSet(HashMap)
class Node{
    Object item;
    Node next;
    
    public Node(Object name){
        this.item = name;
    }
}

public class HashSetStructure{
    public static void main(String[] args){
        //创建一个数组,数组的类型是Node
        //有些人直接把Node数组称为table
        Node[] table = new Node[16];
        
        //1.数组中添加Node数据
        Node zxl = new Node("zxl");
        table[2] = zxl;
        //2.将新的Node数据挂载到后面
        Node jack = new Node("jack");
        zxl.next = jack;
        //3.到这里,就完成了数组中存放着一个简单的链表,可以继续挂载:
        Node rose = new Node("rose");
        jack.next = rose;
    }
}
源码分析
  1. HashSet的底层是HashMap,HashMap底层是:数组+链表+红黑树(**个人理解:**数组里存放着链表)
  2. 当链表达到一定量并且数组的大小再一定范围内时,会对链表进行树化
  3. 树化和剪枝:树化就是链表长度大于等于8转换为红黑树,剪枝就是红黑树经过删除后若长度小于8则转换为链表

扩容机制结论:

  1. 添加一个元素时,先得到hash值(根据hashCode得到),hash值会转换为索引值
  2. 找到存储数据表table,查看这个索引位置是否有已经存放的元素
  3. 如果没有,则直接添加,如果有,就调用equals比较,如果相同就放弃添加,如果不同就添加到最后
  4. 在JDK1.8后,如果一条链表的元素个数大于等于TREEIFY_THRESHOLD(默认为8),并且table的大小大于等于MIN_TREEIFY_CAPACITY(默认为64),就会进行树化转换为红黑树
  5. 第一次扩容数组大小为16,临界值=默认初始容量 ∗ * 加载因子=16$*$0.75​=12,后续达到临界值便扩大2倍:数组大小32,临界值24
  6. 这个扩容的临界值指的是数据的数量,不是数组被占用的数量,扩容则扩容的是数组

创建HashSet:

image-20210715145443906

image-20210715145517745 image-20210717133636812

第一次添加数据:

image-20210715145710179

为了让HashSet使用到HashMap,没有value,要用PRESENT占位:

image-20210715150010327 image-20210715150818176

调用putVal,第一个参数是hash值:

image-20210715150230949

先查看hash方法,观察到实际上得到的是key的hashCode无符号右移16位(防止hash冲突)的值,这个值并不等价于hashCode

image-20210715151216926

再仔细分析putVal的工作流程:(最终是返回null,并且再一路返回true,添加成功)

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    	//定义了辅助变量
        Node<K,V>[] tab; 
    	Node<K,V> p; 
    	int n, i;
    	
    	//table是Node类型的数组,第一次添加数据时,table还为null
        if ((tab = table) == null || (n = tab.length) == 0)
            //resize方法见下方代码块,总之得到了tab是大小为16的数组,且n=16
            n = (tab = resize()).length;
    	//根据hash值得到一个tab[i]并赋值给p(这是一种求余数的方法)
    	//如果p为null,表示还没有存放过数据,就创建一个Node(key="123",value="object..."),直接放在tab[i]
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
    	//当tab[i]不为空时
        else {
            //辅助变量
            Node<K,V> e; 
            K k;
            //如果p索引位置对应的链表的第一个结点的hash值和准备添加的key的hash值相同,
            //如果p索引位置对应的链表的第一个结点的key对象和准备添加的key对象是同一个对象,
            //或者不是同一个对象,但是equals的比较结果相同
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //再判断p索引位置是不是一棵红黑树,如果是,就按照红黑树的方式去执行后续结点的比较和添加操作
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            //p索引位置还不是一个红黑树,就按照链表的方式去执行后续结点的比较和添加操作
            else {
                for (int binCount = 0; ; ++binCount) {
                    //比较结束,满足条件进行添加
                    if ((e = p.next) == null) {
                        //添加到链表的末端
                        p.next = newNode(hash, key, value, null);
                        //判断链表是否达到8个结点,若是,则进行树化操作,
                        //但treeifyBin内部还会判断table的大小是否大于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;
                }
            }
            //存在相同数据时,从上面的代码中可以知道,e不为null,则进入这个if
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;		//threshold默认是0
        int newCap, newThr = 0;
    
    	//oldCap和oldThr都为0
        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;
        }
        else if (oldThr > 0)
            newCap = oldThr;
        else {
            newCap = DEFAULT_INITIAL_CAPACITY;		//默认值是16(位左移4位相当于乘以2的4次方)
            //默认加载因子大小是0.75,在这一步设置临界值:0.75*16=12,是为了到达临界值12时直接进行扩容
            //(起到一个缓冲的作用)
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
    	
        threshold = newThr;		//12
        @SuppressWarnings({"rawtypes","unchecked"})
    	//在这里真正的创建了大小为16的table数组
        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;//大小为16
    }

LinkedHashSet的底层结构和源码分析

一些结论
  1. LinkedHashSet底层是一个LinkedHashMap,LinkedHashMap底层维护了一个:数组+双向链表
  2. LinkedHashSet根据元素的hashCode值来决定元素的顺序位置,同时使用链表维护元素的次序,导致存取顺序可以一致
  3. next结点的链接只是放在同一个数组中,有特殊的before和after结点来描述顺序
源码分析
  1. 在添加元素时,先得到hash值,再根据hash值得到索引,确定该元素在table中的位置
  2. 将元素加入到双向链表中(处理逻辑和HashSet一致)
  3. LinkedHashSet维护了head和tail属性,分别指向头结点和尾结点
  4. 每一个结点有before和after属性,这样可以形成双向链表
  5. table是一个Node数组,但是存放的数据类型是Entry,继承自HashMap.Node,有before和after属性**(多态)**

TreeSet的底层结构和源码分析

一些结论(TreeSet的特性很特殊,我更建议单独分析,不作为Set体系来分析)
  1. TreeSet的底层是TreeMap
  2. TreeSet(TreeMap)最大的特性就是可以排序
  3. TreeSet(TreeMap)的其中一个构造方法参数是比较器(Comparator<? super E>)
  4. TreeSet(TreeMap)的底层是:红黑树
  5. 若是使用无参构造方法,则添加的元素必须实现Comparable接口来重写CompareTo方法
源码分析

我这里是JDK15的源码,与JDK1.8略有不同

    TreeSet treeSet = new TreeSet(new Comparator<String>() {
        @Override
        //前者在前,表示升序
        //前者在后,表示降序
        public int compare(String o1, String o2) {
            return o2.compareTo(o1);
        }
    });
image-20210718090027060 image-20210718090106727 image-20210718090136477 image-20210718090155886

在添加第二个元素时开始比较:

image-20210718090845731

image-20210718091000929

进入到我们实现的匿名内部类中方法:

image-20210718091050992

要么手写比较逻辑,要么使用CompareTo方法

CompareTo方法需要相比较的元素在自己的类中是实现Comparable接口来重写,此处是String:

image-20210718091423157

然后根据比较结果进行判断排序,若相等情况下,会替换value:

这种相等情况很容易迷惑人,比如添加“abc”和“tom”两个字符串并且按照长度来比较,尽管两个对象不一样,还是会进行相等替换value

image-20210718091643548

Map接口体系

特点(多以HashMap为例)
  1. 用于保存具有映射关系的数据(key-value),key-value是一一对应的,一个key对应一个value
  2. key和value可以是任意类型的数据,会封装到HashMap.Node对象中
  3. Map中的key不允许重复,原因和HashSet一样,不过看起来有区别:Map会替换value,Set的value是Present,所以看不出来
  4. Map中的value可以重复,(1.他们都不一定在同一个链表上,2.类比一下Set中的Present)
  5. Map中的key最多只能有一个null,而value可以有多个null
  6. HashMap底层还会创建一个HashMap.EntrySet,封装这个Map的数据,数据类型是Map.Entry,(实际上是HashMap.Node,这是因为HashMap.Node实现了Map.Entry):Entry的key引用自Map中Node的key,value引用自Map中Node的value,这样做的目的是方便遍历:**缕一缕:**Node转换成了Entry,并且封装到了EntrySet,为什么要这样,因为Entry有getKey和getValue方法。我这样理解:Map是根据参数生成的Node对象存放在table中,自身并不能直接使用Node对象,所以需要第三方的容器来收集这些Node对象,然后遍历使用
  7. 除此之外,还有KeySet,KeySet就是(举例:HashMap和HashSet)的桥梁
image-20210717110111734
常用方法
image-20210717110847027
遍历方式

image-20210717114334184

HashMap的底层结构和源码分析

一些结论
  1. HashMap是使用频率最高的实现类
  2. HashMap是线程不安全的
源码分析

扩容机制结论:

  1. 和HashSet一摸一样,一个差异在于HashMap的替换重复value是可见的


Hashtable底层结构和源码分析

一些结论
  1. Hashtable的key和value都不能为null,否则会空指针异常
  2. Hashtable是线程安全的
  3. 底层是:数组+链表,维护了一个Hashtable.Entry类型的数组,Hashtable​.Entry实现了Map.Entry
  4. 这个数组的大小(table)为11,threshold大小为8(11$*$0.75)
  5. 与HashMap添加在链表最后面不同,Hashtable添加数据是添加在链表的最前面
源码分析

我这里是JDK15的源码,与JDK1.8略有不同

扩容机制

根据无参构造创建一个Hashtable,在创建时就会得到一个大小为11的table,且临界值为8:

image-20210717151653025 image-20210717151722515

添加数据时调用put方法:

	public synchronized V put(K key, V value) {
        //判断value是否为空
        if (value == null) {
            throw new NullPointerException();
        }

        //判断key是否存在在table中
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        @SuppressWarnings("unchecked")
        //根据hash得到索引,再根据索引得到entry
        Entry<K,V> entry = (Entry<K,V>)tab[index];
        //顺着这个链表一个一个进行比较
        for(; entry != null ; entry = entry.next) {
            //比较的逻辑
            if ((entry.hash == hash) && entry.key.equals(key)) {
                //通过比较发现key存在的话,就替换key对应的value
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }
		//比较完毕key不存在table中,执行addEntry方法
        addEntry(hash, key, value, index);
        return null;
    }

添加Entry的addEntry方法:

 	private void addEntry(int hash, K key, V value, int index) {
        Entry<?,?> tab[] = table;
        //当操作次数大于等于临界值时,因为这里判断在前,count还没有进行自加操作,所以要有=,
        //要明白仍然是添加的数据大于临界值才行,第一次也就是9,不能是8
        if (count >= threshold) {
            //扩容操作,11*2+1=23
            rehash();

            tab = table;
            hash = key.hashCode();
            index = (hash & 0x7FFFFFFF) % tab.length;
        }

        //还没达到扩容临界值
        @SuppressWarnings("unchecked")
        //Hashtable添加到链表中是添加到最前面的
        Entry<K,V> e = (Entry<K,V>) tab[index];
        //从这里看出来,e是null或者是原来的Entry
        tab[index] = new Entry<>(hash, key, value, e);
        count++;
        modCount++;
    }
image-20210717154613526

Properties的底层结构和源码分析

  1. Properties继承自Hashtable,Hash的一些特性Properties也必须满足
  2. Properties多用于xxx.properties配置文件中,在IO流中发挥很大的作用

TreeMap的底层结构和源码分析

一些结论(TreeMap的特性很特殊,我更建议单独分析,不作为Set体系来分析)
  1. 和TreeSet几乎一摸一样,不过有具体的value
  2. 补充(同样适用于TreeSet):第一次添加的数据也会调用compare方法,不过只是为了判断TreeMap是否为空
  3. 补充(同样适用于TreeSet):第一添加的数据会放入root中
image-20210718102838638

Collections工具类

常用方法

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值