一篇文章带您读懂List集合(源码分析)
今天要分享的Java集合是List,主要是针对它的常见实现类ArrayList进行讲解
内容目录
什么是List核心方法源码剖析1.文档注释2.构造方法3.add()3.remove()如何提升ArrayList的性能ArrayList可以代替数组吗?
什么是List
List集合是线性数据结构的主要实现,用来存放一组数据。我们称之为:列表。
ArrayList是List的一个常见实现类,它的面试频率和使用频率都非常高,所以我们今天通过学习ArrayList来对Java中的List集合有一个深入的理解。
ArrayList最大的优势是可以将数组的操作细节封装起来,比如数组在插入和删除时要搬移其他数据。另外,它的另一大优势,就是支持动态扩容,这也是我们使用ArrayList的主要场景之一,在某些情况下我们没有办法在程序编译之前就确定存储数据
容器的大小。
核心方法源码剖析
这一部分,选取了ArrayList的一些核心方法进行讲解。分别是:构造方法,add()、和remove()。这里有一个小窍门,我们在读jdk源码的时候,一定要先看类上的doc注释,比较核心的知识点都会写在上面。有一个初步的概念再去看源码,就会容易很多。
1.文档注释
This class is roughly equivalent to Vector, except that it is unsynchronized.
大致相当于Vector,不同之处是不同步(线程不安全)
Implements all optional list operations, and permits all elements, including null
实现所有可选列表操作,并允许所有元素,包括null
in the face of concurrent modification, the iterator fails quickly and cleanly
面对并发修改,迭代器将快速而干净地失败
2.构造方法
ArrayList()提供了三种构造方法。
ArrayList():构造一个初始容量为10的空列表。
ArrayList(int initialCapacity):构造具有指定初始容量的空列表。
ArrayList(Collection c):构造一个包含指定集合的元素的列表,按照它们由集合的迭代器返回的顺序。
1/**
2 * Constructs an empty list with an initial capacity of ten.
3 */
4public ArrayList() {
5 this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
6}
这里的DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一个空的部数组,不设定初始值时,只是引用这个内部数组。
1/**
2 * Constructs an empty list with the specified initial capacity.
3 *
4 * @param initialCapacity the initial capacity of the list
5 * @throws IllegalArgumentException if the specified initial capacity
6 * is negative
7 */
8public ArrayList(int initialCapacity) {
9 if (initialCapacity > 0) {
10 this.elementData = new Object[initialCapacity];
11 } else if (initialCapacity == 0) {
12 this.elementData = EMPTY_ELEMENTDATA;
13 } else {
14 throw new IllegalArgumentException("Illegal Capacity: "+
15 initialCapacity);
16 }
17}
这里的EMPTY_ELEMENTDATA同样是一个空内部数组,为了和DEFAULTCAPACITY_EMPTY_ELEMENTDATA做区分,所以没有使用一个对象。
3.add()
add方法是ArrayList中的一个核心方法,涉及到内部数组的扩容。
1 /**
2 * Appends the specified element to the end of this list.
3 *
4 * @param e element to be appended to this list
5 * @return <tt>true</tt> (as specified by {@link Collection#add})
6 */
7public boolean add(E e) {
8 ensureCapacityInternal(size + 1); // Increments modCount!!
9 elementData[size++] = e;
10 return true;
11}
该方法是在集合中追加元素。其中核心方法是ensureCapacityInternal,意思是确定集合内部容量。
1private void ensureCapacityInternal(int minCapacity) {
2 ensureExplicitCapacity(
3 calculateCapacity(elementData,minCapacity));
4}
5
6private static int calculateCapacity(Object[] elementData, int
7 minCapacity) {
8 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
9 return Math.max(DEFAULT_CAPACITY, minCapacity);
10 }
11 return minCapacity;
12}
这里首先计算了集合的容量,如果这个ArrayList是通过无参构造创建的,那么比较默认值10,以及传入的minCapacity,取最大值,这里可能有的同学会有疑问,为什么要比较默认值和minCapacity,默认值不是一定大于minCapacity吗?,这里是因为ensureCapacityInternal这个方法不仅仅是add()会调用,allAll()也会调用。
1public boolean addAll(int index, Collection<? extends E> c) {
2 rangeCheckForAdd(index);
3
4 Object[] a = c.toArray();
5 int numNew = a.length;
6 ensureCapacityInternal(size + numNew); // Increments modCount
7 //省略部分代码..
8 }
这里如果numNew大于10,那么默认值就会不够用。所以才会在calculateCapacity方法中引入一个求最大值的步骤。
算出集合存储数据所需的最小空间后,就要考虑,集合原有存储空间是否够用,是否需要扩容。
1private void ensureExplicitCapacity(int minCapacity) {
2 modCount++;
3 // overflow-conscious code
4 if (minCapacity - elementData.length > 0)
5 grow(minCapacity);
6}
7
8/**
9 * Increases the capacity to ensure that it can hold at least the
10 * number of elements specified by the minimum capacity argument.
11 *
12 * @param minCapacity the desired minimum capacity
13 */
14 private void grow(int minCapacity) {
15 // overflow-conscious code
16 int oldCapacity = elementData.length;
17 int newCapacity = oldCapacity + (oldCapacity >> 1);
18 if (newCapacity - minCapacity < 0)
19 newCapacity = minCapacity;
20 if (newCapacity - MAX_ARRAY_SIZE > 0)
21 newCapacity = hugeCapacity(minCapacity);
22 // minCapacity is usually close to size, so this is a win:
23 elementData = Arrays.copyOf(elementData, newCapacity);
24}
这里我们主要关注4个点:
1.int newCapacity = oldCapacity + (oldCapacity >> 1);每次扩容是原数组的1.5倍
2.扩容也是有限的,存在最大值:MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
3.集合扩容底层调用的是:Arrays.copyOf()方法,需要把数组中的数据复制一份,到新数组中,而这个方法底层是System.arrayCopy是一个native方法,效率不高。
4.最重要的一个点:如果我们可以事先估计出数据量,那么最好给ArrayList一个初始值,这样可以减少其扩容次数,从而省掉很多次内存申请和数据搬移操作。(不指定初始值,至少会执行一次grow方法,用于初始化内部数组)。
3.remove()
1/**
2 * Removes the element at the specified position in this list.
3 * Shifts any subsequent elements to the left (subtracts one from their
4 * indices).
5 *
6 * @param index the index of the element to be removed
7 * @return the element that was removed from the list
8 * @throws IndexOutOfBoundsException {@inheritDoc}
9 */
10public E remove(int index) {
11 rangeCheck(index);
12 modCount++;
13 E oldValue = elementData(index);
14
15 int numMoved = size - index - 1;
16 if (numMoved > 0)
17 System.arraycopy(elementData, index+1, elementData, index,
18 numMoved);
19 elementData[--size] = null; // clear to let GC do its work
20 return oldValue;
21}
删除的代码因为不涉及到缩容,所以比起add较为简单,首先会检查数组是否下标越界,然后会获取指定位置的元素,接着进行数据的搬移,将--size位置的元素置成null,让GC进行回收。最后将目标元素返回即可。
另外最后我想提出一个比较容易犯的错误,集合在遍历的时候,对其结构进行修改(删除、新增元素)。举一个例子:
1public class Test {
2 public static void main(String[] args) {
3 List<Integer> list = new ArrayList<>();
4 list.add(1);
5 list.add(2);
6 list.add(3);
7 for (Integer i : list) {
8 if(i.equals(1)){
9 list.remove(i);
10 }
11 }
12 }
13}
结果:
1Exception in thread "main" java.util.ConcurrentModificationException
2 at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
3 at java.util.ArrayList$Itr.next(ArrayList.java:859)
4 at jialin.li.Test.main(Test.java:12)
产生问题的原因,其实文档注释已经给出了明确的结果,即:
if the list is structurally modified at any time after the iterator is created, in any way except through the iterator's own {@link ListIterator#remove() remove} or {@link ListIterator#add(Object) add} methods, the iterator will throw a {@link ConcurrentModificationException}
如果列表在任何时间从结构上修改创建迭代器之后,以任何方式除非通过迭代器自身remove种或add方法,迭代器都将抛出一个ConcurrentModificationException。这里我建议是遍历的时候,不要对其结构进行修改,而是采用其他方法(打标,或者复制列表)的方式进行。
如何提升ArrayList的性能
1 给定初值,省掉很多次内存申请和数据搬移操作。
2 对于读多写少的场景,可以使用ArrayList替代LinkedList,可以省内存,同时CPU缓存的利用率也会更高。(数组存储的时候,是内存是连续的,CPU读取内存数据、内存读取磁盘数据的时候,都不是一条一条读取,而是一次读取临近的一批数据,所以连续的存储可以让CPU更有机会一次读取较多的有效数据)
ArrayList可以代替数组吗?
不可以,任何数据结构都有它存在的场景和意义,集合没有办法存储基本数据类型,只能存储包装类型,包装类型就意味着需要拆箱和装箱,会有一定的性能消耗,如果对性能要求非常高的系统,或者只需要使用基本类型,那么就应该去使用数组而不是集合。同时数组在表示多维数据的时候,也更加直观,比如二维 int[][] 、ArrayList<arraylist>。我们使用集合更多的情况是想利用它的扩容特性,以及增删数据时不会造成空洞。
最后,期待您的订阅和点赞,专栏每周都会更新,希望可以和您一起进步,同时也期待您的批评与指正!
一篇文章带您读懂List集合(源码分析)的更多相关文章
- 一篇文章带您读懂Map集合(源码分析)
今天要分享的Java集合是Map,主要是针对它的常见实现类HashMap进行讲解(jdk1.8) 什么是Map核心方法源码剖析1.文档注释2.成员变量3.构造方法4.put()5.get() 什么是M ...
- 一篇文章让你读懂Pivotal的GemFire家族产品
一篇文章让你读懂Pivotal的GemFire家族产品 学习了:https://www.sohu.com/a/217157517_747818
- Java 集合源码分析(一)HashMap
目录 Java 集合源码分析(一)HashMap 1. 概要 2. JDK 7 的 HashMap 3. JDK 1.8 的 HashMap 4. Hashtable 5. JDK 1.7 的 Con ...
- java集合源码分析(三):ArrayList
概述 在前文:java集合源码分析(二):List与AbstractList 和 java集合源码分析(一):Collection 与 AbstractCollection 中,我们大致了解了从 Co ...
- java集合源码分析(六):HashMap
概述 HashMap 是 Map 接口下一个线程不安全的,基于哈希表的实现类.由于他解决哈希冲突的方式是分离链表法,也就是拉链法,因此他的数据结构是数组+链表,在 JDK8 以后,当哈希冲突严重时,H ...
- 从Generator入手读懂co模块源码
这篇文章是讲JS异步原理和实现方式的第四篇文章,前面三篇是: setTimeout和setImmediate到底谁先执行,本文让你彻底理解Event Loop 从发布订阅模式入手读懂Node.js的E ...
- Java集合源码分析(二)ArrayList
ArrayList简介 ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长,类似于C语言中的动态申请内存,动态增长内存. ArrayList不是线程安全的,只能用在单线程环境下,多线 ...
- Java集合源码分析(一)ArrayList
前言 在前面的学习集合中只是介绍了集合的相关用法,我们想要更深入的去了解集合那就要通过我们去分析它的源码来了解它.希望对集合有一个更进一步的理解! 既然是看源码那我们要怎么看一个类的源码呢?这里我推荐 ...
- Java集合源码分析(三)Vevtor和Stack
前言 前面写了一篇关于的是LinkedList的除了它的数据结构稍微有一点复杂之外,其他的都很好理解的.这一篇讲的可能大家在开发中很少去用到.但是有的时候也可能是会用到的! 注意在学习这一篇之前,需要 ...
随机推荐
- 黑马eesy_15 Vue:常用语法
自学Java后端开发,发现14 微服务电商[乐优商城]实战项目,在介绍完SpringCloud后就要肝前端的基础知识ES6语法和Vue.js 所以本篇博客作为入门Vue练习记录的过程,目的是供自学后端 ...
- tensorflow(三)
1.placeholder 一个数据占位符,用于在构建一个算法时留出一个位置,然后在run时填入数据. x = tf.placeholder(tf.float32) y = tf.placeholde ...
- 利用Python暴力爆破PDF密码
一个简单的Python脚本,可用于暴力破解受密码保护的PDF文件的密码脚本已在使用128位RC4(大多数信用卡对帐单)加密的PDF上进行了测试,成功率为100% pasword='<passwo ...
- 代码审计中的SQL注入
0x00 背景 SQL注入是一种常见Web漏洞,所谓SQL注入,就是通过把SQL命令插入到Web表单提交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意的SQL命令.本文以代码审计的形式研 ...
- 关于Java集合框架总结
Java集合专门用来存放多个对象,方便程序处理数据.Java提供了多种集合类,以便满足不同的应用需求,这些集合类分为两大系列:Collection和Map List List的通用方法 boolean ...
- 4.Redis持久化方案
1.1 RDB持久化 RDB方式的持久化是通过快照(snapshotting)完成的,当符合一定条件时Redis会自动将内存中的数据进行快照并持久化到硬盘. RDB是Redis默认采用的持久化方式. ...
- 一:MYsql登录,服务开启和停止
字段的属性: 1:名称 2:数据类型 3:长度 4:约束 SQL的分类:(结构化查询语言) 1:数据查询语言DQL select 2:数据操纵语言DML insert delete u ...
- 科技报告|AD报告|DTIC|PB报告|STAR|ERA|NTIS|DTIC|DOE|EPA|NASA |JPL|
信息检索-科技报告是灰色文献获取渠道有限. 技术论文中因保密需要,会删除关键性技术. AD报告也产生较早,1951年开始出版.现由美国国防技术情报中心(DTIC:Defence Technical I ...
- javascript中的undefined 和 not defined
经研究发现,两者之间有很大的区别,不知从英语讲,这两者都有啥区别,研究结果如下 测试os:ubuntu 测试浏览器:chrome 测试案例1 console.log(a) 报错 ReferenceEr ...
- deeplearning.ai 构建机器学习项目 Week 2 机器学习策略 II
1. 误差分析(Error analysis) 误差分析的目的是找到不同误差源的比重,从而指引我们接下来往哪个方向努力改进.NG建议手工统计随机100个错误的误差源,比如对于猫分类器,错误的照片可能是 ...