带你手写ArrayList
原文地址https://www.cnblogs.com/hkblogs/p/9025221.html ,欢迎转载!
前言
这是带给大家的第一篇技术文章,也是所有的java开发人员肯定使用过的。因为相比较其它数据结构而言,无疑它是最简单的。所以今天放第一篇来给大家介绍。并自行实现一个简易版的ArrayList。
本文借鉴jdk7与8实现,部分变量命名参考jdk命名,细节无法顾全,但大体思想是jdk的思想,做一个简化版来帮助大家理解。
关注点
不只是ArrayList,在任何集合中,都应关注以下几点:
|
是否可以为空 |
可以为空 |
|
是否有序(插入时候的顺序和读取时是否一致) |
有序 |
|
是否允许重复 |
允许 |
|
是否线程安全 |
否 |
|
特性 |
读取快,删除与插入慢 |
原理
ArrayList底层通过数组实现,在Java中即Object[] 对象,数组是可以存放大量数据的,所以定义一个Object数组,我们可以想放任何我们要存储的数据。
实现
既然要使用数组存放数据,首先定义一个Object数组,用来存放数据。并定义size属性来标识list的内容的长度。jdk源码是一个泛型类,简易版为了看的方便未加入泛型。(仅需定义类中定义泛型,返回值改为对应泛型,关于泛型的详细讲解,会在后续章节讲到)
public class HkArrayList { private Object[] elementData;//存放数组变量 private int size;//list存放的内容长度
public int size(){
return size;
}
}
定义好之后,就要创建Arraylist对象,给一个默认无参构造器。
//构造器 public HkArrayList(){ this.elementData=new Object[10]; }
此处,给定数组默认大小为10(当然jdk不是这样写的构造器,jdk中默认长度是10,可以自行传参指定长度),那么假如添加11个元素怎么办?是的,肯定是ArrayIndexOutOfBoundsException,那么怎么解决此下标越界,请往下看!
增
接下来,我们实现第一个方法,添加元素add()方法,很简单:
public boolean add(Object o){ elementData[size++]=o; return true; }
因为是顺序添加的,假设第1次调用add("aaa")时,此时size=0,则为:elementData[0]="aaa",因为size++自增,以此保证后续调用正常,此处应该很好理解。
测试
HkArrayList list =new HkArrayList(); list.add("aaa"); list.add("bbb"); list.add("ccc"); System.out.println(list.size()); //3
此处肯定是打印3,此时,用图表示,大概是这样的:

那么,问题来了,假如一直往上增,超过了数组的10个长度怎么办?此时,需要动态对数组进行扩容:
事实上,在java中,数组是没法进行扩容的,即第一次定义了长度后就不可改变了,否则怎么会还有下标越界这种异常呢?那么,我们要想对数组进行容量增大:
定义一个新的更大容量的数组,将原来的旧数组的内容拷贝进去。是的,就是这么简单!拷贝数组可用Arrays.copyOf()方法
新容量的大小,在jdk6中,是旧容量的*3/2+1,在jdk7与8中,是*3/2即1.5倍,只不过jdk为了效率和装X用的是移位算法。
所以对add方法改进如下:
1 public boolean add(Object o){ 2 if ((size+1) - elementData.length > 0){ 3 int oldCapacity=elementData.length; 4 int newCapacity=(oldCapacity*3)/2; 5 elementData=Arrays.copyOf(elementData, newCapacity); 6 } 7 elementData[size++]=o; 8 return true; 9 }
解释:第2行,判断是否需要扩容,如果当前的元素数再+1个元素>数组的长度,则需要扩容
第3-4行,定义新容量。第5行,将旧数组的全部内容拷贝到新数组中。OK,此时add方法应该没有问题了。
查
获取元素,get()方法比较简单
1 public Object get(int index) { 2 rangeCheck(index); 3 return elementData[index]; 4 } 5 6 //检查给定下标是否符合规则 7 private void rangeCheck(int index){ 8 if(index>size||index<0){ 9 throw new IndexOutOfBoundsException("下标有误,Index:"+index+",size:"+size); 10 } 11 }
首先对于给定下标要做检查,鉴于集合中多处要有检查下标的地方,单独提取出此方法rangCheck(),此处不进行测试了,自行测试。
改
set方法也较为简单
1 private Object set(int index,Object element) { 2 rangeCheck(index); 3 Object oldValue = elementData[index]; 4 elementData[index] = element; 5 return oldValue; 6 }
根据下标,返回值是旧的值,将旧值替换为新的值。
删
插入
ArrayList中,删除操作与随机插入操作的原理基本相似。先写一下插入一个元素在ArrayList中的实现
插入相对于顺序添加与修改和查询而言,稍复杂一些。
在jdk源码中,执行以下操作(此处讲解是jdk的Arraylist执行操作,并不是我写的HkArrayList)
1 list.add("aaa"); 2 list.add("bbb"); 3 list.add("ccc"); 4 list.add(1,"new");
此时,1-3行执行完后,大概意思还是这样

当第4行在下标为1的位置插入执行后,变成了这样

画图是为了更好的说明,当然在内存中并不是这样放的。可以看到,下标1之前的没有动,下标1之后的元素,都向后移动了一位。
但是,java中的数组中可以这样操作吗?答案是当然不可以。
此时唯一的做法是数组复制。
调用System.arraycopy(Object src,int srcPos,Object dest,int destPos,int length)此方法第1个参数是源数组,第2个参数是源数组要复制的起始位置,
第3个参数是目标数组,第4个参数是目标数组放置的起始位置,最后一个参数是要复制的长度。
当源数组与目标数组相同时,即可实现我们的插入需求。好了,理论讲完开始代码:
1 public boolean add(int index,Object o){ 2 rangeCheck(index); 3 ensureCapacityInternal(size+1); 4 System.arraycopy(elementData, index, elementData, index+1, size-index); 5 elementData[index]=o; 6 size++; 7 return true; 8 }
第2-3行是判断下标是否越界和是否需要扩容。对于是否需要扩容,多出用到,所以此处单独提取出get中检测是否需要扩容的方法,命名参考jdk8,
第4行即进行数组的拷贝,以刚才的例子:

第4行执行完后是这样的,1位置是空的,将1位置后边的所有元素拷贝到新数组中。
然后执行第5行,对1位置进行复制。
OK,大功告成,删除操作与此极为相似,大家可自行实现
总结
1、ArrayList查找方法get()速度非常快,因为只是从给定下标处拿一个值而已。
2、ArrayList顺序添加的速度也很快,因为只是在数组上给定位置放一个元素而已。(忽略容量不够扩容的影响)
3、ArrayList随机插入的时,性能较差,假如有10000个元素,在第2个位置插入一个元素,那么需要将后9998个元素做一次复制,如果业务中有大量的插入操作,慎用ArrayList。(下标删除操作与插入同理,较为耗性能)
4、网上的很多说法是不准确的,比如插入多用LinkedList,查询用ArrayList等等,其实不然,假如你确定你就是顺序插入,或你是靠末尾插入,此时ArrayList性能明显高于LinkedList(linkedList需要寻址,在下一章节为大家讲到)
疑问
关于大家的疑问,我会整理到这里,此处持续更新!
1、假如扩容多次后,做了大量删除,导致占用内存过大,此时做如何操作?
2、jdk源码中modCount是干嘛的?
附上源码
public class HkArrayList { private Object[] elementData; private int size; //构造器 public HkArrayList(){ this.elementData=new Object[10]; } public int size(){ return size; } public boolean add(Object o){ ensureCapacityInternal(size+1); elementData[size++]=o; return true; } //重载add方法通过下标来添加 public boolean add(int index,Object o){ rangeCheck(index); ensureCapacityInternal(size+1); System.arraycopy(elementData, index, elementData, index+1, size-index); elementData[index]=o; size++; return true; } public Object get(int index) { rangeCheck(index); return elementData[index]; } //检查给定下标是否符合规则 private void rangeCheck(int index){ if(index>size||index<0){ throw new IndexOutOfBoundsException("下标有误,Index:"+index+",size:"+size); } } public Object set(int index,Object element) { rangeCheck(index); Object oldValue = elementData[index]; elementData[index] = element; return oldValue; } private void ensureCapacityInternal(int minCapacity) { if (minCapacity - elementData.length > 0){ //需要的最小容量已经大于了数组的长度,此时需要扩容 int oldCapacity=elementData.length; int newCapacity=(oldCapacity*3)/2; //如果扩容1.5倍还不够,则将传入的长度为新长度 if (newCapacity - minCapacity < 0) newCapacity=minCapacity; elementData=Arrays.copyOf(elementData, newCapacity); } } @Override public String toString() { return "HkArrayList [elementData=" + Arrays.toString(elementData) + ", size=" + size + "]"; } }
============================================================================
如果不觉得一年前的自己是个傻子,那说明这一年你没有任何进步!
============================================================================

浙公网安备 33010602011771号