前言
本博客主要讲解自己对ArrayList的底层实现的一些理解,以及实现一个简略的MyArrayList,具体实现方式可能与Java原生ArrayList有所不同。文章最后有代码地址。
目录
一、实现思路以及类的基础结构搭建
二、size()、isEmpty()、toString()、clear()、toArray()方法的实现
三、集合的扩容以及缩小方法的实现
四、iterator()的实现
五、四个add方法的实现
六、四个remove方法的实现
七、get和set方法实现
八、indexOf和lastIndexOf方法的实现
九、sort方法的实现
十、总结
一、实现思路以及类的基础结构搭建
对于ArrayList可以动态调整大小的特点,可以这样来实现:在底层封装一个指定大小的数组,当添加元素的数量到达某个阈值时,创建一个长度更长的数组,将原数组中的数据复制过去,这样就相当于实现了集合的自动扩容。废话不多说,直接上代码。
package com.llg.collection;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;
public class MyArrayList<E> extends AbstractList<E> {
//默认初始容量
private static final int DEFAULT_CAPACITY = 10;
//默认扩容阈值
private static final float EXPANSION_THRESHOLD = 0.75f;
//默认增长因子
private static final float EXPANSION_FACTOR = 0.5f;
//默认缩小阈值
private static final float NARROW_THRESHOLD = 0.2f;
//默认缩小因子
private static final float NARROW_FACTOR = 0.5f;
//存储元素的数组
private Object[] elements;
//list元素个数
private int size;
//该集合的操作次数
private int modifyCount;
public MyArrayList() {
//以默认初始容量创建list
this(DEFAULT_CAPACITY);
}
/**
* 创建具有指定初始容量的集合
* @param capacity
*/
public MyArrayList(int capacity) {
if (capacity < 0) throw new IllegalArgumentException("容量不能为负数!");
elements = new Object[capacity];
}
}
我创建了一个名为MyArrayList的类,继承AbstractList类(主要是为了可以知道List有那些方法,然后快捷键生成方法体),然后定义了几个静态的常量,分别解释一下:
DEFAULT_CAPACITY :默认初始容量,如果创建MyArrayList对象时不指定大小,将会用次值初始化底层数组。
EXPANSION_THRESHOLD :扩容阈值,当 集合元素个数 >= 扩容阈值 * 集合容量 时将会自动扩容
EXPANSION_FACTOR :增长因子,当集合自动扩容时,扩容后的容量=扩容前的容量 * (1+增长因子)
NARROW_THRESHOLD :缩小阈值,当移除集合元素时,当 元素个数 <= 缩小阈值 * 集合容量 时将会自动缩小容量
NARROW_FACTOR :缩小因子,当缩小容量时,扩容后的容量=扩容前的容量 * 缩小因子
紧接着申明了集合的底层数组elements、集合大小size、modifyCount暂时不做讲解、一个使用默认初始化容量初始化底层数组的无参构造器,一个用指定容量初始化底层数组的带参构造器。
二、size()、isEmpty()、toString()、clear()、toArray()方法的实现
接着来实现最简单的五个方法:
/**
* 返回集合元素个数
* @return
*/
@Override
public int size() {
return size;
}
/**
* 返回集合是否为空
* @return
*/
@Override
public boolean isEmpty() {
return size == 0;
}
/**
* 清空集合
*/
@Override
public void clear() {
elements = new Object[DEFAULT_CAPACITY];
size = 0;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder("[");
for (int i = 0; i < size; i++) {
if (i > 0) builder.append(", ");
builder.append(elements[i].toString());
}
builder.append("]");
return builder.toString();
}
/**
* 返回次集合对象的数组表示形式
* @return
*/
@Override
public Object[] toArray() {
//创建一个长度为size的数组
Object[] objects = new Object[size];
//将elements中的元素复制到数组中
for (int i = 0; i < objects.length; i++) {
objects[i] = elements[i];
}
return objects;
}
对于size()和isEmpty()方法不做讲解,因该能看懂吧。
clear()方法直接重新创建一个空数组指向elements,并将size置为0即可。
toString()方法遍历数组,拼接字符串并返回。
toArray()方法由于elements数组的末尾元素很有可能为空,所以必须将已有元素转化成一个新数组返回
三、集合的扩容以及缩小方法的实现
/**
* 判断list是否需要扩容
* @return
*/
private boolean isExpansion() {
return size >= elements.length * EXPANSION_THRESHOLD;
}
/**
* 判断是否需要减少容量
* @return
*/
private boolean isNarrow() {
return elements.length > DEFAULT_CAPACITY && size <= elements.length * NARROW_THRESHOLD;
}
/**
* 对集合进行扩容操作
*/
private void expansion() {
//创建一个新数组,长度为扩容后的长度
Object[] newElement = new Object[(int) (elements.length * (1 + EXPANSION_FACTOR))];
//遍历element,将元素复制到新数组中
for (int i = 0; i < size; i++) {
newElement[i] = elements[i];
}
//用新数组覆盖旧数组完成扩容
elements = newElement;
}
/**
* 对集合进行缩小容量的操作
*/
private void narrow() {
//定义一个新数组,长度为减少容量后的长度
int len = (int) (elements.length * NARROW_FACTOR) < DEFAULT_CAPACITY ? DEFAULT_CAPACITY : (int) (elements.length * NARROW_FACTOR);
Object[] newElements = new Object[len];
//遍历list,将list中的元素复制到新数组中
for (int i = 0; i < size; i++) {
newElements[i] = elements[i];
}
//用新数组覆盖原数组完成减少容量
elements = newElements;
}
isExpansion():将当前集合的大小size与 扩容阈值 * 集合容量 比较,判断是否需要扩容
isNarrow():将当前集合的大小size与 缩小阈值 * 集合容量 比较,判断是否需要缩小容量
expansion():执行扩容操作,扩容后的容量=扩容前的容量 * (1+增长因子)
narrow():执行缩小容量操作,扩容后的容量=扩容前的容量 * 缩小因子
四、iterator()的实现
此方法返回一个用于遍历该集合的迭代器对象,所以必须先自定义一个迭代器类,我定义了一个内部类MyIterator如下
private class MyIterator<T> implements Iterator<T> {
//当前指针指向位置
int cursor;
//使用迭代器修改集合的次数
int iterModifyCount;
public MyIterator() {
iterModifyCount = modifyCount;
}
@Override
public boolean hasNext() {
return cursor < size;
}
/**
* 使指针指向下一个元素并返回当前指向的元素
*
* @return
*/
@Override
public T next() {
//如果在使用迭代器过程中使用了非迭代器的remove方法修改了集合结构则抛出此异常
if (iterModifyCount != modifyCount) throw new ConcurrentModificationException();
return (T) elements[cursor++];
}
/**
* 使用移除当前指针指向的前一个元素
*/
@Override
public void remove() {
if (cursor == 0) throw new IllegalStateException("在调用remove()方法前必须调用next()方法");
//调用集合的remove方法删除指定索引的元素
MyArrayList.this.remove(--cursor);
iterModifyCount++;
}
@Override
public void forEachRemaining(Consumer<? super T> action) {
throw new NullPointerException();
}
}
在这里讲解一下之前在MyArrayList类中声明的属性modifyCount和此处的iterModifyCount,在java的ArrayList中是不允许在使用迭代器时使用集合的add、remove等方法修改集合的,我同样想实现这个效果,所以定义了这两个属性,在获得迭代器对象时,将modifyCount的值赋值给iterModifyCount,然后在调用next方法时判断这两个值是否相等,如果不想等则表示使用了使用集合的add、remove等方法修改了集合,便抛出一个并发修改异常,如果想要删除元素只能调用迭代器的remove方法,此方法会在添加成功后将iterModifyCount的值加一,然后调用MyArrayList.this.remove(--cursor)是会将modifyCount的值加一,所以他们最后还是相等,forEachRemaining()方法没有予以具体实现。
五、四个add方法的实现
void add(int index, E element):将指定元素插入到指定索引位置。
boolean add(E element):将指定元素追加到集合末尾,返回是否插入成功。
boolean addAll(Collection<? extends E> c):将指定集合中的所有元素追加到此集合尾部,如果插入了元素返回true。
boolean addAll(int index, Collection<? extends E> c):将指定集合中的所有元素插入到此集合指定索引位置,如果插入了元素返回true。
/**
* 向list指定位置添加一个元素
*
* @param index
* @param element
*/
@Override
public void add(int index, E element) {
//判断索引是否越界
if (index < 0 || index > size) throw new IndexOutOfBoundsException();
//判断是否需要扩容
if (isExpansion()) expansion();
//反向遍历数组,将数组每个元素向后移动一个位置,直到找到index对应的位置,将element插入
for (int i = size; i >= 0; i--) {
//判断当前索引和index是否匹配,如果匹配则直接插入element,否则将前一个元素移至当前位置
if (index == i) {
elements[i] = element;
break;
} else elements[i] = elements[i - 1];
}
//list大小加一
size++;
//对该集合的操作次数加一
modifyCount++;
}
来看第一个add方法,在指定索引位置插入指定元素,首先判断索引是否越界,然后调用之前定义的判断是否扩容的isExpansion()方法判断是否需要扩容,需要则直接调用expansion()方法,然后将元素插入数组,后面的元素一次往后移动,最后集合大小加一,对集合的操作次数加一。
/**
* 向list中添加一个元素,添加到末尾
*
* @param element
* @return
*/
@Override
public boolean add(E element) {
add(size, element);
return true;
}
第二个add方法,在尾部追加指定元素,由于size始终等于elements的最大有效索引+1,所以调用add(size, element);相当于在集合尾部追加。
/**
* 将指定集合中的元素添加到list末尾
*
* @param c
* @return
*/
@Override
public boolean addAll(Collection<? extends E> c) {
boolean isModify = false;
//遍历集合,将集合中的元素放入list
for (E t : c) {
add(size, t);
isModify = true;
}
return isModify;
}
第三个add方法与第二个类似,只不过遍历集合c,将所有元素追加到此集合尾部,然后定义了一个变量isModify表示是否修改了次集合,如果集合c为空,那么isModify = true;永远不会被执行,则返回false,否则返回true;
/**
* 将指定集合中的元素添加到list指定位置
*
* @param index
* @param c
* @return
*/
@Override
public boolean addAll(int index, Collection<? extends E> c) {
//判断索引是否越界
if (index < 0 || index > size) throw new IndexOutOfBoundsException();
//遍历集合,将集合中的元素添加到list指定位置
//获得集合迭代器
Iterator iterator = c.iterator();
boolean isModify = false;
for (int i = 0; i < c.size(); i++) {
add(index + i, (E) iterator.next());
isModify = true;
}
return isModify;
}
第四个add方法,将指定集合中的所有元素添加到次集合的指定索引位置。首先判断索引是否越界,然后遍历集合c将c中的所有元素复制到此集合,由于是插入整个集合,那么c中的第一个元素将插入到index的位置,第二个元素插入到index+1的位置,一次类推,因此这里(add(index + i, (E) iterator.next());)使用了index + i。
六、四个remove方法的实现
E remove(int index):移除并返回指定索引位置的元素
boolean remove(Object o):移除指定的元素,没找到此元素返回false
boolean removeAll(Collection<?> c):移除次集合中包含在集合c中的所有元素
removeIf(Predicate<? super E> filter):移除此集合中符合条件的元素
/**
* 删除并返回指定索引的元素
* @param index
* @return
*/
@Override
public E remove(int index) {
//判断索引是否越界
if (index < 0 || index > size - 1) throw new ArrayIndexOutOfBoundsException("索引越界异常:index=" + index);
E result = (E) elements[index];
//遍历数组,将元素前移
for (int i = index; i < size; i++) {
elements[i] = elements[i + 1];
}
//list大小减一
size--;
//集合操作次数加一
modifyCount++;
//判断是否需要减少容量
if (isNarrow()) narrow();
return result;
}
第一个remove方法,移除并返回指定索引位置的元素。首先判断索引是否越界,然后记录index索引的值,将index后边的元素一次前移,然后集合大小减一,对集合的操作加一,最后判断是否需要减少容量。
/**
* 移除list中的指定元素
* @param o
* @return
*/
@Override
public boolean remove(Object o) {
//遍历list,找到指定元素索引并移除
for (int i = 0; i < size; i++) {
if (elements[i] == o) {
remove(i);
return true;
}
}
return false;
}
第二个remove方法,移除指定的元素,没找到此元素返回false,直接遍历此集合,找到该元素的索引,然后通过索引删除元素。
/**
* 删除指定集合中的元素,如果成功删除了一个元素则返回true,否则返回false
* @param c
* @return
*/
@Override
public boolean removeAll(Collection<?> c) {
//定义标记,表示是否删除了元素
boolean mark = false;
//遍历集合,删除相同的元素
for (Object o : c) {
if (remove(o)) mark = true;
}
return mark;
}
第三个remove方法,删除此集合中包含指定集合c中的所有元素,直接遍历集合c,然后调用第二个remove方法删除
/**
* 删除满足条件的元素
* @param filter
* @return
*/
@Override
public boolean removeIf(Predicate<? super E> filter) {
//定义标记,表示是否删除了元素
boolean mark = false;
//遍历list,删除满足条件的元素
Iterator<E> iterator = iterator();
while (iterator.hasNext()) {
E item = iterator.next();
if (filter.test(item)) {
iterator.remove();
mark = true;
}
}
return mark;
}
最后一个remove方法,删除此集合中符合条件的元素。先获得此集合的迭代器遍历,将每个元素依次作为参数调用filter.test(item)方法,如果返回true,就删除该元素。
七、get和set方法实现
E get(int index):获取指定索引位置的元素。
E set(int index, E element):修改指定索引元素的值,并返回修改前的值
/**
* 通过索引获取元素
* @param index
* @return
*/
@Override
public E get(int index) {
if (index < 0 || index > size - 1) throw new ArrayIndexOutOfBoundsException("索引越界异常:index=" + index);
return (E) elements[index];
}
先来看get方法,首先判断索引是否越界,然后直接返回elements[index],由于elements是Object类型的数组,所以需要向下转型。
/**
* 设置指定索引的元素,并返回之前的元素
* @param index
* @param element
* @return
*/
@Override
public E set(int index, E element) {
if (index < 0 || index > size - 1) throw new ArrayIndexOutOfBoundsException("索引越界异常:index=" + index);
E old = (E) elements[index];
elements[index] = element;
return old;
}
再来看set方法,同样是先判断索引是否越界,然后记录旧值,修改值,最后返回旧值。
八、indexOf和lastIndexOf方法的实现
int indexOf(Object o):返回指定元素在此集合中第一次出现的索引,如果集合中没有此元素返回-1
lastIndexOf(Object o):返回指定元素在此集合中最后一次出现的索引,如果集合中没有此元素返回-1
/**
* 返回指定元素在列表中第一次出现的索引,如果不存在则返回-1
* @param o
* @return
*/
@Override
public int indexOf(Object o) {
int index = -1;
//遍历element找到元素对应索引
for (int i = 0; i < size; i++) {
if (elements[i] == o) {
index = i;
break;
}
}
return index;
}
直接遍历elements,如果找到和此元素相等的元素,就记录索引值,然后返回索引,没找到则返回-1
/**
* 返回指定元素在列表中最后一次出现的索引,如果不存在返回-1
* @param o
* @return
*/
@Override
public int lastIndexOf(Object o) {
int index = -1;
//反向遍历element找到元素对应索引
for (int i = size - 1; i >= 0; i--) {
if (elements[i] == o) {
index = i;
break;
}
}
return index;
}
在来看lastIndexOf,实现思路和indexOf基本一致,只不过遍历elements时反向遍历。
九、sort方法的实现
void sort(Comparator<? super E> c):通过指定的比较器,对此集合进行排序。
/**
* 通过传入的比较器将集合排序
*
* @param c
*/
@Override
public void sort(Comparator<? super E> c) {
//使用冒泡排序
for (int i = 0; i < size - 1; i++) {
for (int j = 0; j < size - 1 - i; j++) {
if (c.compare((E) elements[j], (E) elements[j + 1]) > 0) {
Object temp = elements[j];
elements[j] = elements[j + 1];
elements[j + 1] = temp;
}
}
}
}
我在这里使用了冒泡排序,也可以使用其他的排序方法。通过传入的比较器依次对相邻的两个元素进行比较,如果比较器返回值大于0就交换两个元素的位置。
十、总结
ArrayList的实现原理要理解他是怎么自动扩容的,理解之后这些方法还是比较简单的。首先在底层创建一个数组,添加元素时,将集合的元素个数与数组的长度进行比较,如果达到阈值,就扩容,扩容其实就是创建一个长度更长的数组,然后将原数组中的元素复制到新数组中,相反,删除元素时判断是否需要减少容量。还有一些比较简单的方法没有提到,可以到我的github查看。