ArrayList是啥?
ArrayList是一种变长的数组,它比数组来的更强大,你不需要考虑数组的下标是否会超出范围的问题。
步骤 1 引出问题
假如没有ArrayList,我们需要保存一堆数据的集合,不允许用LinkedList和HashMap,你打算怎么做?
好像没有特别好的路子,能用的只有数组了。
数组在你定义的时候,就需要强行指定类型,还必须规定长度。比如:
int[] numbers = new int[5];
我定义了一个int类型的数组,长度是5。接下来,做测试:
int[] numbers = new int[5];
for (int i = 0; i < 6; i++) {
numbers[i] = i ;
}
直接报错了:
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException:
5 at test.TestMap.main(TestMap.java:43)
要是有一种数据结构,我只管往里头添加东西,要是容量不够,它就能自动给我扩容该有多好?
大家回忆一下,当初写LinkedList的时候,是不是没遇到需要扩容的情况?
为什么?那是因为LinkedList内部维护了一个链表,链表的话只要你愿意,内存足够,你就可以无限制地加下去。
但是数组就不行,因为数组的长度是固定的,超过这个长度就会报错。所以,我们需要在内部做一个判断,如果快超限了,就给数组扩容。这也是ArrayList内部的基本原理。
内部的数组
和LinkedList的套路一样,LinkedList实际上真正起作用的是内部的Node,而ArrayList内部就是维护一个数组。这其实就是数组的强化,ArrayList是强化版的数组。
步骤 1 从零开始敲代码
老规矩,从零开始,创建一个Class。
内部不是要维护一个数组吗,就设置一个Object类型的数组,能存放任何的类型。
public class TuziArrayList {
private Object[] array;
}
到时候遍历啥的,是不是得有一个下标,于是就弄个index。
public class TuziArrayList {
private Object[] array;
private int index;
}
数组的总长度是多少啊?为了方便,再来个size属性。现在是我们自己来开发,所以不需要去看JDK里面的ArrayList到底怎么设计的,先按照我们自己的思路来。
public class TuziArrayList {
private Object[] array;
private int index;
private int size;
}
步骤 2 构造方法
接下来设计构造方法,直接new的话,就给数组一个默认的长度,所以还得来个默认的长度变量。
public class TuziArrayList {
private Object[] array;
private int index;
private int size;
private int capacity = 10; // 默认长度为10
public TuziArrayList(){
array = new Object[capacity];
}
}
当然,也可以指定内部数组的长度,传一个容量进去。
public TuziArrayList(int capacity){
this.capacity = capacity;
array = new Object[capacity];
}
为什么这么设计?因为假如你知道这个数组需要存放很多很多的数据,那么就可以一次性把数组长度设置大一点,免得到时候一次次地扩容影响效率。
add方法
给ArrayList添加元素,其实就是给内部的数组添加元素。
public void add(Object o){
array[index++] = o;//给数组赋值
size++;//注意,这个是数组内实际元素的个数,而不是数组的总长度
}
代码很清晰,下标要++,size也要++。
测试:
public static void main(String[] args) {
TuziArrayList list = new TuziArrayList();
for (int i = 0; i < 12; i++) {
list.add(i);
}
}
报错:
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 10
这是因为数组总长度是10,只能存放下标为0-9的数据,所以溢出了。
为了解决上面的问题,还是得数组扩容。我们可以定义一个加载引子,factor,当超过这个百分比时,就让数组自动扩容。
private float factor = 0.75f;
还是老规矩,先写设计占坑。
public void add(Object o){
if(index >= capacity * factor){
enlarge();
}
array[index++] = o;//给数组赋值
size++;//注意,这个是数组内实际元素的个数,而不是数组的总长度
}
反正我就是要enlarge,怎么实现待会再说。这个enlarge方法,就是让数组自动扩容一倍,然后原来的数据要拷贝过新的数组。
private void enlarge() {
//数组容量扩大一倍
capacity *= 2;
//创建新数组
Object[] arrNew = new Object[capacity];
}
写到这里完全没有难度啊,有木有!
但是,如何拷贝数组呢?有了,java本身提供一个好的方法:
System.arraycopy(src, srcPos, dest, destPos, length);
参数的含义(看了api,我反复琢磨以后,感觉这样翻译比较好)
src : 需要拷贝的数组。
srcPos : 从哪里开始 拷贝?
dest : 目标数组
destPos : 从哪里开始 粘贴?
length : 拷贝的元素个数
private void enlarge() {
//数组容量扩大一倍
capacity *= 2;
//创建新数组
Object[] arrNew = new Object[capacity];
//拷贝数组
System.arraycopy(array, 0, arrNew, 0, size);
//把新的数组赋值给原数组
array = arrNew;
}
嗯,应该挺对的,赶紧来试试。
果然,原来的方法不报错了。为了看起来方便,我们下一节来设计toString方法。
toString方法
toString方法可以方便我们观察ArrayList里面的数据。
public String toString(){
StringBuffer sb = new StringBuffer();
for (int i = 0; i < size; i++) {
if(i == size - 1)
sb.append(array[i]);
else
sb.append(array[i]+",");
}
return sb.toString();
}
public static void main(String[] args) {
TuziArrayList list = new TuziArrayList();
for (int i = 0; i < 12; i++) {
list.add(i);
}
System.out.println(list);
}
remove方法
remove方法用来删除某一个元素。
步骤 1 两种删除方法
首先,我们看下ArrayList的remove方法是怎么做的。
第一种,是通过下标删除。
public E remove(int index)
第二种,是根据对象删除。
public boolean remove(Object o)
步骤 2 根据下标删除
这个下标其实就是内部数组的下标,众所周知,数组删除元素一向是个令人头疼的问题,下面演示一种推荐的做法,没错,还是用arrayCopy方法。
假如数组是这样的:
我想要删除元素2,也就是index = 1的位置。
2拿掉,后面的3和4左移一个单位不就好了吗?
再复习一下之前arrayCopy的用法:
System.arraycopy(src, srcPos, dest, destPos, length);
参数的含义(看了api,我反复琢磨以后,感觉这样翻译比较好)
src : 需要拷贝的数组。
srcPos : 从哪里开始 拷贝?
dest : 目标数组
destPos : 从哪里开始 粘贴?
length : 拷贝的元素个数
憋口气,一下子就写出下面的代码:
System.arraycopy(array, index+1, array, index, size - index -1);
size是4,index是1,所以size-index-1就是2, 什么 是快乐星球?
哦不好意思,跑错片场了。
2是什么,2不就是后面需要移动的元素个数嘛!所以,最后一个参数就是拷贝的元素个数。
但是,这样一来就有个问题:
如图,因为数组的长度还是4,remove掉一个元素后,最后一个位置就空了。我们需要手动把它置为空,这里如果不置空,它将会保存着一个引用,那么垃圾收集器将无法回收它,可能会造成内存泄漏。
最终源码:
public Object remove(int index){
//边界检测方法
rangeCheck(index);
Object oldValue = array[index];
System.arraycopy(array, index+1, array, index, size - index -1);
array[--size] = null;
return oldValue;
}
private void rangeCheck(int index) {
if(index < 0 || index >= size){
throw new ArrayIndexOutOfBoundsException(index);
}
}
步骤 3 根据元素删除
如果上面的方法理解了,那么这个方法简直就是soEasy,不就是遍历一遍数组的所有元素,然后重新调用上面的方法么?
说干就干。
public Object remove(Object e){
for (int i = 0; i < size; i++) {
if(array[i].equals(e)){
remove(i);
}
}
return true;
}
步骤 4 问题1
第一个问题,假如我给ArrayList添加null咋办?
如果元素是null,那么这个地方直接.equals肯定会报空指针的。所以,代码需要优化一下。
public Object remove(Object e){
for (int i = 0; i < size; i++) {
if(array[i] != null && array[i].equals(e)){
remove(i);
}
}
return true;
}
这边用了短路与,必须左边的表达式为true,右边的才会执行。
步骤 5 问题1–如何元素值为null怎么办?
第一个问题,假如我给ArrayList添加null咋办?
如果元素是null,那么这个地方直接.equals肯定会报空指针的。所以,代码需要优化一下。
public Object remove(Object e){
for (int i = 0; i < size; i++) {
if(array[i] != null && array[i].equals(e)){
remove(i);
}
}
return true;
}
这边用了短路与,必须左边的表达式为true,右边的才会执行。
步骤 6 问题2–怎么总是返回true
这个问题非常明显,就是return的地方很明显错了,改一下:
public Object remove(Object e){
for (int i = 0; i < size; i++) {
if(array[i] != null && array[i].equals(e)){
remove(i);
return true;
}
}
return false;
}
步骤 7 问题3–如果remove传进来的值是null?
如果remove传进来的值是null?还真有这种可能,虽然在当前的逻辑下,最多就是循环一圈找不到匹配的,返回false。但是,这样还是很影响效率,不如一开始就把这个情况判断掉。
public Object remove(Object e){
if(e == null)
return false;
for (int i = 0; i < size; i++) {
if(array[i] != null && array[i].equals(e)){
remove(i);
return true;
}
}
return false;
}
步骤 8 问题4–凭什么你可以添加null,还不允许我拿null去比较了?
额,好像也有道理,那就允许比较。。。
public Object remove(Object e){
if(e == null){
for (int i = 0; i < size; i++) {
if(array[i] == null){
remove(i);
return true;
}
}
}else{
for (int i = 0; i < size; i++) {
if(array[i] != null && array[i].equals(e)){
remove(i);
return true;
}
}
}
return false;
}
步骤 9 问题5–呵呵了,明知道不可能越界,你还要去一遍遍检查?
这个说到点子上了,因为我们是直接去循环数组的,不可能发生越界,所以,这边直接调用 remove(int index) 其实很耗费性能的。
改一下吧,不妨弄个fastRemove方法,跳过边界检查并且不返回删除元素值。
private void fastRemove(int index){
System.arraycopy(array, index+1, array, index, size - index -1);
array[--size] = null;
}
于是,代码就变成了这样:
public Object remove(Object e){
if(e == null){
for (int i = 0; i < size; i++) {
if(array[i] == null){
fastRemove(i);
return true;
}
}
}else{
for (int i = 0; i < size; i++) {
if(array[i] != null && array[i].equals(e)){
fastRemove(i);
return true;
}
}
}
return false;
}
最后给出一些测试用例,验证ArrayList的稳定性和可行性。
步骤 1 新增测试
public static void main(String[] args) {
TuziArrayList list = new TuziArrayList();
for (int i = 0; i < 20; i++) {
list.add(i);
}
System.out.println(list);
}
0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19
测试结果:通过!
#####步骤 2 删除测试
public static void main(String[] args) {
TuziArrayList list = new TuziArrayList();
for (int i = 0; i < 20; i++) {
list.add(i);
}
list.add(null);
list.add("ABC");
//1.根据下标删除
list.remove(10);
//2.根据参数对象删除
list.remove(new Integer(11));
//3.删除null
list.remove(null);
System.out.println(list);
}
0,1,2,3,4,5,6,7,8,9,12,13,14,15,16,17,18,19,ABC
测试结果:通过!
步骤 3 百万级数据压测
public static void main(String[] args) {
long startTime = System.currentTimeMillis(); //获取开始时间
TuziArrayList list = new TuziArrayList();
for (int i = 0; i < 100 * 10000; i++) {
list.add(i);
}
long overTime = System.currentTimeMillis();
System.out.println("程序运行时间为:"+(overTime-startTime)+"毫秒");
}
程序运行时间为:271毫秒
PK原生的ArrayList:
public static void main(String[] args) {
long startTime = System.currentTimeMillis(); //获取开始时间
List list = new ArrayList();
for (int i = 0; i < 100 * 10000; i++) {
list.add(i);
}
long overTime = System.currentTimeMillis();
System.out.println("程序运行时间为:"+(overTime-startTime)+"毫秒");
}
程序运行时间为:251毫秒
其实已经差不多了,相差不是很大。