数据存储——实现ArrayList
本章节的源代码位于gitee上,想要下载的请点击实现ArrayList
ArrayList简介
Java提供了数据存储的集合List,主要用于存储单值。在List集合中有两个常用的数据存储类,一个是LinkedList,它是以链表的方式进行存储的,关于链表的相关知识点可以去查看我这两个博客数据存储——双向链表和数据存储——单向链表。还有一个就是ArrayList,这个类在开发中的使用占比达90%以上,与LinkedList不同的是,ArrayList使用的是数组进行数据存储。它的有点在于可以直接通过数组下标查询到数据,可以说在查询上的时间复杂度为O(1)。因此基于这个特点,这一章节来详细讲讲。通过我们自己手写的方式来模拟ArrayList。
ArrayList实现
ArrayList作为数据存储的类,一定会提供增删改查的方法。但是首先需要说明的一点是,由于它是基于数组的方式来完成数据存储的,那么我们就需要开辟数组,而不能像LinkedList一样,使用一次在创建一次。同时需要考虑的问题就是如果数组开辟的空间不足,该如何扩容的问题。那么我们就从创建ArrayList开始吧。
创建ArrayList与存储设计
由于在面试中常被问到ArrayList,所以这一次设计就完全按照ArrayList的源码来。首先看看Java提供的ArrayList在实例化的时候都做了些什么。
//有参构造
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);
}
}
//无参构造
public ArrayList() {this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;}
这里就只讨论这两种方式,当前还有ArrayList(Collection<? extends E> c)
这里就先不提。如果有兴趣的朋友可以自行去查询,原理类似,都是通过传入数据的大小进行开辟数组空间。上述的两个构造方法中有一些默认值,它们分别是
private static final Object[] EMPTY_ELEMENTDATA = {};//空数组
transient Object[] elementData;//使用中的数组
这是我们就有一个疑问,默认的是一个空数组啊,我们要存储数据该怎么存储呢?接下来就简单看看添加数据是如何操作的吧,看看有没有操作数组的方法。
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1;
}
private Object[] grow() {
return grow(size + 1);
}
private Object[] grow(int minCapacity) {
return elementData = Arrays.copyOf(elementData, newCapacity(minCapacity));
}
private int newCapacity(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);//数组扩充
if (newCapacity - minCapacity <= 0) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
return Math.max(DEFAULT_CAPACITY, minCapacity);//数组初始化
if (minCapacity < 0)
throw new OutOfMemoryError();
return minCapacity;
}
return (newCapacity - MAX_ARRAY_SIZE <= 0) ? newCapacity : hugeCapacity(minCapacity);
}
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
private static final int DEFAULT_CAPACITY = 10;
这个时候我们就可以看到这个数组的初始化是在什么时候开始的了,通过添加数据,然后判断数组为空,于是实例化一个长度为10的数组。这里主要的判断依据在于数组长度>>1,因为1>>1还是为0,所有就有了下面的return Math.max(DEFAULT_CAPACITY, minCapacity);
因此,到这里我们可以知道了,ArrayList的默认大小为10,每次扩充为原来数组长度的50%。
看上述的一些代码我们可能会有些难懂,接下来我将自己设计一个ArrayList,通过我的代码,然后再去看可能会有更好的理解。我的代码可能会和源代码有所不同,请注意。重点是了解ArrayList的原理和相关的思想。
interface IList<E>{
boolean add(E e);
}
class ArrayListImpl<E> implements IList<E>{
private Object elementData[];//数组
private int listLength;//数组中数据长度
private final int DEFAULTLENGTH = 10;
//无参构造
public ArrayListImpl(){
this(0);
}
public ArrayListImpl(int size){
if(size>=0){
this.elementData = new Object[size];
} else {
throw new IllegalArgumentException("你输入的数据错误"+size);
}
}
private Object[] newCapacity(E e){
if(this.elementData.length==0){
this.elementData = new Object[this.DEFAULTLENGTH];//初始化默认值
} else {
int oldCapacity = this.elementData.length;//获取数据原始长度
int newCapacity = oldCapacity>>1;//增加的长度
this.elementData = Arrays.copyOf(elementData,oldCapacity+newCapacity);//扩容
}
this.elementData[this.listLength++] = e;//添加数据
return this.elementData;
}
@Override
public boolean add(E e) {
if(this.elementData.length==this.listLength){//数组已经满载了,需要开辟新的长度
newCapacity(e);
} else {
this.elementData[this.listLength++] = e;//添加数据
}
return true;
}
}
在我自己写的这个类中,将大量的操作进行了简化,方便初学者理解。
数据读取
在ArrayList之中是没有提供数据输出的方法有很多种,如:toArray()、get(int index)、iterator()等。首先我们还是看看源代码中对这些方法是如何实现的。
public E get(int index) {
Objects.checkIndex(index, size);//检查 index是否在 0 (含)到size(不包括)范围内。
return elementData(index);//返回结果
}
public Object[] toArray() {
return Arrays.copyOf(elementData, size);
}
这两个没有什么可以说的,主要来看看iterator()这个方法。如果感觉看这个源代码有点难懂的可以直接跳过这段代码,直接看我是如何实现的即可,用最简单的放来实现。
public Iterator<E> iterator() {
return new Itr();
}
private class Itr implements Iterator<E> {
int cursor;
int lastRet = -1;
int expectedModCount = modCount;
Itr() {}
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
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];
}
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();
}
}
@Override
public void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
final int size = ArrayList.this.size;
int i = cursor;
if (i < size) {
final Object[] es = elementData;
if (i >= es.length)
throw new ConcurrentModificationException();
for (; i < size && modCount == expectedModCount; i++)
action.accept(elementAt(es, i));
// update once at end to reduce heap write traffic
cursor = i;
lastRet = i - 1;
checkForComodification();
}
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
自定义iterator()方法,该方法需要一个类继承Iterator接口,并实现该接口中的两个方法,如果你想实现四个也是可以的。上述代码就实现了四个。
public Iterator<E> iterator() {
return new Ite();
}
private class Ite implements Iterator{
int len=-1;
@Override
public boolean hasNext() {
return this.len != listLength;
}
@Override
public Object next() {
if(len<listLength){
return elementData[++len];
}
return null;
}
}
该代码的简洁度更高,更加利于理解,初学者适合看这种代码,而想要深入理解的可以直接看源码。
进行测试
public class SimulateArrayList {
public static void main(String[] args) {
IList<String> list = new ArrayListImpl<>();
list.add("A");
list.add("B");
list.add("C");
Iterator iterator = list.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6P3ujmvS-1601964130367)(C:\Users\C3H2\AppData\Roaming\Typora\typora-user-images\image-20201006135605844.png)]
至于修改和删除数据,这里就不在细说了,原理类似,有兴趣的朋友可以直接查看源码。