对于java开发者来说,大概用得最多的容器就是ArrayList了。所以掌握ArrayList的底层实现,还是比较重要的。一般面试的时候也是常见的问题。
废话不多说,直接上代码。注释也很详细。首先是定义一个List接口。这样我们后面可以实现ArrayList,也可以实现LinkedList。
一、定义List接口
package com.zwh.linertable;
/**
* @Auther: zwh
* @Date: 2018/12/26 09:39
* @Description:定义操作列表的接口方法
* @Version:1.0
*/
@SuppressWarnings("all")
public interface List {
/**
* @description : 获取线性表的长度
* @param:
* @return: 长度
* @auther: zwh
* @date: 2018/12/26 9:39
*/
public int size();
/**
* @description : 获取指定索引的内容
* @param: 索引位置
* @return: 指定位置的内容
* @auther: zwh
* @date: 2018/12/26 9:40
*/
public Object get(int index);
/**
* @description : 判断当前容器是否为空
* @param:
* @return:
* @auther: zwh
* @date: 2018/12/26 9:41
*/
public boolean isEmpty();
/**
* @description :判断当前容器是否包含当前对象
* @param:
* @return:
* @auther: zwh
* @date: 2018/12/26 9:41
*/
public boolean contains(Object element);
/**
* @description :往容器中添加一个对象
* @param:
* @return:
* @auther: zwh
* @date: 2018/12/26 9:41
*/
public void add(Object element);
/**
* @description :
* @param:
* @return:
* @auther: zwh
* @date: 2018/12/26 9:42
*/
public void remove(Object element);
/**
* @description : 从容器中删除一个对象
* @param: 要删除对象的索引
* @return:
* @auther: zwh
* @date: 2018/12/26 9:44
*/
public void remove(int index);
/**
* @description : 添加一个对象到指定位置
* @param: i:要添加对象的索引位置,e:要添加的对象
* @return:
* @auther: zwh
* @date: 2018/12/26 9:45
*/
public void add(int index,Object element);
/**
* @description : 获取某个对象在容器中的索引
* @param: 对象
* @return: 索引值
* @auther: zwh
* @date: 2018/12/26 9:45
*/
public int indexOf(Object elemente);
/**
* @description : 在指定对象前添加一个对象
* @param: obj:添加对象的位置,e:要添加的对象
* @return:
* @auther: zwh
* @date: 2018/12/26 9:46
*/
public void addBefore(Object obj,Object element);
/**
* @description : 在指定对象后添加一个对象
* @param:obj:添加对象的位置,e:要添加的对象
* @return:
* @auther: zwh
* @date: 2018/12/26 10:01
*/
public void addAfter(Object obj,Object element);
/**
* @description : 替换制定位置的对象
* @param: i:要替换对象的位置,e:要替换的新的对象
* @return:
* @auther: zwh
* @date: 2018/12/26 9:58
*/
public Object replace(int index,Object element);
}
这个接口中,基本包含了大部分的java.util中List的功能。然后一个一个去实现它。
二、使用ArrayList实现List
1、新建一个ArrayList类,实现List接口。
/**
* @Auther: zwh
* @Date: 2018/12/26 10:05
* @Description: 线性表,底层采用数组,长度可以动态变化
* @Version:1.0
*/
@SuppressWarnings("all")
public class ArrayList implements List {
}
2、在这个实现类中,一个一个的去实现接口中定义的方法。ArrayList底层采用数组来实现。所以我们提前定义好成员变量:
/**
* 存储当前线性表内容
*/
private Object[] elementData;
/**
* 存储当前线性表长度
*/
private int size;
/**
* 默认的空线性表
*/
private Object[] DEFAULT_EMPTY_ELEMENTDATA={};
/**
* 定义一个List的最大容量
*/
private int DEFAULT_MAX_CAPACITY=Integer.MAX_VALUE-8;
3、定义构造方法,用于初始化容器的容量。一般提供两个构造方法,一个为空构造方法,一个提供一个长度参数。
/**
* 初始化一个长度为length的ArrayList,注意,这里的length和size无关
* @param length
*/
public ArrayList(int length) {
elementData=new Object[length];
}
/**
* 初始化一个空的ArrayList
*/
public ArrayList() {
elementData=DEFAULT_EMPTY_ELEMENTDATA;
}
4、实现最简单的两个方法,size(),isempty()
@Override
public int size() {
//返回的是元素的个数,而不是数组的长度,所以只有往List中添加了元素之后,size才会增加
return size;
}
@Override
public boolean isEmpty() {
return size==0;
}
5、实现add方法
在实现add方法之前,有一点需要注意。我们初始化ArrayList时,数组的长度是固定的。如果容器的size已经超过了初始化的数组的长度,就会引发数组索引越界异常。所以在添加之前,都应该去判断容器的size是否超过了数组的长度。如果size>=elementData.length,则需要对数组进行扩容。所以我们首先把数组扩容的方法实现,添加就变得容易了。关于数组扩容,参照java.util.ArrayList中提供的方法。实现代码如下:
/**
* @description : 数组扩容
* @param:minCapacity:需要扩大的最小容量
* @return:
* @auther: zwh
* @date: 2018/12/28 10:23
*/
private void grow(int minCapacity){
//现在的数组的长度
int oldCapacity=elementData.length;
//新数组的长度,在现有数组长度的基础上,在增加1/2。这样不至于增加太多,也不至于太少,导致增加次数过多
int newCapacity=oldCapacity+(oldCapacity>>1);
//如果新的数组的长度>目前需要的数组长度,则扩大现在需要的数组长度
if(minCapacity>newCapacity){
newCapacity=minCapacity;
}
//如果新的数组长度大于设定的最大值,则直接去Integer能表示的最大值
if (newCapacity>DEFAULT_MAX_CAPACITY){
newCapacity=Integer.MAX_VALUE;
}
//通过数组拷贝,将原数组进行扩容。
elementData=Arrays.copyOf(elementData,newCapacity);
}
其实这个grow方法还有可以优化的地方,暂时先不考虑。然后再做add,就比较容易了:
@Override
public void add(Object e) {
if(size>=elementData.length){
grow(size+1);
}
elementData[ size++]=e;
}
6、实现其他带索引位置的add方法。实现了最普通的add方法之后,考虑实现复杂一些的add方法。第一个是add(int index,Object element)。在指定的位置添加一个新的元素。因为其基于数组来实现的原因,在中间添加元素是比较麻烦的一件事情。因为后面的元素全部需要移动一次位置。所以在本次实现中,我主要使用了数组拷贝。当然,也可以使用循环赋值的方式实现,对于初学者来说更容易理解。
@Override
public void add(int index, Object element) {
if(index<0 || index>=size){
throw new ArrayIndexOutOfBoundsException("IndexOutOfBoundsException! current index:"+index+",size:"+size);
}
Object[] newElementData=new Object[size+1];
//将原来数组的index之前的元素复制到新数组
System.arraycopy(elementData,0,newElementData,0,index);
//将元素赋值给新数组的第index个元素
newElementData[index]=element;
//将原数组的index之后的元素复制到新数组的index元素之后。
System.arraycopy(elementData,index,newElementData,index+1,size-index);
//将原来的数组指向新的数组
elementData = newElementData;
size++;
}
接下来addBefore,addAfter就比较容易了。先找到索引位置,调用add(index,element)就ok了。不必再去写同样的代码。
@Override
public void addBefore(Object obj, Object element) {
for(int i=0;i<size;i++){
if(elementData[i]==obj){
add(i,element);
return;
}
}
}
@Override
public void addAfter(Object obj, Object element) {
for(int i=0;i<size;i++){
if(elementData[i]==obj){
add(i+1,element);
return;
}
}
}
7、get方法实现:这个也比较简单了,这是线性表的优势,基于数组的底层实现,对于查询来说非常容易。通过数组的索引即可快速查找对应的元素。
@Override
public Object get(int index) {
if(index<0 || index>=size){
throw new ArrayIndexOutOfBoundsException("IndexOutOfBoundsException! current index:"+index+",size:"+size);
}
return elementData[index];
}
8、remove方法实现:首先remove(int index)方法,通过索引删除某个元素。这个于add方法中带索引参数类似。需要删除后对后面的元素进行位置的移动。同样使用数组拷贝:注意添加时的size++,删除元素时的size--。
public void remove(int i) {
try {
throwIndexOutOfBoundsException(i);
} catch (Exception e) {
e.printStackTrace();
}
Object[] newElementData=new Object[size-1];
/*复制数组,从原数组的下标为0的对象开始,复制长度为i,当i=0时,代表不复制,否则复制长度与i
*相同下标为i的元素未复制,复制范围:elementData[0,i-1]*/
System.arraycopy(elementData,0,newElementData,0,i);
/*复制数组,从原数组的下标为i+1的对象开始,复制长度为size-i-1,下标范围:[i+1,size-1]*/
System.arraycopy(elementData,i+1,newElementData,i,size-i-1);
elementData=newElementData;
size--;
}
再实现remove的其他方法。就可以调用已经写过的方法了。统一通过索引去删除元素
@Override
public void remove(Object e) {
for (int i=0;i<size;i++ ) {
if (e==elementData[i]){
remove(i);
return;
}
}
}
9、实现其他方法:剩下这些方法都很容易理解,就不多做描述了。
1)indexOf方法:通过遍历去找索引
@Override
public int indexOf(Object e) {
for (int i=0;i<size;i++) {
if(e==elementData[i]){
return i;
}
}
return -1;
}
2)contains方法:一样是通过遍历。
@Override
public boolean contains(Object e) {
for (Object obj:elementData) {
if(e==obj){
return true;
}
}
return false;
}
3)replace方法:真正开发中需要注意i越界的问题。
@Override
public Object replace(int i, Object e) {
elementData[i]=e;
return e;
}
三、总结
线性表底层基于数组来实现,目前我们是统一使用Object来作为元素的类型,真正的ArrayList还需要使用泛型,通过泛型可以对元素的数据类型进行指定。
基于数组的底层实现,赋予了ArrayList查询的便利性,并且对于添加元素到最后,删除最后一个元素等方法都非常容易,但对于在List的中间部分添加元素,删除元素等,就显得比较复杂了。这和LinkedList是正好互补。