Java ArrayList【笔记】

ArrayList

ArrayList基本结构

ArrayList 整体架构比较简单,就是一个数组结构

源码中的基本概念

index 表示数组的下标,从 0 开始计数

elementData 表示数组本身

DEFAULT_CAPACITY 表示数组的初始大小,默认是 10

size 表示当前数组的大小,类型 int,没有使用 volatile 修饰,非线程安全

modCount 统计当前数组被修改的版本次数,数组结构有变动,就会 +1

类注释中的说明

1.允许 put null 值,会自动扩容

2.size、isEmpty、get、set、add 等方法时间复杂度都是 O (1)

3.是非线程安全的,多线程情况下,推荐使用线程安全类Collections#synchronizedList

4.增强 for 循环,或者使用迭代器迭代过程中,如果数组大小被改变,会快速失败,抛出异常

ArrayList的初始化方法

有三种,无参数直接初始化,指定大小初始化(给出的是数组的大小),指定初始数据初始化(给出的是数组元素,数组大小由元素个数来确定)

需要注意的是,ArrayList无参构造器初始化的时候,默认大小是空数组,并不是10,10是第一次add的时候的扩容的数组值

ArrayList新增以及扩容

新增就是往数组中添加元素,主要可以分成两步:第一步是判断是否需要扩容,如果需要的话先进行扩容的操作,第二步就直接进行赋值

以上两步的源码:

public boolean add(E e) {
//确保数组大小是否足够,不够执行扩容,size 为当前数组的大小
ensureCapacityInternal(size + 1); // Increments modCount!!
//直接赋值,线程不安全
elementData[size++] = e;
return true;
}

扩容的设计,如果初始化数组大小的时候,有给定过初始值,那么就以给定的大小为标准,不需要按照数组元素个数来确定数组的大小,我们在扩容的时候要记录数组被修改的情况,如果我们需要的最小容量大于目前的数组的长度,那么就扩容,需要将现有的数据拷贝进新的数组中

其中,如果扩容后的值小于期望值,那么扩容后的值就等于期望值,比如,我们需要16个,之前有10个,扩容以后有15个,还是不够,那么最后数组扩容后的大小就是16,但是,如果扩容后的值大于jvm所能分配的数组的最大值,那么就只能用 Integer 的最大值,然后通过复制进行扩容

源码:

private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
if (minCapacity - elementData.length > 0)
grow(minCapacity);
} private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}

关于新增和扩容有一些事项:

1.扩容的规则并不是整倍整倍的增加,而是原来的容量加上容量的一半,也就是变成原来的1.5倍

2.ArrayList中的数组的最大值为Integer.MAX_VALUE,一旦超过了这个数值,jvm就不会给数组分配任何内存空间了

3.在新增的时候,允许null值

4.扩容实际上是由Arrays.copyOf(elementData, newCapacity)实现的,其本质就是数组之间的新建与复制,先建一个符合的数组,然后将以前的数组的数据全部复制过去

ArrayList删除

删除元素的方式有很多,以根据值删除的方式来进行说明

首先我们要找到需要删除的元素的索引的位置,如果要删除的值是 null,那么就找到第一个值是 null 的删除,如果要删除的值不为 null,那么就找到第一个和要删除的值相等的删除,寻找的操作是通过equals来判断值是否相等的,那么在判断值相等以后,再根据索引位置进行删除

需要注意,在新增的时候是没有对 null 进行校验的,是允许null值的,那么在删除的时候,其实也是允许删除 null 值的

而且因为寻找值在数组中的索引位置是通过 equals 来判断的,数组元素是基本类型的情况时就按照正常的比较就可以,但是,如果数组元素不是基本类型,这个时候就需要关注 equals 的具体实现才可以

public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}

在找到需要删除的索引的位置以后,就需要根据索引位置来进行删除操作,简单来说,在某一个元素被删除后,为了维护数组结构,需要把数组后面的元素往前移动,数组最后空出来的位置为null

numMoved 表示删除 index 位置的元素后,需要从 index 后移动多少个元素到前面去,至于为什么要减 1,是因为 size 从 1 开始算,index 从 0开始算,我们从 index +1 位置开始被拷贝,拷贝的起始位置是 index,长度是 numMoved

private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index, numMoved);
elementData[--size] = null;
}

ArrayList迭代器

迭代器一般来说有三个方法:

1.hasNext 还有没有值可以迭代

2.next 如果有值可以迭代,迭代的值是多少

3.remove 删除当前迭代的值

hasNext的方法是比较简单的,如果下一个元素的位置和实际大小相等,就说明没有元素可以迭代了,不等的话,就还有可以迭代的

源码

public boolean hasNext() {
return cursor != size;
}

next的方法则是在迭代的过程中需要判断版本号有没有被修改,如果有修改就报异常,没有修改的话就在本次迭代过程中找元素的索引位置,找到迭代的值,并为下一次的迭代进行准备

源码:

public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}

Remove的方法,如果上一次操作的时候,数组位置已经小于0了,这就说明数组已经删除完了,然后也要在迭代的过程中检测版本号有没有修改,修改了就要报异常

源码:

public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification(); try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}

需要注意的是,remove中的lastRet = -1 的操作目的,是防止重复删除操作,且在删除元素成功,数组当前 modCount 就会发生变化,这里会把 expectedModCount 重新赋值,下次迭代时两者的值就会一致了

ArrayList线程安全

只有当 ArrayList 作为共享变量时,才会有线程安全问题,当 ArrayList 是方法内的局部变量时,是没有线程安全的问题的

一些问题

有一个 ArrayList,数据是 2、3、3、3、4,中间有三个 3,现在通过 for (int i=0;i<list.size ();i++) 的方式,需要把值是 3 的元素删除,请问可以删除干净么?最终删除的结果是什么,为什么?

删除代码如下:

List<String> list = new ArrayList<String>() {{
add("2");
add("3");
add("3");
add("3");
add("4");
}};
for (int i = 0; i < list.size(); i++) {
if (list.get(i).equals("3")) {
list.remove(i);
}
}

显而易见的,删不干净,因为没删除一个元素后,这个元素后面的元素就会往前移动,而此时循环的i是不会停下来的,会一直增长,这样就会漏掉一个

i为0的时候,不删除

i为1的时候,从0号位到了1号位,此时发现了3,进行删除操作,删除以后,元素前移,原本在2号位的3移动到了1号位,3号位的3移动到了2号位,4号位的4移动到了3号位,最后的位置变成了null

i为2的时候,从1号位到2号位,删除2号位的3,元素继续前移

i为3的时候,没有删除,结束,此时数组的情况是,0号位为2,1号位为3,2号位为4,3号位和4号位为null,这样就还剩一个3删不掉

可以发现,这种删除的话每次删除都会让后一个需要删除的遗漏

这种情况可以使用Iterator.remove () 方法来解决,因为 Iterator.remove () 方法在执行的过程中,会把最新的 modCount 赋值给 expectedModCount,这样在下次循环过程中,modCount 和 expectedModCount 两者就会相等,就不会出现遗漏的情况

这种情况对于 LinkedList 也是如此,虽然二者的底层结构不同,一个是数组,一个是双向链表

Java ArrayList【笔记】的更多相关文章

  1. Java开发笔记(六十七)清单:ArrayList和LinkedList

    前面介绍了集合与映射两类容器,它们的共同特点是每个元素都是唯一的,并且采用二叉树方式的类型还自带有序性.然而这两个特点也存在弊端:其一,为啥内部元素必须是唯一的呢?像手机店卖出了两部Mate20,虽然 ...

  2. 《Java学习笔记(第8版)》学习指导

    <Java学习笔记(第8版)>学习指导 目录 图书简况 学习指导 第一章 Java平台概论 第二章 从JDK到IDE 第三章 基础语法 第四章 认识对象 第五章 对象封装 第六章 继承与多 ...

  3. 瘋耔java语言笔记

    一◐ java概述                                                                                        1.1 ...

  4. Java基础笔记 – Annotation注解的介绍和使用 自定义注解

    Java基础笔记 – Annotation注解的介绍和使用 自定义注解 本文由arthinking发表于5年前 | Java基础 | 评论数 7 |  被围观 25,969 views+ 1.Anno ...

  5. 20145330第八周《Java学习笔记》

    20145330第八周<Java学习笔记> 第十五章 通用API 通用API 日志:日志对信息安全意义重大,审计.取证.入侵检验等都会用到日志信息 日志API Logger:注意无法使用构 ...

  6. 20145330第六周《Java学习笔记》

    20145330第六周<Java学习笔记> . 这周算是很忙碌的一周.因为第六周陆续很多实验都开始进行,开始要准备和预习的科目日渐增多,对Java分配的时间不知不觉就减少了,然而第十和十一 ...

  7. 20145330第五周《Java学习笔记》

    20145330第五周<Java学习笔记> 这一周又是紧张的一周. 语法与继承架构 Java中所有错误都会打包为对象可以尝试try.catch代表错误的对象后做一些处理. 使用try.ca ...

  8. 【转】Java基础笔记 – 枚举类型的使用介绍和静态导入--不错

    原文网址:http://www.itzhai.com/java-based-notes-introduction-and-use-of-an-enumeration-type-static-impor ...

  9. Java学习笔记4

    Java学习笔记4 1. JDK.JRE和JVM分别是什么,区别是什么? 答: ①.JDK 是整个Java的核心,包括了Java运行环境.Java工具和Java基础类库. ②.JRE(Java Run ...

随机推荐

  1. 9、SpringBoot整合之SpringBoot整合SpringSecurity

    SpringBoot整合SpringSecurity 一.创建项目,选择依赖 选择Spring Web.Thymeleaf即可 二.在pom文件中导入相关依赖 <!-- 导入SpringSecu ...

  2. 架构之:serverless架构

    目录 简介 什么是serverless serverless的例子 简单的三层服务 消息驱动 FaaS FaaS的缺点 FaaS的优点 总结 简介 不知道什么时候,出现了一个叫做Serverless架 ...

  3. 《Linux基础知识及命令》系列分享专栏

    <Linux基础知识及命令>系列分享专栏 本专题详细为大家讲解了Linux入门基础知识,思路清晰,简单易懂.本专题非常适合刚刚学习Linux的小白来学习,通过学习该专题会让你由入门达到中级 ...

  4. IDA 动态调试

    感谢南邮,让我把ida动态调试,给搞定了,困扰了很久,之前下的ubuntu的源,好像有问题,ifconfig这个命令一直装不上,突然想起来了我的服务器很久没用了,重装了下系统,换成ubuntu,这里记 ...

  5. Spring常见异常说明

    文章要点 Spring bean 的声明方式 Spring bean 的注入规则 Spring bean 的依赖查找规则 Spring bean 的名称定义方式和默认名称规则 XXX required ...

  6. varnish配置语言(1)

    目录 1. vcl语法 1.1 主体语法 1.2 操作符 1.3 Subroutines 1.4 关键字 2. 内置的Subroutines 2.1 client-side vcl_recv vcl_ ...

  7. 「AGC020F」 Arcs on a Circle

    「AGC020F」 Arcs on a Circle Link 这个题非常 Amazing 啊.果然AtCoder全是智商题 首先你可以注意到数据范围真的是小得离谱,让你想要爆搜. 然后你发现不可做, ...

  8. C语言:按相反顺序输出字符

    #include <stdio.h> void pailie(int n) { char next; if (n<=1) { next=getchar(); putchar(next ...

  9. LeetCode 847. Shortest Path Visiting All Nodes

    题目链接:https://leetcode.com/problems/shortest-path-visiting-all-nodes/ 题意:已知一条无向图,问经过所有点的最短路径是多长,边权都为1 ...

  10. ESCMScript6(3)Promise对象

    1. Promise的含义 Promise 是异步编程的一种解决方案,比传统的解决方案--回调函数和事件--更合理和更强大.它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了P ...