源码解析系列:ArrayList(1) - 介绍和接口实现


前言

这个系列开始要对ArrayList进行逐步的分析,视频参考了黑马的 ArrayList 源码解析, 在分析的过程中我会尽量每一行代码都涉及到,尽可能全面地把ArrayList分析到位。

ArrayList源码解析(1):ArrayList源码解析(1)
ArrayList源码解析(2):ArrayList源码解析(2)
ArrayList源码解析(3):ArrayList源码解析(3)
ArrayList源码解析(4):ArrayList源码解析(4)
ArrayList源码解析(5):ArrayList源码解析(5)


1. 简介

ArrayList 是 Java 中的一种存储集合,底层主要使用的是数组形式来存储,对比于 HashMap,ArrayList 的整个过程是没有那么复杂的。由于 ArrayList 是由数组存储的,这就导致了 ArrayList 的一个特性就是查找快,因为数组在空间中的存储是连续的,可以通过下标索引的方式快速定位到查找的位置;而增加和删除就慢,涉及到数组元素的移动。


2. 实现接口

了解一个集合的整体,首先就要从接口开始,看看这个集合实现了什么接口或者是抽象类

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable {}

2.1 Serializable 接口

这个接口应该不陌生,就是序列化接口,实现这个接口之后,就可以进行序列化。也就是说 ArrayList 是可以写入文件的,并且通过反序列化可以从文件中读出 ArrayList。下面来演示一下:

@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Student implements Serializable {
    private String name;
    private int age;
}

测试类:

public class TestAAA {

    public static void main(String[] args) throws Exception {
        //创建对象操作流 --> 序列化
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("obj.txt"));
        ArrayList<Student> arrayList = new ArrayList<>();
        arrayList.add(new Student("aaa", 18));
        arrayList.add(new Student("bbb", 19));
        arrayList.add(new Student("ccc", 20));
        arrayList.add(new Student("ddd", 21));
        oos.writeObject(arrayList);

        //创建对象输入流 --> 反序列化
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("obj.txt"));
        final Object o = ois.readObject();
        ArrayList<Student> arrs = (ArrayList<Student>)o;
        for (Student arr : arrs) {
            System.out.println(arr);
        }
    }
}

结果:
在这里插入图片描述


如果是一个对象没有实现 Serializable 接口,就会抛出异常。这个接口内部没有东西,只是作为一个接口进行判断。
在这里插入图片描述



2.2 Cloneable 接口

原理

实现这个接口之后,就可以使用 object.clone() 方法,复制原有的类成一份新的,并且里面的属性的值也会复制过去。如果不是先这个接口,那么就会抛出异常 CloneNotSupportedException,总之克隆就是依据已经有的数据,创造一份新的完全一样的数据拷贝


接口
克隆接口内部和序列化接口一样,也是没有东西的

在这里插入图片描述
如果要实现克隆的效果,得分成两步:

  • 被克隆对象所在的类必须实现 Cloneable 接口
  • 必须重写 clone 方法

克隆

当然了,这个接口的克隆也分为浅克隆和深克隆。这两个的区别在我的享元模式这篇文章中有提到。简单来说就是浅克隆中克隆对象指向同一个,而深克隆指向不同,下面就举个例子来看看这两者的区别:

1、浅克隆

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student implements Serializable, Cloneable {

    private String name;
    private int age;
    private Teacher teacher;

    @Override
    protected Student clone() throws CloneNotSupportedException {
        //浅克隆
        return this.clone();
    }
}

@AllArgsConstructor
@NoArgsConstructor
@Data
public class Teacher implements Cloneable {
    private String name;
}

测试类:

public class TestAAA {

    public static void main(String[] args) throws Exception {
        ArrayList<Student> arrayList = new ArrayList<>();
        arrayList.add(new Student("aaa", 18, new Teacher("aa")));
        arrayList.add(new Student("bbb", 19, new Teacher("bb")));
        arrayList.add(new Student("ccc", 20, new Teacher("cc")));
        arrayList.add(new Student("ddd", 21, new Teacher("dd")));
        for (Student student : arrayList) {
            System.out.println(student);
        }
        System.out.println("==================================克隆之后=============================================");
        //浅克隆
        final ArrayList<Student> clone = (ArrayList<Student>)arrayList.clone();
        for (Student student : clone) {
            System.out.println(student);
        }
    }
}

结果输出:
在这里插入图片描述

可以看到,克隆之后的数据和之前的是一样的。但是如果我们尝试去修改原来的内容,那么浅克隆之后的也会一起变化,下面就来修改下原来的 name=aaa 的学生信息。下面我们只加入这一行代码,然后测试(arrayList是原来的集合):
arrayList.get(0).getTeacher().setName("修改后的aa");
在这里插入图片描述
可以看到,修改了原来的,后来的也会变化。当然要知道的是修改的是原来Student类对象中的Teacher对象。所以可以得出结论就是,浅克隆之后的对象属性指向的是同一个。而我们如果修改Student中的普通属性,后来的是不会变化的,这种情况针对对象属性。



2、深克隆
为了避免上面的问题,我们就要用到深克隆。而深克隆的写法其实很简单,就是把对象里面的对象属性再克隆一次,然后赋值,写法如下:

@AllArgsConstructor
@NoArgsConstructor
@Data
public class Teacher implements Cloneable {
    private String name;

    @Override
    protected Teacher clone() throws CloneNotSupportedException {
        return (Teacher)super.clone();
    }
}

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student implements Serializable, Cloneable {

    private String name;
    private int age;
    private Teacher teacher;

    @Override
    protected Student clone() throws CloneNotSupportedException {
        //浅克隆
        final Student clone = (Student) super.clone();
        //对象属性也要克隆一次
        clone.setTeacher(this.teacher.clone());
        return clone;
    }
}

下面我们不在 ArrayList 里面测试了,因为我们无法修改 ArrayList 中的 clone方法,我们另外测试:

public class TestAAA {

    public static void main(String[] args) throws Exception {
        Student student = new Student("aaa", 18, new Teacher("aa"));
        final Student clone = student.clone();
        student.getTeacher().setName("修改之后的aa");

        System.out.println(student);
        System.out.println(clone);
    }
}

测试结果如下:
在这里插入图片描述

显然,克隆前和克隆后中的 Teacher 对象不是同一个。


总结起来就是:ArrayList 中的 clone 方法其实是一个浅克隆,克隆出来的集合中的对象元素的地址和原来的是一样的。



2.3 RandomAccess接口

介绍

RandomAccess 是一个随机访问接口,由 List 集合进行实现以表明它们支持快速随机访问。主要体现在算法通过判断有没有实现这个接口,然后选择对应的遍历算法。主要的判断方法就是通过 instanceof 来判断,这个关键字可以判断一个类是否实现了某个接口。

对于 ArrayList 来说,由于底层使用了数组存储,所以支持下标访问,所以 ArrayList 实现了 RandomAccess 接口,这样使用基于下标的随机访问的访问方式可以更快。而 LinkedList 底层使用的是链表存储,所以对于 LinkedList 而言就不需要实现这个接口,使用顺序访问的方式更快,我们也可以测试一下顺序访问和随机访问的性能


测试

1、ArrayList 的随机访问和顺序访问

public class TestAAA {

    public static void main(String[] args) throws Exception {
        //创建ArrayList集合
        List<String> list = new ArrayList<>();
        //添加10W条数据
        for (int i = 0; i < 100000; i++) {
            list.add(i + "a");
        }
        System.out.println("----通过索引(随机访问:)----");
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < list.size(); i++) {
            list.get(i);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("随机访问: " + (endTime - startTime));
        System.out.println("----通过迭代器(顺序访问:)----");
        startTime = System.currentTimeMillis();
        Iterator<String> it = list.iterator();
        while (it.hasNext()) {
            it.next();
        }
        endTime = System.currentTimeMillis();
        System.out.println("顺序访问: " + (endTime - startTime));
    }
}

结果:
在这里插入图片描述


2、LinkedList 的随机访问和顺序访问

public class TestAAA {

    public static void main(String[] args) throws Exception {
        //创建ArrayList集合
        List<String> list = new LinkedList<>();
        //添加10W条数据
        for (int i = 0; i < 100000; i++) {
            list.add(i + "a");
        }
        System.out.println("----通过索引(随机访问:)----");
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < list.size(); i++) {
            list.get(i);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("随机访问: " + (endTime - startTime));
        System.out.println("----通过迭代器(顺序访问:)----");
        startTime = System.currentTimeMillis();
        Iterator<String> it = list.iterator();
        while (it.hasNext()) {
            it.next();
        }
        endTime = System.currentTimeMillis();
        System.out.println("顺序访问: " + (endTime - startTime));
    }
}

测试结果:
在这里插入图片描述

源码中的 instanceof
比如在 collections 中的 binarySearch 中,就用到了这个方法
public static <T> int binarySearch(List<? extends Comparable<? super T>> list, T key) {
		//如果是没有实现 RandomAccess 接口并且长度大于5000的就使用随机访问方法
        return !(list instanceof RandomAccess) && list.size() >= 5000 ? iteratorBinarySearch(list, key) : indexedBinarySearch(list, key);
    }

//内部其实就是一个二分查找,基于下标的集合才可以这样做
private static <T> int indexedBinarySearch(List<? extends Comparable<? super T>> list, T key) {
        int low = 0;
        int high = list.size() - 1;

        while(low <= high) {
            int mid = low + high >>> 1;
            //如果是ArrayList就直接下标索引了
            Comparable<? super T> midVal = (Comparable)list.get(mid);
            int cmp = midVal.compareTo(key);
            if (cmp < 0) {
                low = mid + 1;
            } else {
                if (cmp <= 0) {
                    return mid;
                }

                high = mid - 1;
            }
        }

        return -(low + 1);
    }

//这里内部而是二分,但是在get方法中是通过遍历的方法去查找mid的
private static <T> int iteratorBinarySearch(List<? extends Comparable<? super T>> list, T key) {
        int low = 0;
        int high = list.size() - 1;
        ListIterator i = list.listIterator();

        while(low <= high) {
            int mid = low + high >>> 1;
            //这个get方法中是通过和mid比较,然后如果是小于mid,就往后一直遍历,如果大于mid,就往前一直遍历
            Comparable<? super T> midVal = (Comparable)get(i, mid);
            int cmp = midVal.compareTo(key);
            if (cmp < 0) {
                low = mid + 1;
            } else {
                if (cmp <= 0) {
                    return mid;
                }

                high = mid - 1;
            }
        }

        return -(low + 1);
    }

为什么LinkedList两种方式查找时间不同

上面的例子我们也看到了,LinkedList随机访问的方式和顺序访问的方式效率居然差了这么多,这又是为什么呢?我们直接到源码中去找答案:

1、随机访问方式

public E get(int index) {
	//1、校验下标是否规范
    this.checkElementIndex(index);
    //然后通过下标查询
    return this.node(index).item;
}

LinkedList.Node<E> node(int index) {
	//变量x
    LinkedList.Node x;
    int i;
    //如果查询的下标小于整个集合长度的1/2
    if (index < this.size >> 1) {
    	//那么就从头开始遍历查找
        x = this.first;
        for(i = 0; i < index; ++i) {
            x = x.next;
        }

        return x;
    } else {
    	//如果这个下标是大于总长度的1/2
        x = this.last;
		//就从后往前开始遍历
        for(i = this.size - 1; i > index; --i) {
            x = x.prev;
        }

        return x;
    }
}

2、顺序访问方式

首先是获取迭代器的流程

//首先获取迭代器 ListItr
Iterator<String> it = list.iterator();

//iterator方法会进入到这里面
public ListIterator<E> listIterator() {
	//返回一个linkedList实现的迭代器
    return this.listIterator(0);
}

public ListIterator<E> listIterator(int index) {
	//检查索引位置
    this.checkPositionIndex(index);
    //然后返回LinkedList的迭代器
    return new LinkedList.ListItr(index);
}

private class ListItr implements ListIterator<E> {
     private LinkedList.Node<E> lastReturned;
     private LinkedList.Node<E> next;
     private int nextIndex;
     //期望修改次数
     private int expectedModCount;

     ListItr(int index) {
     	 //实际修改次数赋值给期望修改次数
         this.expectedModCount = LinkedList.this.modCount;
         //0 == this.size, 实际上就是调用后面的node(index)方法
         this.next = index == LinkedList.this.size ? null : LinkedList.this.node(index);
         this.nextIndex = index;
     }
}

LinkedList.Node<E> node(int index) {
    LinkedList.Node x;
    int i;
    //一开始初始化迭代器的时候index = 0
    //同样如果是index < 长度的一半,就从头开始
    if (index < this.size >> 1) {
        x = this.first;

        for(i = 0; i < index; ++i) {
            x = x.next;
        }

        return x;
    } else {
    	//否则就从后面向前遍历
        x = this.last;

        for(i = this.size - 1; i > index; --i) {
            x = x.prev;
        }

        return x;
    }
}

然后下面是调用 hasNext

public boolean hasNext() {
	//判断下一个下标有没有小于整个的大小,如果小于就表示没有到结尾
 	return this.nextIndex < LinkedList.this.size;
}

最后调用next

public E next() {
	//检查实际修改次数和期望修改次数是不是一样,如果不是就证明出现了并发修改异常
    this.checkForComodification();
    //如果没有下一个了,就抛异常
    if (!this.hasNext()) {
        throw new NoSuchElementException();
    } else {
    	//将链表的第一个元素赋值给lastReturned
        this.lastReturned = this.next;
        //将下一个元素赋值给next,下一次访问就从next开始访问
        this.next = this.next.next;
        //nextIndex++
        ++this.nextIndex;
        //返回获取到的当前下标的数据
        return this.lastReturned.item;
    }
}

最后总结下来就是,顺序访问首先初始化迭代器的时候使用了一次二分查找,然后后续的查找过程都是在上一次的基础上继续往下遍历的。而随机访问每一次都要经过二分查找,所以就变得很慢了。



2.4 AbstractList抽象类

这个类里面的 get、set、add、remove等方法都是留给子类重写的,当然了里面也有很多已经写好的方法,有兴趣可以去看看。





如有错误,欢迎指出!!!!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值