细谈ArrayList和LinkedList (1)

时间:2020-10-22 作者:admin

引言

作为java开发人员,相信大家对ArrayList和LinkedList都不陌生,new她分分钟一个。而且ArrayList查询快,增删慢and LinkedList增删快,查询慢的特点可谓无人不知,无人不晓。但是作为庞大的JDK集合团队允许你两句话就能做出解释吗,或者说疑惑的你就不想证明点什么?虽然每天跟她在代码中恩恩爱爱,你侬我侬,就像跟你的女朋友卿卿我我好多年,但是突然有一天她问起:“你了解我吗?”,
你:“了解!”,
“不,你不了解!”

ArrayList实现接口、继承类

要想知道一个类能做什么,首先得了解她实现了哪些接口以及接口的特点,虽然每天跟她face to face,很多致命细节都会落空。这件事得追溯的几天前,我同事阿涵上周陪她女朋友去逛街,他女朋友看中了一双鞋很漂亮,非得要买,阿涵百般阻止,她女友开口了,说:“如果你能说出我的脚码,我就不买了”, 阿涵犹豫了两秒钟,灵光一闪,看着自己脚底的鞋子跟女友脚底的鞋子,目测差了4公分,
阿涵自信满满的说:“39”,
女友泪目,阿涵以为她被感动了,“阿涵,我们分手吧”,
阿涵一脸懵A?C,就问了:“为什么呀 ?”,
女友:“你不了解我,我们在一起这么久了都不知道我的脚码,都不如专柜的那个帅帅的男生了解我”。
阿涵既愤怒又懊悔,在商场大声哭啼,鼻涕四溅,哀求挽留, “Please don’t leave me, please !”,
女友看出来阿涵对她是真爱,给了他一次机会,说:“阿涵,既然你跟代码在一起的时间比跟我在一起的时间都长,那么给我解释一下ArrayList吧!”
阿涵第一时间喊我,“阿选,帮我搞定ArrayList,我请你吃大餐”,为了挽救阿涵这段来之不易的感情,为了让阿涵头顶上变得风轻云淡,我决定摩擦ArrayList!

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

对比

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
RandomAccess

源码解释大概意思是,他是一个接口标记,支持随机访问(基于下标),用于区分ArrayList和LinkedList,更好的去选择遍历优化方式,提高性能。

Cloneable

Cloneable是一个接口,实现这个接口,重写clone方法,默认调用父类clone方法。
我举一个例子,便于理解:
定义一个pojo类,实现Cloneable接口,重写父类clone方法
浅拷贝

public class BallTeam implements Cloneable, Serializable {

    private static final long serialVersionUID = -3998490931149880710L;

    private String name;
    private int championNum;
    private Player player;

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

属性Player:

public class Player implements Cloneable {

    private String playerName;
    private int playerAge;

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

编写测试方法:

private static void copyDepth() throws CloneNotSupportedException {
        BallTeam ballTeamNo = new BallTeam("Laker", 17);
        Player playerNo = new Player("James", 36);
        ballTeamNo.setPlayer(playerNo);

        BallTeam ballTeamClone = (BallTeam) ballTeamNo.clone();
        ballTeamNo.setname("Heat");
        ballTeamNo.setChampionNum(18);
        ballTeamNo.getPlayer().setPlayerName("Davis");

        System.out.println("noClone playerName : " + ballTeamNo.getPlayer().getPlayerName());
        System.out.println("clone playerName   : " + ballTeamClone.getPlayer().getPlayerName());

        System.out.println("noClone name       : "+ballTeamNo.getname());
        System.out.println("clone name         : "+ballTeamClone.getname());

        System.out.println("noClone championNum: "+ballTeamNo.getChampionNum());
        System.out.println("clone championNum  : "+ballTeamClone.getChampionNum());

        System.out.println("noClone playerObj  : "+ballTeamNo.getPlayer());
        System.out.println("clone playerObj    : "+ballTeamClone.getPlayer());
    }

打印结果:
细谈ArrayList和LinkedList (1)
基本数据类型:clone完成后championNum不会跟源变量改变而变化,所以他是独立的。
String类型:name没发生变化,也是独立的,但是两个name指向字符串常量池中不同的常量;
引用类型:在栈中引用的是堆内存的同一对象Player,指向相同的地址,所以Player playerName发生改变,也就是引用的对象属性发生改变。

深拷贝
所有引用类型变量除了String,都要实现Cloneable接口(数组可以直接调用clone方法),重写clone方法,并且各自调用父类的clone方法,重新赋值。
修改BallTeam类中重写的clone方法:

 @Override
    protected Object clone() throws CloneNotSupportedException {
        BallTeam ballTeam = (BallTeam) super.clone();
        ballTeam.setPlayer((Player) player.clone());
        return ballTeam;
    }

执行结果:
细谈ArrayList和LinkedList (1)
很明显引用类型变量经过clone,并且指向了不同地址,源变量不受影响。
类似于java方法传参:
基本数据类型 是在栈中复制一份,然后在方法体中执行,复制的是变量名和值,方法体中变量改变不会影响源变量;
引用数据类型 复制的是变量名和引用地址,地址指向堆内存的对象,对象变了,源变量也会变;
String类型 被final修饰的不可变对象,重新赋值时,会在常量池中找存在的字符串,如果有,直接取地址,如果没有,重新new一个,并且将地址赋值给栈中的变量,源变量不改变。

Serializable

序列化的目的

  1. 存盘
  2. 网络传输

序列化定义
将内存中的对象转化成流的形式并持久化磁盘中;
反序列化定义
以流的形式恢复成对象。

如果一个类实现了Serializable接口,就是通知jvm你要帮我序列化,并且会默认生成一个serialVersionUID,反序列化时jvm会自动检测文件中的serialVersionUID,判断它是否与当前类中的serialVersionUID一致,如果不一致,会导致InvalidClassException异常。
写一个序列化反序列化的方法:

private static void serializerTrain() throws IOException, ClassNotFoundException {
        List<BallTeam> ballTeams = new ArrayList<>();
        ballTeams.add(new BallTeam("Laker",17));
        ballTeams.add(new BallTeam("Celtics",17));
        ballTeams.add(new BallTeam("Bulls",6));
        ballTeams.add(new BallTeam("Warriors",6));

        FileOutputStream fos = new FileOutputStream("C:\\basic_code\\it\\october\\src\\com\\practice\\collection\\obj.txt");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(ballTeams);
        oos.flush();
        oos.close();


        FileInputStream fis = new FileInputStream("C:\\basic_code\\it\\october\\src\\com\\practice\\collection\\obj.txt");
        ObjectInputStream ois = new ObjectInputStream(fis);
        List<BallTeam> ballTeamList = (List<BallTeam>) ois.readObject();
        ois.close();
    }

如果BallTeam不实现Serializable这个接口,反序列化会报错,
执行:
细谈ArrayList和LinkedList (1)

AbstractList

继承这个抽象类说明就拥有增、删、查、改的功能。

List

这里大家都有疑问,你说你都继承了AbstractList了,为什么还要实现List接口?

I’ve asked Josh Bloch, and he informs me that it was a mistake. He used to think, long ago, that there was some value in it, but he since “saw the light”. Clearly JDK maintainers haven’t considered this to be worth backing out later.

意思就是:我问过Josh Bloch,他告诉我这就是个错误,并且以后会取消。
当然这不是我问的。

ArrayList属性

	//序列化版本号,类文件标签,类的内容改变会导致反序列化失败。
 private static final long serialVersionUID = 8683452581122892189L;

    /**
     * 未指定实例化容量的默认数组长度
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * 初始化ArrayList指定长度(下面要考)指向这个数组
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
     * 与EMPTY_ELEMENTDATA 区分开来,
     * 初始化ArrayList不指定长度指向此数组,默认长度为10,并且会按照相应的扩容因子扩容。
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     * 真正存放元素的地方(数组,下面要考)
     */
    transient Object[] elementData; 

    /**
     * 真正存放元素个数
     */
    private int size;

ArrayList构造

	 /**
     *如果指定长度并且长度大于0,就new一个指定长度的数组指向elementData,
     *如果等于0,EMPTY_ELEMENTDATA空数组属性指向elementData 
     */
    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);
        }
    }

    /**
     *无参构造,初始化一个空数组,并在第一次添加的时候扩大长度为10
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    /**
     * 有参构造添加一个集合对象,并将集合对象转换成数组赋值给elementData 
     * 判断长度是否为0,如果为0,相当于new ArrayList(0),
     * 如果不为0,判断该数组是否是Object[].class类型的,
     * 如果不是就重新copy到new的新数组中,copy长度为原数组长度和size中的最小值
     */
    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // ArrayList属性EMPTY_ELEMENTDATA空数组指向并赋值给elementData 
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

添加元素

 	/**
     * 确认数组容量,判断是不是需要扩容,操作数modCount++,元素个数size++
     */
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
    
	private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
	/**
	 *在构造方法中初始化未指定长度会有这一步操作
	 *elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA,所以这里判断成功,如果size+1 < 10 這里返回的长度就是10。
	 *如果指定了长度,在这里会直接返回size+1
	 */
	 private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }
    /**
     * 如果说size+1 > elementData.length ,也就是要添加的元素容量超过原来数组容量,
     * 就需要扩容
     * 初始化不指定容器长度,会在第一次添加的时候扩容长度到10
     */
	private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

指定下标添加


    public void add(int index, E element) {
        rangeCheckForAdd(index);
        
        ensureCapacityInternal(size + 1);  // 同上,确认数组长度,需不需要扩容
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);//将数组此下标以及此下标往后的数据往后复制一格,是复制不是移动,将新的下标位置和后一个下标位置都是指向相同元素(下图)
        elementData[index] = element;//将插入的元素赋值给该下标
        size++;
    }

细谈ArrayList和LinkedList (1)

扩容


    private void grow(int minCapacity) {
      
        int oldCapacity = elementData.length;//获取数组长度
        int newCapacity = oldCapacity + (oldCapacity >> 1);//将数组长度扩大1.5倍
        if (newCapacity - minCapacity < 0)//扩大1.5倍不够,直接将所需容量赋值
        	//一般不指定初始化容量,数组长度为0,扩大1.5倍还是0,会走这一步
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)//如果扩容1.5倍太大,直接用Integer.MAX_VALUE
        	//(minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE
            newCapacity = hugeCapacity(minCapacity);
        //将原数组elementData的数据复制到大小为newCapacity的新数组(new出来的)当中,并且重新赋值给elementData
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

移除元素

 	public E remove(int index) {
        rangeCheck(index);//检查索引是否合理

        modCount++;//操作数标记
        E oldValue = elementData(index);//获取具体元素

        int numMoved = size - index - 1;
        if (numMoved > 0)
        //这里主要判断移除的元素下标是不是最后一个,如果是,直接将下标元素置为null,
        //如果不是最后一个,该下标后边的所有元素往前复制一格,也是将最后一位置空
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; //无引用的元素会被GC回收
        return oldValue;
    }
    //遍历下标所有元素,对元素是否为空做了判断,如果该下标元素为null也会被移除,主要目的还是找到想要移除元素o的下标,知道下标同上remove(int index),只不过又另写了一个方法fastRemove(int index),删除下标为index的元素
    public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }
    //约等于remove(int index),只不过这里不用做索引是否合法校验,不用返回具体元素
    private void fastRemove(int index) {
        modCount++;
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; 
    }

迭代器

 private class Itr implements Iterator<E> {
        int cursor;       // 代表下一个要访问的元素下标
        int lastRet = -1; // 代表上一个要访问的元素下标
        int expectedModCount = modCount;//是跟ArrayList 操作数modCount保持一致的一个期望值

        Itr() {}

		//如果下一元素的下标等于元素个数就证明已经迭代结束了,返回
        public boolean hasNext() {
            return cursor != size;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            checkForComodification();//检查expectedModCount和modCount是否相等,如果迭代中间,删除或添加过元素,会抛异常ConcurrentModificationException
            int i = cursor;
            if (i >= size) //针对cursor做出判断,是不是一次性迭代数代超过元素个数
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;//cursor 初始化为0,lastRet初始化为-1,第一次迭代后为0,每一次迭代cursor 和lastRet 都会+1,
            return (E) elementData[lastRet = i];
        }

        public void remove() {
            if (lastRet < 0)//如果不进行迭代,直接删除元素会报错,因为初始化lastRet =-1
         		
                throw new IllegalStateException();
             //只有调用next方法,lastRet 被赋值后才会执行,
            checkForComodification();//校验同上

            try {
                ArrayList.this.remove(lastRet);//这里的modCount会+1
                cursor = lastRet;//元素删除后,将lastRet赋值给cursor 
                lastRet = -1;//lastRet重新置为初始值,
                //也就是说调用next方法后,只能调用一次remove方法,否则就会报错
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

        @Override
        @SuppressWarnings("unchecked")
        public void forEachRemaining(Consumer<? super E> consumer) {
            Objects.requireNonNull(consumer);
            final int size = ArrayList.this.size;
            int i = cursor;
            if (i >= size) {
                return;
            }
            final Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length) {
                throw new ConcurrentModificationException();
            }
            while (i != size && modCount == expectedModCount) {
                consumer.accept((E) elementData[i++]);
            }
            // update once at end of iteration to reduce heap write traffic
            cursor = i;
            lastRet = i - 1;
            checkForComodification();
        }

        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }

fail-fast机制

定义
是java集合(Collection)中的一种错误检测机制。当在迭代集合的过程中该集合在结构上发生改变的时候,就有可能会发生fail-fast,即抛出 ConcurrentModificationException异常。
发生原因
在单线程或多线程环境下,迭代过程中元素被添加或删除。
应用场景
ArrayList、HashMap中都有一个属性叫modCount,每次对集合的修改(增加、删除)这个值都会加1,在迭代遍历前记录这个值到expectedModCount中,遍历中检查两者是否一致,如果出现不一致就说明modCount发生修改,则抛出ConcurrentModificationException异常。
描述
并不是所有集合都有这种机制,ConcurrentHashMap、CopyOnWriterArrayList等都是没有。
底层数组结构的集合基于下标存取(set、get)效率比较高,时间复杂度为O(1),像查找(index of、contains、lastIndexOf…)、插入(add)、删除(remove)时间复杂度为O(n)

总结

并不是ArrayList一定比LinkedList 增删慢。主要原因是ArrayList在不指定初始化长度的情况下会频繁的grow、arraycopy,效率很低。

private static void arrayAndLinkedCompare() {
        int len = 10000000;
        ArrayList<Integer> arrayList1 = new ArrayList<>(len);
        LinkedList<Integer> linkedList = new LinkedList<>();

        long start1 = System.currentTimeMillis();
        for (int i = 0; i < len; i++) {
            arrayList1.add(i);
        }
        long end1 = System.currentTimeMillis();
        System.out.println("arrayList1 need time: " + (end1 - start1));

        long start2 = System.currentTimeMillis();
        for (int i = 0; i < len; i++) {
            linkedList.add(i);
        }
        long end2 = System.currentTimeMillis();
        System.out.println("linkedList need time: " + (end2 - start2));
    }

执行结果:
arrayList1 need time: 4806
linkedList need time: 13237
在故事结尾,阿涵成功的挽救的他的爱情,但是他每次才能够一个arrayList1 need time的时间,她女友说了,“阿涵,我需要一个linkedList need time,你再学习一下LinkedList 好吗?”

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。