ArrayList底层原理

ArrayList 查找数据的时间复杂度

ArrayList的底层实现,是数组,那么数组的特性则是定长,可以通过下标查找数据,那么大家可能要说,ArrayList底层是数组,怎么做到不定长的呢?接下来我们带着这个问题来解析ArrayList源码

  1. AyyayList的时间复杂度,有三种情况
    1)通过下标查找,可以直接定位,则时间复杂度为O(1);
    2)通过二分法查找,则时间复杂度为O(logN)
    3) 通过遍历数组方法查找,每一个数据都需要查找,则时间复杂度为O(N)

ArrayList 构造函数

ArrayList的构造函数有三个,分别为如下代码,从代码中可以看出来,ArrayList对象实例化的时候,初始容量时0,并不是是实例化的时候就会有容量

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{ //实现序列化接口,需要有一个序列号
    private static final long serialVersionUID = 8683452581122892189L;
//默认扩容长度为10
    private static final int DEFAULT_CAPACITY = 10;
//默认空的数组
    private static final Object[] EMPTY_ELEMENTDATA = {};
//默认扩容的空数组
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//数组定义
    transient Object[] elementData; // non-private to simplify nested class access
//数组长度
    private int size;
//带一个初始化长度的构造函数
    public ArrayList(int initialCapacity) {
    //判断初始容量是否大于0
        if (initialCapacity > 0) {
        //在推内存中为数组开辟长为initialCapacity的数组
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
        //如果初始容量为0;则等于空数组
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
        //
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }
//无参构造
    public ArrayList() {
    //默认扩容的数组,初始容量为0;
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
//有数据的有参构造
    public ArrayList(Collection<? extends E> c) {
    //将Collection数据给数组
        elementData = c.toArray();
        //集合长度就是数组长度
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            //反射机制,
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

ArrayList add方法与扩容机制

1.arrayList是一个可变长,值可以重复的集合,那是怎么实现的呢?可以看如下源码中,并没有校验该数据,数组中是否已经存在了,只有校验数组的长度是否已经到达容量,通过grow方法,有一个Arrays.copyOf()方法,在往下追,通过System.arraycopy,该方法是一个native修饰的方法,放在本地方法栈中属于线程独占区,所以,如果有比较运行速度,那么System.arraycopy > Arrays.copyOf()

 public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
 private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
private static int calculateCapacity(Object[] elementData, int minCapacity) {
//判断如果集合数组等于默认扩容数组,默认长度为0
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        //DEFAULT_CAPACITY =10 ,
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
        }
        //第一次的时候
        // a(DEFAULT_CAPACITY) = 10 ,b(minCapacity)=1;
         public static int max(int a, int b) {
        return (a >= b) ? a : b; 返回a=10
    }
//modCount 在AbstractList类中定义,作为记录数据结构改变的次数
protected transient int modCount = 0;
 private void ensureExplicitCapacity(int minCapacity) {
        modCount++;   //结构变更次数

        // overflow-conscious code
        //正常情况下minCapacity =elementData.length
        //只有当数组长度超过容量是,才会触发
        if (minCapacity - elementData.length > 0)
        //开始扩容
            grow(minCapacity);
    }
    //最大集合长度
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private void grow(int minCapacity) {
        // overflow-conscious code
        //数组长度
        int oldCapacity = elementData.length;
        // (oldCapacity >> 1),比如10 ,二进制为00001010 往左移一位为00000101  =5
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //如果新数组长度小于最小容量,则数组长度为最小容量
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
            //如果新数组长度大于
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        //将旧数组拷贝到新的数组中
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
  private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }
//下面是Arrays中的代码
  public static <T> T[] copyOf(T[] original, int newLength) {
        return (T[]) copyOf(original, newLength, original.getClass());
    }

看过代码知道了,ArrayList其实在实例化的时候并没有在推内存中开闭空间,而是第一次执行add()方法的时候,才进行扩容,并且只有在当前数组长度已经超过当时扩容大小的时候,才会走扩容方法,其实白话的说法:根本上将就是一个数组存放数据放不下去了,但是我又不知道还有多少数据需要放,我就新创建一个数组,数组长度是之前数组长度的1.5倍,并且将之前数组中的数组放到现在数组中,
在这里插入图片描述

ArrayList 并发修改异常

并发修改异常(ConcurrentModificationException)一般发生在使用Iterator(迭代器)遍历集合,或者foreach(底层通过Iterator实现)时 修改集合的结构发生,而通过for循环,或者使用Iterator.remove并不会出现并发修改异常,这里面有一个重要的成员变量modCount(记录集合结构变更的次数)。

 */
public class arraylistTest {
    public static void main(String[] args) {

        //请动手实践运行一下
        List<String> list = new ArrayList<String>();
        list.add("a");
        list.add("b");
        list.add("e");
        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            String str = iterator.next();
            if (str.equals("e")) {
                list.remove(str); //注意,时使用list的remove方法
            } else {
                System.out.println(str);
            }
        }
    }
}

当我们之前上面的代码,一定会发生并发修改异常,接下来我们看一下源码,为什么会发生并发修改异常

//进入AbstractList.java类中,查找
  public Iterator<E> iterator() {
        return new Itr();
    }
    //进入Itr类中
    private class Itr implements Iterator<E> {
        //游标,下标
        int cursor = 0;
        //上一个下标
        int lastRet = -1;
        //期望中的modcount,初始化时与将modcount赋值给它,
        //modCount 数据结构修改次数
        int expectedModCount = modCount;
        
        //判断当前数组是否有下一个数据
        public boolean hasNext() {
        //这个判断非常重要,看清楚,使用 !=  比较
            return cursor != size();
        }
        
        //指针指向下一个数据
        public E next() {
        //检测当前结构是否修改,当前修改次数与期待的修改次数是否相等,等于,则结构不变,否则发生并发修改异常
            checkForComodification();
            try {
                //游标时成员变量,初始值为0
                int i = cursor;
                //获取当前下标下的值
                E next = get(i);
                //上一个值为与i
                lastRet = i;
                //游标指向下一个数据
                cursor = i + 1;
                return next;
            } catch (IndexOutOfBoundsException e) {
                checkForComodification();
                throw new NoSuchElementException();
            }
        }
		//当前方法调用需要通过迭代器调用 iterator.remove
        public void remove() {
        //判断上一个值是小于0 ,lastRet最小为0 ,最大为数组长度-1;
            if (lastRet < 0)
                throw new IllegalStateException(); 
            checkForComodification();//检查并发修改异常

            try {
            //移除当前数组下标为lastRet上的值,也就是现在遍历符合条件的下标下的数据
                AbstractList.this.remove(lastRet);
                删除成功后,游标需要变成lastRet的值,因为该数据后的值会往前移一位                
                if (lastRet < cursor)
                    cursor--;
                lastRet = -1;
                //将修改次数更新
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException e) {
                throw new ConcurrentModificationException();
            }
        }
        //检查是否发生并发修改异常
        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }

一般我们删除的数据都是在中间的位置,有的就在想,如果我删除的是最后一个数据呢,逼近remove方法中并不会触发并发修改异常,触发实在next方法中 ,比如我现在删除最后一个数据,在走next的时候不会有任何异常,然后走到remove()方法,一致都不会有异常,然后while循环旧结束了,但是我们看hasNext()方法中,

return cursor != size();

现在我有3个数据,那么index为【02】,size为【13】,当cursor为2时,size =3,则while 判断为true
那么remove方法后
index为【01】,size为【12】,cursor ++;-> 3, size = 2,while 判断为true,则就算我删除的时最后一个数据,那么还会有一次判断通过,走next()方法,这个时候触发并发修改异常,并且不会影响之前的结果。

//我们来看看导致并发修改异常的remove代码和iterator.remove() 方法的区别,下面时ArrayList自带的删除代码

//方法的重载:方法名相同,参数不同,返回值不同
//该方法时通过数组下标来删除数据
  public E remove(int index) {
        rangeCheck(index);

        modCount++;
        E oldValue = elementData(index);

        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }
    //我们分析的是这个romove方法
    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;
    }
    
    private void fastRemove(int index) {
    //数组结构改变,当modCount改变时,并没有更新ExceptMogCount,则两个变量的值不一致,当值后续触发并发修改异常
        modCount++;
        //
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work
    }

并发修改异常解决办法

public class arraylistTest {
    public static void main(String[] args) {

        //请动手实践运行一下
        List<String> list = new ArrayList<String>();
        list.add("a");
        list.add("b");
        list.add("e");
        
      //  Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            String str = iterator.next();
            if (str.equals("e")) {
            //并发修改异常
              //  list.remove(str); //注意,时使用list的remove方法
              //解决办法一。
              iterator.remove();
            } else {
                System.out.println(str);
            }
        }
        //解决办法三
        for (int i = 0 ; i < list.size()-1; i ++){
            if (list.get(i).equals("e")){
                list.remove(i);
                i--;
            }else {
                System.out.println(list.get(i));
            }
        }
    }
}

序列化与反序列化

把对象转换为字节序列的过程称为对象的序列化。 
把字节序列恢复为对象的过程称为对象的反序列化。

 序列化版本ID的真实用途:当实体中增加属性后,文件流中的class和classpath中的class,也就是修改过后的class,不兼容了,处于安全
 机制考虑,程序抛出了错误,并且拒绝载入。那么如果我们真的有需求要在序列化后添加一个字段或者方法呢?应该怎么办?那就是自己
 去指定serialVersionUID。在例子中,如果没有指定Person类的serialVersionUID的,那么java编译器会自动给这个class进行一个摘要算
 法,类似于指纹算法,只要这个文件多一个空格,得到的UID就会截然不同的,可以保证在这么多类中,这个编号是唯一的。所以,添加
 了一个字段后,由于没有显指定serialVersionUID,编译器又为我们生成了一个UID,当然和前面保存在文件中的那个不会一样了,于是就
 出现了2个序列化版本号不一致的错误。因此,只要我们自己指定了serialVersionUID,就可以在序列化后,去添加一个字段,或者方法,
 而不会影响到后期的还原,还原后的对象照样可以使用,而且还多了方法或者属性可以用。可以说serialVersionUID是序列化和反序列化之
 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值