计划先对 Java8 容器的源码进行逐个逐个分析,就当是学习数据结构了。争取一个月看完。
数据结构的定义:
数据之间互相存在的一种或多种特定的关系的 元素的集合。
什么是数据:巧妇难为无米之炊。
数据对象中数据元素之间的关系(逻辑结构):
1、集合结构
2、线性结构
3、树形结构
4、图形结构
物理结构
1、顺序存储结构
2、链式存储结构
抽象数据类型
数据类型:一组性质相同的值的集合及定义在此集合上的一些操作的总称
抽象数据类型:一个数字模型及定义在该模型上的一组操作
线性表(List)
存储方式:
顺序存储方式:存储位置连续,可以很方便 查找元素,但是增加和删除元素比较慢。
在Java中最基础的顺序存储结构:数组
集合类中的ArrayList即为一种线性表。
obj1是 obj2的前驱,obj2是obj1的后继。
存储位置连续,可以很方便计算各个元素的地址
下面对ArrayList中的特性以及常用方法进行分析:
ArrayList源码注释的第一行是:
Resizable-array implementation of the List interface. //List接口的可变 数组实现。
可以看出List底层是 使用数组实现的
从ArrayList源码的顶部注释可以总结出以下几点:
- 底层:ArrayList是List接口的大小可变数组的实现
- 是否允许null:ArrayList允许null元素。
- 时间复杂度:size、isEmpy、get、set、iterator方法都是以固定时间运行,时间复杂度为O(1).add和remove方法需要O(n)时间。
- 容量:ArrayList的容量可以自动增长。
- 是否同步:不同步
先来看看ArrayList的定义:
public class ArrayList extends AbstractList implements List, RandomAccess, Cloneable, java.io.Serializable
从上可以看出 :
ArrayList:支持泛型
extends AbstractList :继承了AbstractList类
implements List:实现了所有列表的功能
RandomAccess,:表明ArrayList支持快速(通常是固定时间)随机访问。
Cloeable:表明可以调用clone方法来返回实例的field-for-field拷贝。
implements java.io.Serilizable:表明该类具有序列化的功能。
从类的定义要能看出其可能继承和拥有的某些方法。
接下来看看在类中定义的一些 域值。
/**
初始化默认容量
*/
private static final int DEFAULT_CAPACITY = 10;
/**
指定该ArrayList容量为0时,返回该空数组
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* The array buffer into which the elements of the ArrayList are stored.
* The capacity of the ArrayList is the length of this array buffer. Any
* empty ArrayList with elementData == EMPTY_ELEMENTDATA will be expanded to
* DEFAULT_CAPACITY when the first element is added.
*/
transient Object[] elementData; // non-private to simplify nested class access
/**
ArrayList实际包含的元素
*/
private int size;
构造方法
方法一: public ArrayList(int initialCapacity) ;Constructs an empty list with the specified initial capacity.
方法二:public ArrayList() ; Constructs an empty list with an initial capacity of ten.
方法三: public ArrayList(Collection<? extends E> c) ;Constructs a list containing the elements of the specified collection, in the order they are returned by the collection’s iterator.
下面分析下上述三种构造方法的源码:
方法一
public ArrayList(int initialCapacity) { //参数 initialCapacity :为赋予数组的初始长度,也就是ArrayList的初始长度
super();
if (initialCapacity < 0) //判断初始长度的正负
throw new IllegalArgumentException("Illegal Capacity: "+ //如果小于0,抛出异常
initialCapacity);
this.elementData = new Object[initialCapacity]; //若不是异常则,对该对象进行赋值
}
这种方法,对 ArrayList 赋予 指定大小的长度,若小于0则抛出异常,否则 直接创造 大小为 InitCapcacity大小的数组(也就是ArrayList).
方法二
public ArrayList() { //无参的构造函数
super();
this.elementData = EMPTY_ELEMENTDATA;//直接将域中创建的空数组赋值给该 数组(ArratList)
}
此种方法,无参的构造函数,直接将 ArrayList赋值为 空数组。
方法三
public ArrayList(Collection<? extends E> c) { //参数为Collection接口型的
elementData = c.toArray(); //将集合转换为数组
size = elementData.length; //求数组的大小
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class); //进行数组的拷贝
}
核心方法
ArrayList中的核心方法主要 覆盖以下功能:
增、删、改、查(结合数据结构看)。
方法名 | 时间复杂度 |
---|---|
get(int index) | O(1) |
add(E e) | O(1) |
add(int index,E element) | O(n) |
– | – |
remove(Int index) | O(n) |
set(int index)– | O(1) |
get(int index)
public E get(int index) {
rangeCheck(index); //进行索引异常检测
return elementData(index); //这种其实就是数组的访问方式
}
private void rangeCheck(int index) { //进行索引大小的判断
if (index >= size) //假如索引大于等于 size;因为 数组的长度是从 0开始的
throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); //抛出异常
}
从代码可以看出,因为ArrayList底层是数组,所以它的get方法非常简单,先是判断一下有没有越界,之后就直接通过数组下标 来获取元素。
add(E e)
public boolean add(E e) { //e待添加的元素
//确认list容量,如果不够,容量加1.注意:只加1,保证资源不被浪费
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e; //将数组最后一个元素赋值为e
return true;
}
从源码中可以看出,add(E e)有两个步骤:
1、空间检查,如果 有需要进行扩容
2、插入元素
扩容 ensureCapacity等方法
//数组容量检查,不够时则进行扩容,只供类内部使
private void ensureCapacityInternal(int minCapacity) { //minCapacity:想要的最小容量
if (elementData == EMPTY_ELEMENTDATA) { //假如之前的arrayList是空的话
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); //取 10 与 minCapacity中的最大值赋值给minCapacity
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0) //确保指定的最小容量>数组缓冲区当前的长度
grow(minCapacity); //扩容
}
//扩容,保证ArrayList至少能存储 minCapacity个元素,逻辑为 newCapacity = OldCapacity +(OldCapacity>>1);即在
//原有的容量基础上增加一半第一次扩容后,如果容量还是小于minCapacity,就将容量扩充为 minCapacity
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length; //获取当前数组容量
int newCapacity = oldCapacity + (oldCapacity >> 1); //扩容。新的容量=当前容量+当前容量/2
if (newCapacity - minCapacity < 0) //如果扩容后的容量还是小于想要的最小容量
newCapacity = minCapacity; //将扩容后的容量再次扩容为想要的最小容量
if (newCapacity - MAX_ARRAY_SIZE > 0) //如果扩容后的容量大于临界值,则进行大容量分配
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity); //进行容量的扩充
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow //如果minCapacity<0,抛出异常
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
看完代码,可以对扩容方法总结如下:
1、进行空间检查,决定是否进行扩容,以及确定最少需要的容量
2、如果确定扩容,就执行 grow(int minCapacity),minCapacity为最少需要的容量。
3、第一次扩容,逻辑为 newcapacity = oldCapacity + (oldCapacity>>1);即在原有的容量基础上增加一半
4、第一次扩容后,如果容量还是小于 minCapacity,就将容量扩充为 minCapacity
5、对扩容后的容量进行判断,如果大于允许的最大容量MAX_ARRAY_SIZE,则将容量再次调整为 MAX_ARRAY_SIZE。至此扩容操作结束。
add(int index,E element)
public void add(int index, E element) {
//越界检查
rangeCheckForAdd(index);
//确定list容量,如果不够,容量加1.注意:只加1,保证资源不被浪费
ensureCapacityInternal(size + 1); // Increments modCount!!
//对数组进行复制处理,目的就是空出index位置插入element,并将index后的元素位移一个位置
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
//将指定的index位置赋值给element
elementData[index] = element;
//实际容量+1
size++;
}
remove(int index)
public E remove(int index) {
//检查索引是否越界。如果参数指定索引index>=size,抛出越界异常
rangeCheck(index);
//结构性修改次数+1
modCount++;
//记录索引为index处的元素
E oldValue = elementData(index);
//删除指定元素后,需要左移的元素的个数
int numMoved = size - index - 1;
//如果有需要左移的元素,就移动(移动后,该删除的元素就已经被覆盖了)
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//size-1,然后将索引 为size-1处的元素置为null.为了让GC起作用,必须显示的为最后一个位置赋 null值
elementData[--size] = null; // clear to let GC do its work
//返回被删除的元素
return oldValue;
}```
看完代码后,可以将ArrayList删除指定索引的元素的步骤总结为:
- 检查索引是否越界。如果参数指定索引 index>=size,抛出一个越界异常
- 将索引大于index的元素 左移一位(左移后,该删除的元素就被覆盖了,相当于被删除了)
- 将索引为size-1处的元素置为null(为了让GC起作用)
注意:为了让GC起作用,必须显示的为最后一个位置赋null值。
** public E set(int index, E element)**
public E set(int index, E element) {
//检查索引是否越界
rangeCheck(index);
//记录被替换的元素
E oldValue = elementData(index);
//替换元素
elementData[index] = element;
//返回被替换的元素
return oldValue;
}