本篇博客所涉及到的代码,均已上传到github:
项目github链接
本篇博客涉及代码github链接
本篇博客要点如下:
一. 数组简介
众所周知,ArrayList的底层是数组.
因此,在实现ArrayList之前,我们有必要对数组做一个基本的了解
数组概念
数组是用于储存多个相同类型数据的有限序列
从概念中,我们能够得到数组具有以下特性:
1.数据类型相同 : 这意味着内存分配时,数组里的每个元素占据相同的内存空间
2.有限 : 数组的元素个数有限
3.序列 : 顺序性,除了首尾元素,每个元素都有唯一的直接前驱和直接后继
数组存储结构
数组的存储可以用以下图片来直接描述:
从图片中,我们能够直观的看到,数组的元素在内存中连续,只存储数据,不存储地址.
实际上: 当我们执行new 操作创建数组时, 它存储在堆中. 同时还在栈内存中定义一个特殊的变量,这个变量的取值等于数组在堆内存中的首地址,栈中的这个变量就成了数组的引用变量,引用变量实际上保存的是数组在堆内存中的地址
那么这种存储结构具有什么优点和缺点呢?
优点:
1.根据索引查询性能快
假设我们要查询第i个索引的数据.
只需要使用数组的首地址 + i * 每个元素在内存中占用的空间,时间复杂度为T(n)=O(1)
2.节约存储空间
存储时只存储数据项,不存储地址
缺点:
1.插入删除成本高
在某个位置插入删除元素,需要修改该位置及之后全部数据的索引,
平均每次插入删除需要操作(1 + n) / 2次,时间复杂度T(n)=O(n)
2.按值查询效率低
数组的按值查询需要逐个遍历,时间复杂度T(n)=O(n)
3.数组没有存储满时,浪费存储空间
假设我们定义了100个元素的数组,在堆中会为这个数组开辟100个内存空间,
但如果我们只存储了10个数据,相当于剩下的90个空间会被浪费
二.使用Java代码实现ArrayList
基于上述的了解: 我们可以开始试着手写一个简单的ArrayList
首先明确, 集合的常用操作:
集合大小,非空判断,增加元素,删除元素,修改元素,获取元素
我们把上面列举出来的每一种操作封装为一个方法,因此可以得到一个接口,如下:
List接口
public interface List {
// 返回线性表的大小,即数据元素的个数
int size();
// 返回线性表序号为i的数据元素
Object get(int i);
// 判断线性表是否包含数据元素e
boolean contains(Object e);
// 线性表为空返回true,否则返回false
boolean isEmpty();
// 返回数据元素e在线性表中的序号
int indexOf(Object e);
// 将数据元素e插入到线性表中的i号位置
void add(int i, Object e);
// 将元素e插入到线性表末尾
void add(Object e);
// 将元素e插入到元素obj之前
boolean addBefore(Object obj, Object e);
// 将元素e插到元素obj之后
boolean addAfter(Object obj, Object e);
// 删除线性表中序号为i的元素,并返回
Object remove(int i);
// 删除线性表中第一个与a相同的元素
boolean remove(Object e);
// 替换线性表中序号为i的数据元素为e,返回原数据元素
Object replace(int i, Object e);
}
实现ArrayList
除了List接口中的方法,我们还需要两个成员变量来表示
ArrayList的底层存储结构和大小
private Object[] elementData; //底层是一个数据,没有确定长度
private int size; // 不是数组分配了几个空间,而是元素的个数
构造方法
我们常用的ArrayList构造方法有两种:
第一种初始化的时候指定大小
第二种初始化的时候不指定大小,默认的大小为10
基于此,我这里提供了如下的两个构造方法
public ArrayList(int intialCapacity) {
// 给数组分配指定数量的空间
elementData = new Object[intialCapacity];
// 指定顺序表的元素个数,默认为0
// size = 0;
}
public ArrayList() {
// 没有指定长度,默认长度为10
this(10);
// 没有指定长度,长度为0
elementData = new Object[]{10};
}
获取集合大小
成员变量size表示集合大小,返回它即可
@Override
public int size() {
return size;
}
获取集合元素
获取集合指定索引的元素
@Override
public Object get(int i) {
// 对输入参数进行判断(以免引起数组越界)
if (i < 0 || i >= size) {
throw new RuntimeException("数组索引越界异常: " + i);
}
return elementData[i];
}
获取指定元素第一次出现的索引
@Override
public int indexOf(Object e) {
for (int i = 0; i < size; i++) {
// 要首先判断输入参数为null的情况,否则可能会引起空指针异常
if (e == null) {
if (elementData[i] == null) {
return i;
}
} else {
if (e.equals(elementData[i])) {
return i;
}
}
}
// 遍历结束后还没有找到,返回-1
return -1;
}
判断集合是否包含指定元素
@Override
public boolean contains(Object e) {
// 这里我最初是采用遍历的方法进行判别的,后面想到,我在之前实现的indexOf方法
// 试想,若集合包含某个元素,则该元素在集合里对应的索引应该大于等于0
// 所以,我们在这里复用上面的indexOf方法,实现如下:
return indexOf(e) >= 0;
}
判断集合是否为空
@Override
public boolean isEmpty() {
// 集合为空,代表集合大小为0
return size == 0;
}
集合数据添加
添加元素有两种情况,一种是添加到集合的最后,一种是添加到集合指定索引位置
// 先来看比较简单的插入到最后的情况
// 首先要判断集合是不是已经满了,如果满了的话,要对数组进行扩容,不然会出现数组越界问题
@Override
public void add(Object e) {
// 这里采用Arrays的数组拷贝方法来扩容, 让elementData指向扩容后的数组(我这里扩容二倍)
if (size == elementData.length) {
elementData = Arrays.copyOf(elementData, elementData.length * 2);
}
// 添加元素到最后
elementData[size] = e;
// 元素个数+1
size ++;
}
// 添加到指定索引, 这里除了要给指定索引的元素赋值之外,还需要把这个元素之后的元素全部移位
@Override
public void add(int i, Object e) {
if (size == elementData.length) {
elementData = Arrays.copyOf(elementData, elementData.length * 2);
}
// 添加元素到最后
elementData[size] = e;
// 元素个数+1
size ++;
// 后移i后面元素,从最后一个元素开始
for (int j = size; j > i; j--) {
elementData[j] = elementData[j - 1];
}
// 给数组的第i个位置赋值
elementData[i] = e;
}
集合数据删除
/*
删除集合对应索引的元素,返回旧值
*/
@Override
public Object remove(int i) {
// 索引取值应该合理,否则会引起数组越界
if (i < 0 || i >= size) {
throw new IndexOutOfBoundsException("指定的索引越界,集合大小为:" + size + ",您指定的索引大小为:" + i);
}
Object oldValue = elementData[i];
int numMoved = size - i - 1;
if (numMoved > 0)
System.arraycopy(elementData, i+1, elementData, i,
numMoved);
elementData[--size] = null; // 释放空间
return oldValue;
}
/*
删除集合里指定的元素
*/
@Override
public boolean remove(Object e) {
// 遍历集合,找寻元素
for (int i = 0; i < size; i++) {
// 首先仍要考虑空值的情况,防止空指针异常
if (e == null) {
if (elementData[i] == null) {
remove(i); // 调用上面写的方法删除集合元素
return true;
}
} else {
if (e.equals(elementData[i])) {
remove(i);
return true;
}
}
}
return false;
}
集合数据替换
/*
将集合指定索引的元素替换为特定的值,并返回原数据元素
*/
@Override
public Object replace(int i, Object e) {
if (i < 0 || i >= size) {
throw new ArrayIndexOutOfBoundsException("索引越界!");
}
Object oldValue = elementData[i];
elementData[i] = e;
return oldValue ;
}
如上,我们通过自己的方式实现了ArrayList的基本功能!