Java集合终极版!看这一篇就够了!超详细!

没有伞的孩子必须努力奔跑!


推荐阅读:

《谈谈HashMap为什么是线程不安全的?》

《ConcurrentHashMap实现原理(JDK1.7和JDK1.8)》

《通俗易懂搞定HashMap底层原理》

酱子今天主要详细介绍集合的常用实现类

其中集合系列的HashMap和ConcurrentHashMap我有单独写文章,小伙伴们可以移步推荐阅读查看。

在本篇文章开始之前,先上图!

集合相关类和接口都在java.util中

Java集合主要由两个接口派生出来的:Collection 和 Map

Collection:用于存放单一元素

  • List存放有序可重复数据

  • Set存放无序不可重复数据

Map:用于存放key-value键值对数据

接下来就一一介绍其具体实现类 

目录

一、ArrayList

二、LinkedList

三、ArrayList 和 LinkedList的比较

四、Vector

五、HashSet


一、ArrayList

  • 如何实现

ArrayList实现了List接口、RandomAccess标记接口、Cloneable接口、Serializable接口,继承了AbstractList抽象类。源码如下

RandomAccess标记接口,这个接口内部是空的,标记“实现了这个接口的类支持快速随机访问”,即不需要遍历,通过下标(索引)就可以直接访问到内存地址。

Cloneable接口,表明ArrayList支持拷贝clone()

Serializable标记接口,这个接口内部也是空的,标记“实现了这个接口的类支持序列化”。

这里序列化指将对象转化成以字节序列的形式来表示。序列化后的对象可以被写到数据库、写到文件,也可用于网络传输。

  • 结构

底层基于数组实现(ArraList基于数组,是一块连续的内存空间),并且实现了动态扩容。

数组的容量是在定义的时候确定的,如果数组满了再插入会数组溢出,所以在插入数据的时候会先检查是否需要扩容,扩容是按照原来数组的1.5倍扩容。

  • Get 查询操作

可以直接通过数组下标获取get(index)

  • Add / Remove 维护操作

ArrayList可以通过有参构造方法制定底层数组的大小,通过无参构造方法的方式,数组的容量为0,只有真正进行数据添加add时,才会分配默认DEFAULT_CAPACITY = 10 的初始容量。

ArrayList新增元素有两种情况,一种是指定位置新增,一种是直接新增。当我们存入基本数据类型int、long等时只能存储他们对应的包装类Integer、Long等。源码如下

直接新增,在新增之前会先是否需要扩容ensureCapacityInternal,然后将元素添加到末尾。

指定位置新增,先检查插入的位置是否在合理范围,如果不在合理范围会直接抛出角标越界异常,如果在合理范围则继续判断是否需要扩容,然后就是arraycopy将元素添加到指定位置。(这种性能会比较差)

arraycopy是效率慢的主要原因

比如我要add(3,"我"),它是从index=2的地址开始复制了一个数组,然后放在index=3的位置,给要添加的元素“我”腾出位置,让其放在index=2的位置上完成新增操作。

这里有个问题!

如果定义了new ArrayList<>(10);然后add(3,"我");会抛出角标越界异常,因为ArrayList不会初始化数组大小。虽然设置了初始大小,但是直接打印List大小的时候仍然是0。

ArrayList删除元素的方式也有两种,一种直接删除,一种指定index删除,两种性能都比较差,源码如下

直接删除,就是直接遍历数组,找到元素对应索引,然后删除。

指定index删除,则会先判断索引是否在合理范围,然后删除。原理同新增差不多,这里就不多介绍了。

  • 扩容机制

实现方式比较简单,就是通过数组扩容的方式实现。

比如现有一个长度为10的数组,新增一个元素,发现满了,那就需要进行扩容,重新定义一个长度为 10+(10>>1) 即 15的数组,然后把原数组的数据原封不动的复制到新数组中,这个时候再把指向原数的地址换到新数组。就实现动态扩容。

动态扩容意味着实际大小可能永远无法被填满,总有多余出来空置的内存空间。

比如上面添加第11个数据的时候,扩容到了15,有4个内存空间是闲置的。序列化的时候,如果把整个数组都序列化,就多序列化了4个内存空间,元素数量越多,空闲的空间就可能越大,序列化耗费的时间就越多。

ArrayList的序列化不太一样,它使用transient修饰存储元素的elementData的数组,transient关键字的作用是让被修饰的成员属性不被序列化

ArrayList内部提供了两个私有方法writeObject 和 readObject 来完成序列化和反序列化。序列化使用的是ArrayList的实际大小而不是数组的长度。

  • ArrayList是否是线程安全的?什么是线程安全的?

ArrayList不是线程安全的。

线程安全版本的数组容器是Vector,Vector的实现很简单,就是把所有的方法统统加上synchronized就完事了。

也可以不使用Vector,用Collections.synchronizedList把一个普通ArrayList包装成一个线程安全版本的数组容器,原理同Vector是一样的。

还有就是使用CopyOnWriteArrayList。

  • CopyOnWriteArrayList

CopyOnWriteArrayList采用了一种读写分离的并发策略,允许并发读,读操作是无锁的,性能较高。至于写操作,比如向容器中添加一个元素,则首先将当前容器复制一份,然后在新副本上执行写操作,结束之后再将原容器的引用指向新容器。

缺点

CopyOnWriteArrayList是很耗费内存的,每次set()/add()都会复制一个数组出。

另外就是CopyOnWriteArrayList只能保证数据的最终一致性,不能保证数据的实时一致性。

对于线程不安全的类,并发情况下可能会出现fail-fast情况;而线程安全的类,可能出现fail-safe的情况。

  • 快速失败(fail-fast)

快速失败是Java集合的一种错误检测机制

单线程:当在用增强for循环对一个集合进行遍历操作时,如果遍历的过程中集合的结构发生了变化(增加或者删除元素),就会抛出并发修改异常ConcurrentModificationException。(所以不要在 for-each 循环里进行元素的 remove/add 操作。可以使用Iterator,因为Iterator在remove的时候保证了expectedModCount 与 modCount 的同步。)

多线程:当一个线程在对一个集合进行遍历操作的时候,如果其他线程对这个集合的结构进行了修改,就会抛出并发修改异常ConcurrentModificationException。

原理:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。如果出现ABA的情况则不会抛出异常。

  • 安全失败(fail-safe)

如 ConcurrentHashMap、CopyOnWriteArrayList等。其原理在于开始遍历之前会将原集合完整的复制一份,前者在复制的集合上进行遍历,在原集合上进行元素添加、删除操作,后者反之,这样就可以避免了ConcurrentModificationException异常。

二、LinkedList

  • 如何实现

LinkedList实现了List接口,Deque接口,Cloneable接口,Serializable接口,继承了AbstractSequentialList抽象类,源码如下

AbstractSequentialList提供了一套基于顺序访问的接口。通过继承此类,子类仅需实现部分代码即可拥有完整的一套访问某种序列表的接口。

Deque接口继承自Queue接口,所以LinkedList就具备了队列的功能。

LinkedList支持序列化,并实现了writeObject方法按照自己的方式序列化,在序列化时只保留了元素的内容item,并没有保留元素的前驱和后继。这样就节省了很多内存空间。反序列化时,通过for循环中的linkLast方法,把链接重新链接起来,这样就恢复健康了链表序列化之前的顺序。

与ArrayList相比,LinkedList并没有实现RandomAccess接口,因为LinkedList存储数据的内存地址不是连续的,所以不支持随机访问。

  • 结构

LinkedList是一个双向链表结构,可以作为堆栈、队列或双端队列进行操作。

链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

双向链表的每个节点用内部类Node表示,LinkedList通过first和last引用分别指向链表的第一个和最后一个元素,当链表为空的时候first和last都指向null

内部定义了Node节点,包含三个部分:item元素内容,next后继(存储下一个节点地址),prev前驱(存储上一个节点地址)

  • get查询操作

根据index查询,会先校验index是否在合理范围,然后会调用node方法遍历LinkedList查找指定位置上的元素,如果插入的元素在前半段,就从队头first开始找,否则从队尾last开始找。所以插入的位置越靠中间,花费的时间就越多。

使用的是for循环,所以LinkedList在get的时候性能会非常差,所以在遍历LinkedList的时候,建议使用迭代器。

迭代器只会调用一次 node(int) 方法,在执行 list.iterator() 的时候:先调用 AbstractSequentialList 类的 iterator() 方法,再调用 AbstractList 类的 listIterator() 方法,再调用 LinkedList 类的 listIterator(int) 方法

  • add/remove更新操作

新增元素有两种情况,一种直接将元素添加到队尾,一种是将元素添加到指定位置。

直接将元素添加到队尾,源码如下

简单总结一下就是:

  • 首先获取最后一个节点l,

  • 然后创建一个新的节点newNode,前驱指向l,要保存的数据e,后继节点为null

  • 然后将新节点作为最后一个节点last

  • 在判断当前是否为第一次添加数据即l是否为null,如果 l 为 null,说明是第一次添加,所以 first 为新的节点;否则将新的节点赋给之前 last 的 next。

添加数据到指定位置,源码如下

简单总结一下就是:

  • 先校验插入的位置是否在合理范围内,如果不合理会抛出异常。

  • 判断插入的位置是否是队尾,如果是,直接调用linklast插入数据到队尾即可,否则调用linkBefore方法。

  • 在执行linkBefore方法之前,会调用node方法遍历LinkedList查找指定位置上的元素,如果插入的元素在前半段,就从队头first开始找,否则从队尾last开始找。所以插入的位置越靠中间,花费的时间就越多。

  • linkBefore中,先将 succ 的前一个节点(prev)存放到临时变量 pred 中,然后生成新的 Node 节点(newNode),并将 succ 的前一个节点变更为 newNode,如果 pred 为 null,说明插入的是队头,所以 first 为新节点;否则将 pred 的后一个节点变更为 newNode。

删除也差不多的原理,大家有兴趣可以自己看一下源码,这里就不多介绍啦

三、ArrayList 和 LinkedList的比较

  • 结构不同,ArrayList基于数组实现,LinkedList基于双向链表实现

  • 多数情况下,ArrayList查询快增删慢,LinkedList增删更快查询慢。因为ArrayList基于数组实现,可以直接通过数组下标获取,所以查询比较快,插入删除的时候需要元素向前或向后移动,还有可能触发扩容,所以增删慢。LinkedList基于双向链表实现,需要遍历链表,所以查询慢,插入删除元素只需要改变前驱和后继不需要移动元素,所以增删快。(注:凡事没有绝对,比如ArrayList删除的元素就在第一位,也很快哦)

  • ArrayList基于数组需要一块连续的内存空间,LinkedList基于链表,内存空间可以不连续。

四、Vector

Vector底层结构是数组,用的比较少,相对于ArrayList它是线程安全的,所有公有方法全都是synchronized的,扩容时他是2倍扩容。

五、HashSet

HashSet实现Set接口,标志着内部元素是无序的,元素是不可以重复的。实现Cloneable接口,标识着可以它可以被复制。实现Serializable接口,标识着可被序列化。

源码如下

HashSet底层就是基于HashMap。底层使用HashMap来保存所有元素,因此HashSet 的实现比较简单,相关HashSet的操作,基本上都是直接调用底层HashMap的相关方法来完成:

定义一个虚拟的Object对象作为HashMap的value,使用HashMap的key保存HashSet的元素。HashMap允许key和value为null,所以HashSet也可以存null。

HashSet就是限制了功能的HashMap,所以了解HashMap的实现原理,这个HashSet自然就通;对于HashSet中保存的对象,主要要正确重写equals方法和hashCode方法,以保证放入Set对象的唯一性;

从底层代码看,HashSet执行add方法,底层调用的也是HashMap的put方法,如果key相同,值不同,则会覆盖value;HashSet看起来值没有覆盖,是因为HashSet底层是HashMap,他的值就是HashMap的key,而value则是一个固定的Object对象。在这,相当于key相同,value也相同,故不会替换。

总结

集合系列差不多就这些啦,面试经常会问到的就这些,还有一些比较少遇到,在本篇文章中就没有过多介绍,高频出现的HashMap系列酱子单独写了文章。

如果本篇文章有任何错误,请大家多多包涵批评指教,不胜感激!

我是酱子,感谢大家对本期文章的阅读,创作不易,各位的支持和认可是我最大的动力,如果觉得文章写的不错的话,就请各位点赞在看关注,我们下期见~

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
非常感谢您的提问,我很乐意为您解答关于JAVA的ArrayList集合的问题。 ArrayList是JAVA中最常用的集合之一,它是一个动态数组,可以根据需要自动扩展大小。ArrayList可以存储任何类型的对象,包括基本数据类型和自定义对象。下面是一些关于ArrayList的详细讲解: 1. ArrayList的创建和初始化 在JAVA中,创建ArrayList对象的语法如下: ArrayList<数据类型> arrayList = new ArrayList<数据类型>(); 其中,数据类型可以是任何JAVA中的数据类型,例如:Integer、String、Double等等。初始化ArrayList对象后,可以使用add()方法向其中添加元素,例如: arrayList.add("Hello"); arrayList.add("World"); arrayList.add("!"); 2. ArrayList的常用方法 ArrayList提供了许多常用的方法,例如: - add():向ArrayList中添加元素 - get():获取ArrayList中指定位置的元素 - set():替换ArrayList中指定位置的元素 - remove():从ArrayList中删除指定元素 - size():获取ArrayList中元素的数量 - clear():清空ArrayList中的所有元素 下面是一些示例代码: // 添加元素 arrayList.add("Java"); arrayList.add("is"); arrayList.add("awesome"); // 获取元素 String firstElement = arrayList.get(0); System.out.println(firstElement); // 输出:Java // 替换元素 arrayList.set(1, "really"); System.out.println(arrayList); // 输出:[Java, really, awesome] // 删除元素 arrayList.remove("Java"); System.out.println(arrayList); // 输出:[really, awesome] // 获取元素数量 int size = arrayList.size(); System.out.println(size); // 输出:2 // 清空ArrayList arrayList.clear(); System.out.println(arrayList); // 输出:[] 3. ArrayList的遍历 遍历ArrayList可以使用for循环或者foreach循环,例如: // 使用for循环遍历ArrayList for (int i = 0; i < arrayList.size(); i++) { String element = arrayList.get(i); System.out.println(element); } // 使用foreach循环遍历ArrayList for (String element : arrayList) { System.out.println(element); } 4. ArrayList的性能 ArrayList的性能比较高,因为它是基于数组实现的,可以随机访问元素。但是,当需要频繁插入或删除元素时,ArrayList的性能会受到影响,因为需要移动数组中的元素。在这种情况下,可以考虑使用LinkedList。 总之,ArrayList是JAVA中非常常用的集合之一,它提供了许多常用的方法,可以方便地操作元素。希望我的回答能帮助您更好地理解ArrayList。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值