对于广大java程序员来说,ArrayList的使用是非常广泛的,但是发现很多工作了好几年的程序员不知道底层是啥。。。这我觉得对于以后的发展是非常不利的,因为java中的每种数据结构的设计都是非常完善的,学习了这种思想,在设计自己的容器是非常有帮助的。

一、ArrayList底层结构

ArrayList的底层其实就是一个数组,数组的劣势想必也不用多说,一旦创建,长度无法更改。而ArrayList则可以不停的add或是remove,也可以称之为动态数组。

二、类定义以及成员变量

1、类定义

//1、ArrayList<E>说明此类是支持泛型的
2、继承AbstractList不用多说了,抽象父类中有许多子类可以共用的方法
3、此类实现了RandomAccess接口,说明支持快速随机存取,在实现了该接口的话,那么使用普通的for循环来遍历,性能更高,例如arrayList。而没有实现该接口的话,使用Iterator来迭代,这样性能更高,例如linkedList。
所以这个标记性只是为了让我们知道我们用什么样的方式去获取数据性能更好。
4、Cloneable、Serializable接口则代表可以使用Object.Clone()方法以及可以被序列化
5、这地方还有一个问题,就是ArrayList继承了AbstractList,为啥还要去实现List接口。开发这个的作者已经说了,这是一个错误。。。链接地址:
https://stackoverflow.com/questions/2165204/why-does-linkedhashsete-extend-hashsete-and-implement-sete
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable

2、成员变量

//当对象序列化之后,jvm通过这玩意来比对类的版本
private static final long serialVersionUID = 8683452581122892189L;
//默认容量大小为10
private static final int DEFAULT_CAPACITY = 10;
//空对象数组
private static final Object[] EMPTY_ELEMENTDATA = {};
//默认空对象数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//真正存放元素的数组,并且不需要被序列化
transient Object[] elementData;
//数组的长度
private int size;
    //数组最大容量 
  private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

3、构造函数

    //没有给定初始容量时,默认给10个长度
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//长度>0则按照给定长度创建;长度为0,则为默认空对象数组;<0自然报错了
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
//给定collection时,转换成数组并赋值给elementData
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
if (elementData.getClass() != Object[].class)
//每个集合的toarray()的实现方法不一样,所以需要判断一下,如果不是Object[].class类型,那么就需要使用
ArrayList中的方法去改造一下。并且Arrays.copyOf是浅拷贝,当数组中存放的为对象,并且对象中的属性值有
改变,会影响到之前的数组
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
this.elementData = EMPTY_ELEMENTDATA;
}
}

三、主要方法

ArrayList中提供的add()方法有四种:

1、add(E)

    //直接在list的最后一位添加此元素
解释几个变量:
1、size:当前底层真正存储元素的个数
2、DEFAULT_CAPACITY:10,默认的容量大小
3、elementData.length:底层数组的长度。比如初始化20长度的list,但是里面只存放了5个元素。length为20,size则是5.
public boolean add(E e) {
//确定内部容量是否够了,size是数组中数据的个数,因为要添加一个元素,所以size+1,先判断size+1的这个个数数组能否放得下,就在这个方法中去判断是否数组.length是否够用了。
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData,
minCapacity));
}
//如果当前数组为空,则取minCapacity与10之间大的数为数组容量。如果底层数组不为空,则取minCapacity
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//当elementData为空时,并且minCapacity<10时,直接扩容到10
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
//如果所需的数组容量已经大于当前底层数组的长度,那么需要扩容。
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
//真正的扩容模块
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
//将老容量扩充1.5倍,>>1也就是右移一位,也就是/2,不明白位运算的可以看本人位运算的一篇文章https://www.cnblogs.com/alimayun/p/10693358.html
int newCapacity = oldCapacity + (oldCapacity >> 1);
//如果扩容1.5倍还是小于minCapacity,那么以minCapacity为准
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//如果大于数组最大容量
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
//最大就是给Integer.MAX_VALUE,否则就是MAX_ARRAY_SIZE=Integer.MAX_VALUE - 8。主要作用就是防止溢出
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}

对上面的整个流程总结一遍吧,用中文来说:

先看下流程图

  • 当执行add(E)时,首先将minCapasize=size+1;
  • 当底层的elementData为空时,取minCapasize和10之间的最大值;若elementData不为空,则取minCapasize,将最终的结果赋值给minCapasize;
  • 判断此时minCapasize是否大于elementData.length,如果大于就开始扩容;
  • 获取当前的elementData.length,并扩容1.5倍,扩容完之后与minCapasize比较,取二者的最大值;
  • 获取到二者最大值后,判断该值是否大于MAX_ARRAY_SIZE=Integer.MAX_VALUE - 8,如果大于,要么取Integer.MAX_VALUE,要么取MAX_ARRAY_SIZE;
  • 通过Arrays.copyOf(elementData, newCapacity)方法,创建一个新长度的数组;
  • 将元素E放到size++的索引处,并返回true,结束整个流程

2、add(int index, E element)

    public void add(int index, E element) {
//判断下index是否符合规矩
rangeCheckForAdd(index);
//这个方法前面已经花了很多精力介绍了
ensureCapacityInternal(size + 1);
//将elementData在插入位置后的所有元素往后面移一位。
     //api:
     //public static void arraycopy(Object src,int srcPos,Object dest,int destPos,int length)
     //src:源对象
//srcPos:源对象对象的起始位置
//dest:目标对象
//destPost:目标对象的起始位置
//length:从起始位置往后复制的长度
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
//检查index是否合法
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

3、addAll(Collection)、addAll(index, Collection)

    //其实大体上与add()方法差不多
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew);
//通过上面确定elementData的长度之后,通过System.arraycopy()将c全部添加到原arrayList的后面,可以理解成追加吧
//关于System.arraycopy(),Arrays.copyOf()底层也是调用的这个方法,这是个native方法,所以也不用去关注它是咋实现的吧
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
} public boolean addAll(int index, Collection<? extends E> c) {
rangeCheckForAdd(index); Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount int numMoved = size - index;
//如果index小于size的话,说明ArrayList原元素有一部分需要被覆盖掉
if (numMoved > 0)
System.arraycopy(elementData, index, elementData, index + numNew,
numMoved); System.arraycopy(a, 0, elementData, index, numNew);
size += numNew;
return numNew != 0;
}

4、get()、set()

    public E get(int index) {
//检查索引是否超过范围,如果index为负数,则由底层数组报ArrayIndexOutOfBoundsException
rangeCheck(index); return elementData(index);
}
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
E elementData(int index) {
//如果index为负数,则由底层数组报ArrayIndexOutOfBoundsException
return (E) elementData[index];
}
    public E set(int index, E element) {
//跟get()一样
rangeCheck(index);
//取出对应index位置的元素
E oldValue = elementData(index);
//替换成新元素
elementData[index] = element;
//返回老元素
return oldValue;
}

5、remove(index)、remove(object)、clear()

    public E remove(int index) {
rangeCheck(index); modCount++;
E oldValue = elementData(index); int numMoved = size - index - 1;
//假如当前lsit是这样{0,1,2,3}删除第二个位置的,那么第三个位置的元素需要往左移。当numMoved>0,说明需要移动
if (numMoved > 0)
//0到index的元素是不需要动的,index+1之后的需要移动
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work return oldValue;
} public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
//以index为分界线,0-index的元素不用动,后面的往左移动
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
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; // clear to let GC do its work
}
public void clear() {
modCount++; // clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null; size = 0;
}

6、size()、empty()、contains()、indexOf()、lastIndexOf()、clone()

public int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
//判断是否包含某个对象
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
//这方法很简单,没啥说的
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
//跟indexOf的区别就是一个从前面往后匹配,一个从后往前匹配
public int lastIndexOf(Object o) {
if (o == null) {
for (int i = size-1; i >= 0; i--)
if (elementData[i]==null)
return i;
} else {
for (int i = size-1; i >= 0; i--)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
//克隆一个新的ArrayList
public Object clone() {
try {
//若想调用super.clone(),必须要实现cloneable接口
ArrayList<?> v = (ArrayList<?>) super.clone();
v.elementData = Arrays.copyOf(elementData, size);
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
}

总结:ArrayList是动态数组,优化了数组不能更改长度的缺点,而且继承了数组访问快的优点。缺点就是删除数据时(尤其删除index靠前位置的数据)比较慢。

删除操作时,需要将后面的元素移动空白位置,不慢就怪了。

另外提供了add、get、set等方法来操作数据。另外感兴趣的小伙伴可以自己看下ArrayList的迭代器Itr以及ListItr,这个在AbstractList中有介绍。

走进JDK(六)------ArrayList的更多相关文章

  1. 调试过程中发现按f5无法走进jdk源码

    debug 模式 ,在fis=new FileInputStream(file); 行打断点 调试过程中发现按f5无法走进jdk源码 package com.lzl.spring.test; impo ...

  2. 走进JDK(十)------HashMap

    有人说HashMap是jdk中最难的类,重要性不用多说了,敲过代码的应该都懂,那么一起啃下这个硬骨头吧!一.哈希表在了解HashMap之前,先看看啥是哈希表,首先回顾下数组以及链表数组:采用一段连续的 ...

  3. 走进JDK(八)------AbstractSet

    说完了list,再说说colletion另外一个重要的子集set,set里不允许有重复数据,但是不是无序的.先看下set的整个架构吧: 一.类定义 public abstract class Abst ...

  4. 走进JDK(二)------String

    本文基于java8. 基本概念: Jvm 内存中 String 的表示是采用 unicode 编码 UTF-8 是 Unicode 的实现方式之一 一.String定义 public final cl ...

  5. 走进JDK(七)------LinkedList

    要学习LinkedList,首先得了解链表结构.上篇介绍ArrayList的文章中介绍了底层是数组结构,查询快的问题,但是删除时,需要将删除位置后面的元素全部左移,因此效率比较低. 链表则是这种机制: ...

  6. 走进JDK(五)------AbstractList

    接下来的一段时间重点介绍java.util这个包中的内容,这个包厉害了,包含了collection与map,提供了集合.队列.映射等实现.一张图了解java中的集合类: AbstractList 一. ...

  7. 走进JDK(一)------Object

    阅读JDK源码也是一件非常重要的事情,尤其是使用频率最高的一些类,通过源码可以清晰的清楚其内部机制. 如何阅读jdk源码(基于java8)? 首先找到本地电脑中的jdk安装路径,例如我的就是E:\jd ...

  8. Java集合(六)--ArrayList、LinkedList和Vector对比

    在前两篇博客,学习了ArrayList和LinkedList的源码,地址在这: Java集合(五)--LinkedList源码解读 Java集合(四)--基于JDK1.8的ArrayList源码解读 ...

  9. 【JDK】ArrayList集合 源码阅读

    这是博主第二次读ArrayList 源码,第一次是在很久之前了,当时读起来有些费劲,记得那时候HashMap的源码还是哈希表+链表的数据结构. 时隔多年,再次阅读起来ArrayList感觉还蛮简单的, ...

随机推荐

  1. JVM学习总结(一):Java内存区域

    一.JVM运行时数据区 1.程序计数器: (1)一块较小的线程私有的内存空间. (2)JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(或一个内核) ...

  2. vmdk转qcow2格式

    关闭vm 多文件格式转换成单文件格式vmdk 进入cmd命令行模式的窗口 进入VMware workstations的安装路径下 vmware-vdiskmanager.exe -r "E: ...

  3. 团队第二次 # scrum meeting

    github 本此会议项目由PM召开,召开时间为4-3日晚上9点 召开时长15分钟 任务表格 袁勤 学习SpringBoot https://github.com/buaa-2016/phyweb/i ...

  4. 笔记:python (2015)

    [开发环境]: Python 3.3  http://rj.baidu.com/soft/detail/25283.html  大小:20.2M 版本:3.3.5150 位数:64 更新日期:2014 ...

  5. SpringBoot 之 thymeleaf

    thymeleaf 的maven 配置我们都知道: <dependency> <groupId>org.springframework.boot</groupId> ...

  6. spring 之 注入之 by name or by type, or both ?

    @Autowired 和  @Qualifier 使用xml 注入的时候, 我们可以指定 autowire=“byType” 或“byName” . 但是使用 注解的时候, @Autowired  只 ...

  7. Python第2天

    今天学习的主要内容: pycharm专业版的安装和注册,采用注册码的方式注册. 运算符,+ — * / // % < > <=  >= != <> . 基本数据类型 ...

  8. python使用细节

    python的函数位置参数在调用时可以直接传参,也可以a=5,b=7的形式传参,原以为kw参数才可以. >>> def f(a,b): print a+b >>> ...

  9. CMake Error at cuda_compile_generated_warp.cu.o.cmake:264 (message)

    今天,我来给大家分享一下opencv安装时报的错.然后讲错是怎么解决的. 为啥老是写一些环境搭建的博客?因为环境搭建琐碎而繁杂,希望写下来,帮助大家.让大家少走弯路. 专注主业,专注算法的实现和优化. ...

  10. django在centos部署

    各种坑各种蛋疼,搞了两天终于能用了.整体分2步: 1. 通过uwsgi 启动django项目 在manage.py同级目录,新建uwsgi.ini文件 [uwsgi] # 配置服务器的监听ip和端口, ...