面试题经常会问到LinkedList与ArrayList的区别,与其背网上的废话,不如直接撸源码!

  文章源码来源于JRE1.8,java.util.ArrayList

  既然是浅析,就主要针对该数据结构的内部实现原理和部分主要方法做解释,至于I/O以及高级特性就暂时略过。

变量/常量

  首先来看定义的(静态)变量:

class ArrayList2<E>
//extends AbstractList<E>
//implements RandomAccess, Cloneable, java.io.Serializable
{
private static final long serialVersionUID = 8683452581122892189L;
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;
}

  这里在一开始定义了6个变量,其中第一个跟序列化相关不用管,其余5个依次解释一下:

DEFAULT_CAPACITY:代表容器ArrayList的初始化默认大小

Object[] EMPTY_ELEMENTDATA:一个空数组,在某些方法调用后(例如removeAll)会用到

Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA:默认空数组,未传参初始化时默认为这个

Object[] elementData:保存着当前ArrayList的内容,该变量被标记为序列化忽略对象

int size:很明显,当前ArrayList大小

  需要注意的是,其中几个被标记为static final变量。

构造函数

  看完变量,接下来看构造函数部分,构造函数有3个重载版本,分别阐述如下。

1、无参版本

    public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

  如果不传任何参数直接初始化一个ArrayList,会得到上面默认的空数组。

2、int版本

    public ArrayList(int initialCapacity) {
// 正常情况会初始化一个指定大小的数组
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
}
// 传0在实现上与不传是一样的
else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
}
// 乱传就抛异常
else {
throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
}
}

  这基本上最普遍的情况,可以看出内部实现就是普通的数组。

3、Collection版本

    public ArrayList(Collection<? extends E> c) {
// 将集合转换为数组并赋给本地变量
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// 传空集合相当于空数组
this.elementData = EMPTY_ELEMENTDATA;
}
}

  如果初始化传入一个集合,会将此集合作为ArrayList的初值。

  这里存在一个bug,即toArray方法返回的不一定是Object,虽然默认情况下是,但是如果被重写就不一定了。

  详细问题可见另一位的博客:http://blog.csdn.net/gulu_gulu_jp/article/details/51457492

  如果返回不是Object类型,会做向上转型。

方法

  接下来看看常用的方法。

首先是get/set方法:

    public E get(int index) {
rangeCheck(index);
return elementData(index);
}
    public E set(int index, E element) {
rangeCheck(index); E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}

  可以看出十分简单暴力,首先会进行范围检查,然后返回/设置对应index的元素。

  简单看一下rangeCheck:

    private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

  也十分简单,主要是判断所给的索引index是否大于数组的大小size,否则抛出异常。

  获取对应的值时,没有直接用elementData[index],而是用了一个方法elementData(),看着有点混,看一下方法定义:

    E elementData(int index) {
return (E) elementData[index];
}

  方法其实只是对取出来的值进行了类型转换,保证了返回类型的准确。

接下来是add/remove方法

  这两个方法都有重载版本,但是并不复杂,而且都用的比较多。

  首先看add的一个参数版本,会在尾部插入给定元素。

    public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}

  稍微讲下源码注释的modCount,这个变量来源于java.util.AbstractList,专门用来计算容器被改动的次数,对于我这种菜鸟使用者来说没啥用。

  这里会首先检测下容器的容量,然后在尾部加入元素,并将size加1。

  看看ensureCapacityInternal方法:

    private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}

  原来这是一个皮包函数,当数组元素为空时,会进行参数修正,由于容器的默认大小为10,所以不会对10以下的容量进行检测。

  修正后,将10或者比10大的形参传入ensureExplicitCapacity进行检测:

    private void ensureExplicitCapacity(int minCapacity) {
modCount++;
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}

  这看起来也像个皮包方法,不过并不是,如果当前容器大小已经达到上限,会调用grow进行扩容:

    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);
}

  其实这里的形参名字我觉得不是特别好,应该叫最小所需容量,即minNeededCapacity。

  这里首先会获取当前容器大小,并进行扩容,这里的扩容是这样算的:

  oldCapacity + (oldCapacity >> 1)

  也就是如果之前为10,那么新容量为10 + Math.floor(10/2) = 15。

  得到新容量后,会与传进来的 所需容量进行对比,如果还不够,那就干脆取所需容量为新容量。

  第二个if是判断扩容后的容量是否大于最大(数组可达)整数,看下MAX_ARRAY_SIZE变量定义就明白了:

    /**
* The maximum size of array to allocate.
* Some VMs reserve some header words in an array.
* Attempts to allocate larger arrays may result in
* OutOfMemoryError: Requested array size exceeds VM limit
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

  这里的注释有必要看一眼,简单讲就是有些JVM会在数组中加入一些东西,所以实际上数组大小是比理论上小一点的。这个很容易理解的,比如电脑硬盘,容量100G,可用容量其实会打个折扣,一个道理的。

  为了完整,所以也看一下hugeCapacity函数的内部实现:

    private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}

  参数检测挺好玩,内部使用的函数还怕传入负数。

  这里会将所需容量与最大可用安全容量作比较,如果实在没办法,就将容量设置为最大可用容量,至于这里会不会出问题我也不知道。

  回到grow方法,得到新的容量后,会调用Arrays.copyOf方法,这个方法是包内另一个类的方法,内部实现是调用System.arraycopy直接进行内存复制,效率很高,最后返回一个新数组,size为加大后的容量。

  

  接下来看第二个重载的add方法:

    public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}

  这里的检测不太一样,多了一步,不过看一眼方法就明白了:

    private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

  由于这个重载方法是插入,所以需要进行数值检测,如果插入索引大于数组大小或者小于0,抛个异常。

  接下来是常规的容量检测。

  下一步的方法就是之前提到的System.arraycopy,该方法会将索引+1后面的元素全部复制到源数组,举个简单的例子:

  如果原数组为[1,2,3,4],假设索引为1,经过这一步,数组会变为[1,2,2,3,4]。

  最后是将对应索引的值赋为给定值,size++。

  可以看出,在数组中间插入一个元素是非常耗时的,会变动索引后面的每一个数组元素。

  接下来是remove,这个方法也有2个重载,一个是删除给定索引,一个是删除给定元素:

    public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
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
return oldValue;
}

  没啥好讲的,忽略检测,一句话概括就是将对应索引-1的所有元素复制到原数组,然后size-1,并将末尾元素置null让GC进行回收,最后返回删除元素。

    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进行了特殊处理,这个奇怪的东西只能用==来进行比较。

  总体来讲就是遍历数组,如果找到了匹配元素,进行fastRemove,删除成功返回true,否则返回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
}

  跟第一个重载的remove很相似,只是移除了范围检测与返回值的处理,更快一些。

  其余的方法大多数是上面的变种,没什么研究的必要了,有兴趣的可以自行阅读源码。

浅析Java源码之ArrayList的更多相关文章

  1. 浅析Java源码之LinkedList

    可以骂人吗???辛辛苦苦写了2个多小时搞到凌晨2点,点击保存草稿退回到了登录页面???登录成功草稿没了???喵喵喵???智障!!气! 很厉害,隔了30分钟,我的登录又失效了,草稿再次回滚,不客气了,* ...

  2. 浅析Java源码之HashMap

    写这篇文章还是下了一定决心的,因为这个源码看的头疼得很. 老规矩,源码来源于JRE1.8,java.util.HashMap,不讨论I/O及序列化相关内容. 该数据结构简介:使用了散列码来进行快速搜索 ...

  3. Java源码阅读ArrayList

    1简介 public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAc ...

  4. Java源码之ArrayList分析

    一.ArrayList简介 ArrayList底层的数据结构是数组,数组元素类型为Object类型,即可以存放所有类型数据. 与Java中的数组相比,它的容量能动态增长.当创建一个数组的时候,就必须确 ...

  5. Java源码-集合-ArrayList

    基于JDK1.8.0_191 介绍   在Java中,对于数据的保存和使用有多种方式,主要的目的是以更少的资源消耗解决更多的问题,数组就是其中的一种,它的特点是所有的数据都保存在内存的一段连续空间中, ...

  6. Java源码之ArrayList

    本文源码均来自Java 8 总体介绍 Collection接口是集合类的根接口,Java中没有提供这个接口的直接的实现类.Set和List两个类继承于它.Set中不能包含重复的元素,也没有顺序来存放. ...

  7. 浅析Java源码之HttpServlet

    纯粹是闲的,在慕课网看了几集的Servlet入门,刚写了1个小demo,就想看看源码,好在也不难 主要是介绍一下里面的主要方法,真的没什么内容啊~ 源码来源于apache-tomcat-7.0.52, ...

  8. 浅析Java源码之Math.random()

    从零自学java消遣一下,看书有点脑阔疼,不如看看源码!(๑╹◡╹)ノ""" ​ JS中Math调用的都是本地方法,底层全是用C++写的,所以完全无法观察实现过程,Jav ...

  9. 浅析Java源码之HashMap外传-红黑树Treenode(已鸽)

    (这篇文章暂时鸽了,有点理解不能,点进来的小伙伴可以撤了) 刚开始准备在HashMap中直接把红黑树也过了的,结果发现这个类不是一般的麻烦,所以单独开一篇. 由于红黑树之前完全没接触过,所以这篇博客相 ...

随机推荐

  1. Spring @RequestParam乱码问题

    在网上找了很多资料才找到解决的方法,通过URL传递命名参数,后台接收的却是乱码解决方法如下: 方法一:将接收的参数重新编码 @RequestMapping(value="/handle&qu ...

  2. jmeter按比例执行业务场景

    可用函数 __counter实现: 函数助手中 找到 __counter,如 ${__counter(false,num)},功能简介 ---- 参数为true,每个用户有自己的计数器 ---- 参数 ...

  3. 内核对象 windows操作系统

    问题: 什么是内核对象? 答:内核对象实际上时由内核分配的一块内存,而且只能由内核来访问.它时一个数据结构,成员包含有关于该对象的信息.一些成员对于所有对象类型都是一样的,比如对象名称.安全描述.使用 ...

  4. java学习——java按值传递和按址传递

    先复制一个面试/笔试的题: 当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递? 答案: 是值传递.Java语言的方法调用只支持参 ...

  5. 使用千位分隔符(逗号)表示web网页中的大数字

    做手机端页面我们常常遇到数字,而在Safari浏览器下这些数字会默认显示电话号码,于是我们就用到了补坑的方法加入<meta>标签: <meta name="format-d ...

  6. 自适应 Tab 宽度可以滑动文字逐渐变色的 TabLayout(仿今日头条顶部导航)

    TabLayout相信大家都用过,2015年Google大会上发布了新的Android Support Design库里面包含了很多新的控件,其中就包含TabLayout,它可以配合ViewPager ...

  7. 深入理解C# 静态类与非静态类、静态成员的区别 [转载]

    静态类 静态类与非静态类的重要区别在于静态类不能实例化,也就是说,不能使用 new 关键字创建静态类类型的变量.在声明一个类时使用static关键字,具有两个方面的意义:首先,它防止程序员写代码来实例 ...

  8. Python自学笔记-字符串编码(来自廖雪峰的官网Python3)

    感觉廖雪峰的官网http://www.liaoxuefeng.com/里面的教程不错,所以学习一下,把需要复习的摘抄一下. 以下内容主要为了自己复习用,详细内容请登录廖雪峰的官网查看.   1.理解变 ...

  9. 小程序解释HTML富文本的两种办法

    今天写着着代码,读取数据库的内容时突然跳出"<span>.<p>. "这些HTML标签.字符,吓一跳:本来如果是写HTML.JS倒也没什么,但是我在写小程序 ...

  10. IBM Minus One 简单字符处理

    IBM Minus One Time Limit: 2 Seconds      Memory Limit: 65536 KB You may have heard of the book '2001 ...