美文网首页
Arraylist源码--之那些年的面试题

Arraylist源码--之那些年的面试题

作者: fkxuexi | 来源:发表于2018-09-03 11:44 被阅读0次

基于jdk1.8版本

一、本节目录:

  • ArrayList的继承体系
  • Arraylist的重要的属性
  • ArrayList的构造函数
  • ArrayList的扩容
  • ArrayList的核心方法
  • ArrayList的fast fail机制延伸至fast safe

二、继承体系图

image.png

三、重要属性:

  • modCount
    这个参数我们单独讲,与后面讲到的fast fail有关联,同时也与一个著名的以后ConcurrentModificationException有关。
  • 默认初始容量值:
private static final int DEFAULT_CAPACITY = 10;
private int size;
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; //最大容量值2^31-9
这两个变量值我们需要区分开,一个是ArrayList的容量capacity,一个是ArrayList盛装的元素的数量
  • 盛装元素的容器:
 transient Object[] elementData; // transient 表名当前对象不参与序列化
上面所说的size即便是elementData的size
  • 初始化容器:
// 如果使用 0 作为初始化容量的时候,ArrayList就会使用这个对象来初始化
private static final Object[] EMPTY_ELEMENTDATA = {};
// 如果使用默认的构造函数的话,就会使用这个对象初始化
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

四、构造函数:

  • 默认构造
// 直接使用DEFAULTCAPACITY_EMPTY_ELEMENTDATA来初始化
public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
  • 使用指定大小容量的构造
// 当initialCapacity 为0的时候,则会使用EMPTY_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);
        }
    }
  • 使用指定的集合来初始化
public ArrayList(Collection<? extends E> c) {
        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 {
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

五、扩容机制:

5.1 如果自己来写这个扩容的思路

我们先自己分析一下思路(本来想画一个流程图的,但是后来发现不好命名,所以这个只好自己描述一下),首先只有在往容器里面添加数据的时候才会有扩容这一说,首先由于我们初始化的方法不同,所以在初次添加元素的时候需要做特别的处理,然后判断是否超过最大容量,判断在现有的size+1的基础上是否需要扩容,然后扩容,拷贝数组添加元素。

5.2 ArrayList的思路
private void ensureCapacityInternal(int minCapacity) {
/** 如果使用默认的构造参数的话,则选择minCapacity和DEFAULT_CAPACITY中较大的一个来作为最小的初始容量,
大家可能奇怪,既然判断了初始化这一说,为何还会有传入的minCapacity
和DEFAULT_CAPACITY比较这一说,是因为ensureCapacityInternal也会被addAll调用
*/
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        ensureExplicitCapacity(minCapacity);
    }
// 下面这个完全是基于 **溢出来考虑的**
 private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

核心的扩容代码:

 private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

上面的两面不难,大家应该能看懂,不过上面确实有两个考点:

  • 扩容后的新容量:为原来容量的1.5倍
  • x>>y 和 x<<y:
    • 首先大原则:左乘右除
    • 改写: x>>y x/(2^y)
    • 举例:怎样算28最快,2<<3即:2(2*3)
    • 原理:具体的原理需要去掰扯位移运算,这个大家自行百度

六、核心方法:

  • 添加: 从源码看ArrayList是可以添加null的同时是可以添加重复元素的
public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
  • 判断空和容量,都是基于size这个成员属性来进行判断的
public int size() {
        return size;
}
 public boolean isEmpty() {
        return size == 0;
}
  • indexOf:是返回首个和指定元素相等的索引
 public int indexOf(Object o) {
        if (o == null) {
            for (int i = 0; i < size; i++)
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = 0; i < size; i++)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }
  • remove:大家知道数组的一大优势是查询索引快,但是缺点就是对于元素的删减操作效率比较低,所以与之互补的一个数据结构为链表,在java中对应的则是LinkedList
 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;
    }

图示如下:


image.png

还有很多方法,这里不再一一介绍,因为其他的方法比较常规,所以直接看api文档即可,下面重点来介绍一下一个异常和fast fail的故事。

七:fast fail 与ConcurrentModificationException

先看异常:ConcurrentModificationException
modCount:list每进行一次cud操作,modCount就会+1,而在进行遍历的时候则会把这个值复制给expectedModCount ,在遍历的过程中一但这两个值不相等则说明了在遍历的过程中,有另外的线程对list做了修改(添加、删除、修改)这样出现数据不一致的情况是不允许的,所以抛出ConcurrentModificationException

那如果我不使用Iterator遍历是不是就不会出现这个情况呢,是的如果我们使用for循环就不会出现这个事情,但是也有一点小技巧需要注意,如果我们在for循环中遍历的同时删除数据,arraylist的size就会发生改变

for(int i = 0 ; i<list.size();i++) // 不会出现问题
int size = list.size();
for(int i = 0 ; i<size ;i++) // 这种则会报空指针异常
// 这个是ArrayList的一个内部类,用来快速的遍历list的
private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;
}

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

现在我们基本上了解了ConcurrentModificationException,下面聊聊fast fail机制:其实ConcurrentModificationException是fast fail的一种响应,当Collection在遍历的过程中发现了修改就可能产生fast-fail,fast fail 机制并不保证在不同步代码的情况下一定会抛出异常,只是进最大努力抛出。注意这个机制对大多数的Collection都是有效的

  • 出现的场景:
    • 在单线程中:遍历的过程中进行修改(使用Iterator遍历)
    • 多线程中:一个线程修改另外一个线程的遍历的 Collection

解决的方法:

  • 对对象加锁
  • 使用java.util.concurrent包中的类来代替ArrayList和Hashmap来达到

fast safe 和fast fail则是相对应的,可能这个地方我并没有介绍的太清楚,这个点在面试的过程中可能会被问到,做一了解即可,详细的介绍可以看一下这一篇博文https://www.cnblogs.com/shamo89/p/6685216.html

相关文章

网友评论

      本文标题:Arraylist源码--之那些年的面试题

      本文链接:https://www.haomeiwen.com/subject/ibtbwftx.html