文章目录


此文章中使用的是JDK1.8

一、 ArrayList继承关系

1-1 Serializable标记性接口

序列化:将对象的数据写入到文件(写对象)

反序列化:将文件中对象的数据读取出来(读对象)

附:toString的优化

原始的toString方法如下所示,字符串常量在拼接的时候会产生很多垃圾占用内存空间

    public String toString() {
        return "Student{" +
                "username='" + username + '\'' +
                ", password='" + password + '\'' +
                '}';
    }

使用StringBuilder可以解决此问题

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("Student{username='");
        sb.append(this.username);
        sb.append("', password=");
        sb.append(this.password);
        sb.append("'}");
        return sb.toString();
    }

1-2 Cloneable标记性接口

  1. 介绍

    一个类实现 Cloneable 接口来指示 Object.clone() 方法,该方法对于该类的实例进行字段的复制是合法的。在不实现 Cloneable 接口的实例上调用对象的克隆方法会导致异常 CloneNotSupportedException 被抛出。

  2. 克隆的前提条件

被克隆对象所在的类必须实现 Cloneable 接口

必须重写 clone 方法

  1. clone的基本使用
    public static void main(String[] args) {
        ArrayList list = new ArrayList();
        list.add("123");
        list.add("456");
        list.add("789");
        Object clone = list.clone();
        System.out.println(clone);
    }
  1. clone方法底层的实现

    ArrayList源码解析,完整版!_数组

clone的案例

需求:

案例:已知 A 对象的姓名为小胖,年龄33 。由于项目特殊要求需要将该对象的数据复制另外一个对象B 中,并且此后 A 和 B 两个对象的数据不会相互影响

  1. 使用浅拷贝的方式实现

    步骤:

    ①需要拷贝的类实现Cloneable接口并重写其clone方法

    ②修改clone方法的访问权限为public,也可以修改返回值类型为当前类的类名

    public class Student implements Serializable,Cloneable {
        private String name;
        private Integer age;
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public Integer getAge() {
            return age;
        }
    
        public void setAge(Integer age) {
            this.age = age;
        }
    
        /** 此方法需要修改访问权限为public  也可以修改返回值类型为当前类的类名
         * @return
         * @throws CloneNotSupportedException
         */
        @Override
        protected Object clone() throws CloneNotSupportedException {
            return super.clone();
        }
    }
    
        @Test
        public void test01() throws CloneNotSupportedException {
            Student student = new Student();
            student.setName("小胖");
            student.setAge(33);
            Object clone = student.clone();
            System.out.println(student == clone);
        }
    

浅拷贝的局限性

存在的问题:基本数据类型可以达到完全复制,引用数据类型则不可以

原因:在学生对象s被克隆的时候,其属性skill(引用数据类型)仅仅是拷贝了一份引用,因此当skill的值发生改变时,被克隆对象s的属性skill也将跟随改变

//实体类中Skill是引用数据类型
public class Student implements Serializable,Cloneable {
    private String name;
    private Integer age;
    private Skill skill;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public Skill getSkill() {
        return skill;
    }

    public void setSkill(Skill skill) {
        this.skill = skill;
    }

    public Student(String name, Integer age, Skill skill) {
        this.name = name;
        this.age = age;
        this.skill = skill;
    }

    /** 此方法需要修改访问权限为public  也可以修改返回值类型为当前类的类名
     * @return
     * @throws CloneNotSupportedException
     */
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", skill=" + skill +
                '}';
    }
}
    @Test
    public void test02() throws CloneNotSupportedException {
        Skill skill = new Skill("倒拔垂杨柳");
        Student student = new Student("小胖",33,skill);
        Object clone = student.clone();
        System.out.println(skill == clone);
        System.out.println(student);
        System.out.println(clone);
        System.out.println("===========================");
        skill.setName("拳打giao方杰");
        System.out.println(student);
        System.out.println(clone);
    }

执行结果如下图所示

ArrayList源码解析,完整版!_数组_02

  1. 使用深拷贝的方式解决上面的问题

    解决步骤:

    ①Student中的引用数据类型也实现Cloneable接口

    ②修改Student对象的clone方法,手动clone引用数据类型并赋值

    public class Skill implements Serializable,Cloneable {
        //詳細代碼以忽略
    }
    
    //Student类重写clone方法
        @Override
        protected Object clone() throws CloneNotSupportedException {
            /*return super.clone();*/
            Student student = (Student) super.clone();
            Skill skill = (Skill) this.skill.clone();
            student.setSkill(skill);
            return student;
        }
    

    执行结果:

    ArrayList源码解析,完整版!_数据_03

1-3 RandomAccess标记接口

此接口的主要目的是允许通用算法更改其行为,以便在应用于随机访问列表顺序访问列表时提供良好的性能。

随机访问如下

for (int i=0, n=list.size(); i < n; i++) 
	list.get(i);

顺序访问如下

for (Iterator i=list.iterator(); i.hasNext(); ) 
	i.next();

结论:

使用了此接口后,随机访问效率会比顺序访问的效率要高。

    @Test
    public void test03(){
        List list = new ArrayList();
        for(int i=0;i<1000000;i++){
            list.add(i);
        }
        //进行随机访问
        long startTime = System.currentTimeMillis();
        for(int i=0;i<1000000;i++){
            list.get(i);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("随机访问用时: " + ( endTime - startTime ));
        //进行顺序访问
        startTime = System.currentTimeMillis();
        Iterator iterator = list.iterator();
        while (iterator.hasNext()){
            iterator.next();
        }
        endTime = System.currentTimeMillis();
        System.out.println("顺序访问用时: " + ( endTime - startTime ));
    }

执行结果:

ArrayList源码解析,完整版!_数据_04

LinkedList测试随机访问和顺序访问

    @Test
    public void test04(){
        List<String> list = new LinkedList();
        for(int i=0;i<100000;i++){
            list.add(i+"a");
        }
        //进行随机访问
        long startTime = System.currentTimeMillis();
        for(int i=0;i<list.size();i++){
            list.get(i);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("Linked使用随机访问:" + (endTime-startTime));
        startTime = System.currentTimeMillis();
        Iterator iterator = list.iterator();
        while (iterator.hasNext()){
            iterator.next();
        }
        endTime = System.currentTimeMillis();
        System.out.println("Linked使用顺序访问:" + (endTime-startTime));
    }

结论:

LinkedList底层并没有实现RandomAccess接口,使用随机访问(也就是通过索引进行获取时)的时间要远大于进行顺序访问。

执行结果如下:

ArrayList源码解析,完整版!_数据_05

RandomAccess企业应用

场景:

从数据库查询出来的数据,框架会自动封装到List中。如果这时候要对List用for循环进行遍历,需要判断其List有没有实现RandomAccess接口,如果实现了的话 则使用随机访问效率比较高。如果没有实现,则需要使用顺序访问(增强for或者是迭代器)

具体优化代码如下:

		if (list instanceof RandomAccess){
            //进行随机访问
            for (int i=0;i<list.size();i++){
                System.out.println(list.get(i));
            }
        }else{
            //进行顺序访问
            for (String s : list){
                System.out.println(s);
            }
        }

1-4 继承AbstractList抽象类

二、ArrayList源码分析

构造方法

ArrayList源码解析,完整版!_数据_06

  1. 无参构造方法

    //无参的构造方法会将真正存数据的elementData 初始化为一个空的Object数组
    public class ArrayList<E> {
        /**
        * 默认初始容量
        */
        private static final int DEFAULT_CAPACITY = 10;
        /**
        * 空数组
        */
        private static final Object[] EMPTY_ELEMENTDATA = {};
        /**
        * 默认容量的空数组
        */
        private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
        /**
        * 集合真正存储数组元素的数组
        */
        transient Object[] elementData;
        /**
        * 集合的大小
        */
        private int size;
    	public ArrayList() {
    		this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    	}
    }
    
  2. 有参构造一

    	public ArrayList(int initialCapacity) {
            if (initialCapacity > 0) {
                this.elementData = new Object[initialCapacity];
            } else if (initialCapacity == 0) {
                this.elementData = EMPTY_ELEMENTDATA;
            } else {
                throw new IllegalArgumentException("Illegal Capacity: "+
                                                   initialCapacity);
            }
        }
    
  3. 有参构造二

    ArrayList源码解析,完整版!_数组_07

arraycopy方法(扩展)

    @Test
    public void test11(){
        String[] s = new String[]{"000","001","002","003","004","005","006"};
        String[] ss = new String[6];
        //第一个参数代表数据源(也就是想要拷贝的对象)
        //第二个参数为拷贝数据源的起始位置  这里是1,也就是s[1]也就是从001开始拷贝
        //第三个参数为拷贝目的数组对象
        //第四个参数为拷贝目的数组对象的位置  这里是5,也就是从ss[5]开始插入拷贝的数据
        //第五个参数为拷贝数据源的长度,这里为1  也就是只在s数组中拷贝一个元素
        System.arraycopy(s,1,ss,5,1);
        for (String s1 : ss) {
            System.out.println(s1);
        }
    }

执行结果图

ArrayList源码解析,完整版!_java_08

add方法

ArrayList源码解析,完整版!_i++_09

  1. 添加的方法

    ArrayList源码解析,完整版!_java_10

ArrayList源码解析,完整版!_数组_11

  1. 带索引的添加元素

ArrayList源码解析,完整版!_i++_12

  1. addAll方法

    ArrayList源码解析,完整版!_数组_13

  2. addAll方法(带索引)

    @Test
    public void test12(){
        List list1 = new ArrayList();
        list1.add("000");
        list1.add("001");
        list1.add("002");
        list1.add("003");
        list1.add("004");
        List list2 = new ArrayList();
        list2.add("一");
        list2.add("二");
        list2.add("三");
        list1.addAll(3,list2);
        System.out.println(list1);
    }
    

    底层源码

    ArrayList源码解析,完整版!_Java_14

    ArrayList源码解析,完整版!_Java_15

set方法

    @Test
    public void test13(){
        List list = new ArrayList();
        list.add("小胖");
        list.add("小瘦");
        list.add("giao方杰");
        System.out.println(list.set(1, "凹凸曼"));
        System.out.println(list);
    }

ArrayList源码解析,完整版!_i++_16

get方法

ArrayList源码解析,完整版!_java_17

toString方法

ArrayList源码解析,完整版!_java_18

Iterator方法

    @Test
    public void test15(){
        List list = new ArrayList();
        list.add("小胖");
        list.add("小瘦");
        list.add("giao方杰");
        Iterator iterator = list.iterator();
        while (iterator.hasNext()){
            Object next = iterator.next();
            System.out.println(next);
        }
    }

ArrayList源码解析,完整版!_Java_19

remove方法

ArrayList源码解析,完整版!_i++_20

ArrayList源码解析,完整版!_数据_21

并发修改异常

    @Test
    public void test17(){
        List list = new ArrayList();
        list.add("小胖");
        list.add("der子");
        list.add("giao方杰");
        list.add("凹凸曼");
        Iterator iterator = list.iterator();
        while (iterator.hasNext()){
            Object next = iterator.next();
            if (next.equals("der子")){
                list.remove(next);
            }
        }
    }

ArrayList源码解析,完整版!_数组_22

结论:

  1. 集合在执行add和remove方法的时候,实际修改次数都会+1
  2. 在获取迭代器的时候,集合只会执行一次

判断下面的代码会不会出现并发修改异常

    @Test
    public void test17(){
        List list = new ArrayList();
        list.add("小胖");
        list.add("der子");
        list.add("giao方杰");
        list.add("凹凸曼");
        Iterator iterator = list.iterator();
        while (iterator.hasNext()){
            Object next = iterator.next();
            if (next.equals("giao方杰")){
                list.remove(next);
            }
        }
        System.out.println(list);
    }

执行结果:

ArrayList源码解析,完整版!_Java_23

原因:

删除元素是会让实际修改次数(modCount)+1。同时从remove的源码可以看到,会让size-1.

当删除倒数第二个元素时,删除完成后会刚好指针(cursor)== size

ArrayList源码解析,完整版!_数组_24

这时hasNext会返回false,就不会进入到while循环中,也就不会执行iterator.next()方法。就不会出现并发修改异常了。

解决并发修改异常 (迭代器的default remove方法)

代码如下:

	@Test
    public void test18(){
        List list = new ArrayList();
        list.add("小胖");
        list.add("der子");
        list.add("giao方杰");
        list.add("凹凸曼");
        Iterator iterator = list.iterator();
        while (iterator.hasNext()){
            Object next = iterator.next();
            if (next.equals("der子")){
                iterator.remove();
            }
        }
        System.out.println(list);
    }

ArrayList源码解析,完整版!_i++_25

总结:

①其实迭代器的remove底层还是调用的集合的remove方法

②只不过每次都会将预期修改次数进行赋值 所以不会产生并发修改异常

clear方法

作用:清空集合中的所有元素

ArrayList源码解析,完整版!_i++_26

contains方法

作用:查找集合中是否包含某个元素

ArrayList源码解析,完整版!_java_27

isEmpty方法

ArrayList源码解析,完整版!_Java_28

三、面试题

①ArrayList是怎么扩容的?

源码分析查看标题二中的add方法

第一次扩容10
以后每次都是原容量的1.5倍

②ArrayList频繁扩容导致添加性能急剧下降,如何处理?

使用ArrayList指定容量的构造方法创建ArrayList

    @Test
    public void test21(){
        long startTime = System.currentTimeMillis();
        List list = new ArrayList();
        for(int i=0;i<10000000;i++){
            list.add(i);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("不断扩容所用的时间:" + (endTime-startTime));

        startTime = System.currentTimeMillis();
        List list1 = new ArrayList(10000000);
        for(int i=0;i<10000000;i++){
            list1.add(i);
        }
        endTime = System.currentTimeMillis();
        System.out.println("初始化容量所用时间:" + (endTime-startTime));
    }

测试结果:

ArrayList源码解析,完整版!_i++_29

③ArrayList插入或删除元素一定比LinkedList慢么?

根据索引删除元素

LinkedList在删除元素的时候不一定比ArrayList快,有时候结果可能相反

ArrayList源码解析,完整版!_Java_30

下图为LinkedList删除数据

ArrayList源码解析,完整版!_数组_31

④ArrayList是线程安全的吗?

答案:不是

解决方案:

使用Vector

    public void test23() throws InterruptedException {
        List list = new Vector();
        list.add("123");
    }

使用集合工具类将ArrayList转为线程安全的

    @Test
    public void test23() throws InterruptedException {
        List list = new ArrayList();
        List list1 = Collections.synchronizedList(list);
        list1.add("111");
    }

⑤如何复制某个ArrayList到另一个ArrayList中去?

使用clone()方法

    @Test
    public void test23() throws InterruptedException {
        ArrayList list = new ArrayList();
        list.add("小胖");
        list.add("giao方杰");
        list.add("小瘦");
        ArrayList list1 = new ArrayList();
        list1 = (ArrayList) list.clone();
        System.out.println(list);
        System.out.println(list1);
    }

使用ArrayList构造方法

    public void test23() throws InterruptedException {
        List list = new ArrayList();
        list.add("小胖");
        list.add("giao方杰");
        list.add("小瘦");
        List list1 = new ArrayList(list);
        System.out.println(list1);
    }

使用addAll方法

    public void test23() throws InterruptedException {
        List list = new ArrayList();
        list.add("小胖");
        list.add("giao方杰");
        list.add("小瘦");
        List list1 = new ArrayList();
        list1.addAll(list);
    }

⑥已知成员变量集合存储N多用户名称,在多线程的环境下,使用迭代器在读取集合数据的同时如何保证还可以正常的写入数据到集合?

这时需要使用读写分离的集合CopyOnWriteArrayList

/
/线程任务类
class CollectionThread implements Runnable{
	private static CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
    static{
        list.add("Jack");
        list.add("Lucy");
        list.add("Jimmy");
    } 
    @Override
    public void run() {
        for (String value : list) {
            System.out.println(value);
            //在读取数据的同时又向集合写入数据
            list.add("coco");
        }
    }
}
//测试类
public class ReadAndWriteTest {
    public static void main(String[] args) {
        //创建线程任务
        CollectionThread ct = new CollectionThread();
        //开启10条线程
        for (int i = 0; i < 10; i++) {
        	new Thread(ct).start();
        }
    }
}

⑦ArrayList 和 LinkList区别?

ArrayList

基于动态数组的数据结构

对于随机访问的get和set,ArrayList要优于LinkedList

对于随机操作的add和remove,ArrayList不一定比LinkedList慢 (ArrayList底层由于是动态数组,因此
并不是每次add和remove的时候都需要创建新数组)

LinkedList

基于链表的数据结构
对于顺序操作,LinkedList不一定比ArrayList慢
对于随机操作,LinkedList效率明显较低