ArrayList,经典永不过时,掌握设计亮点和面试技巧

1 核心知识点

  • 底层数据存储结构
  • 初始化容量
  • 扩容机制
  • 线程安全
  • 时间复杂度

2 关键代码分析

从add方法开始分析

public boolean add(E e) {
// 步骤1
ensureCapacityInternal(size + 1);  // Increments modCount!!
// 步骤2
elementData[size++] = e;
return true;
}

步骤1:确保内部容量充足,走进ensureCapacityInternal方法查看,这里注意要确保的容量大小是size+1,也就是保证容量能够再添加一个元素

private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

发现这个方法里面也分成两部分,第一部分是calculateCapacity()计算本次添加需要的容量大小,继续跟进去看看

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

这里逻辑比较简单,首先判断elementData这个变量是否等于DEFAULTCAPACITY_EMPTY_ELEMENTDATA,这里DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一个空的Object[],如果elementData是空Object[],那么指定的容量minCapacity和默认的容量DEFAULT_CAPACITY比较大小,这里的DEFAULT_CAPACITY=10,意思就是初始化容量是10,随着元素的添加,超过10就返回传递的容量值size+1。

当确定了需要的容量大小后,接着我们看第二部分ensureExplicitCapacity(),是怎么确Object[]保容量够用的

 private void ensureExplicitCapacity(int minCapacity) {
          // 改变数量
        modCount++;

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

首先会给modCount的值+1,这个值的作用是保证在遍历ArrayList的时候,集合里面的元素数量没有发生变化,这部分分析放到文章底部进行详细的说明,然后判断需要的容量大小是否超过现在的容量大小的,如果超过了,那么就需走进grow()方法扩容

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)新容量是旧容量的1.5倍,newCapacity=oldCapacity+0.5*oldCapacity

(2)判断新的容量大小是否已经满足需要的容量大小,如果不满足,那么新容量的大小就是需要的容量大小

(3)再判断新容量的大小是否超过了最大容量大小(0x7fffffff,2的31次方减1),如果超过则抛异常

(4)创建一个新的容量大小的Object数组,并将原来的元素拷贝到新数组当中,这个操作非常重,所以在代码实现的时候尽量避免频繁扩容

到这里已经完成了容量的保证,接下来要做的就是将新的元素保存到Object[]当中

会到步骤2,elementData[size++] = e,制定数组的下一个索引位置元素为新增的元素,最后返回true,标识添加成功

3 modCount作用说明

因为ArrayList是一个集合,最常见的操作就是遍历,那么一定会使用到ArrayList的遍历器,看看遍历方法iterator的实现

public Iterator iterator() {
    return new Itr();
}

继续跟进Itr(),这是一个内部类

private class Itr implements Iterator {
    int cursor;       // index of next element to return
    int lastRet = -1; // index of last element returned; -1 if no such
    int expectedModCount = modCount;

    Itr() {}

    public boolean hasNext() {
        return cursor != size;
    }

    @SuppressWarnings("unchecked")
    public E next() {
        checkForComodification();
        int i = cursor;
        if (i >= size)
            throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length)
            throw new ConcurrentModificationException();
        cursor = i + 1;
        return (E) elementData[lastRet = i];
    }

    public void remove() {
        if (lastRet < 0)
            throw new IllegalStateException();
        checkForComodification();

        try {
            ArrayList.this.remove(lastRet);
            cursor = lastRet;
            lastRet = -1;
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }

    @Override
    @SuppressWarnings("unchecked")
    public void forEachRemaining(Consumer 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();
    }
}

因为遍历的时候我们通常要判断是否有下一个元素,这里是通过next()方法进行判断

@SuppressWarnings("unchecked")
    public E next() {
        checkForComodification();
        int i = cursor;
        if (i >= size)
            throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length)
            throw new ConcurrentModificationException();
        cursor = i + 1;
        return (E) elementData[lastRet = i];
    }

注意checkForComodification()方法,看看具体实现

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

判断modCount是否等于expectedModCount,如果不等于,就抛异常,这里的modCount是ArrayList的全局变量,add()添加元素的时候,这个值会自增1,expectedModCount则是在创建迭代器的时候,将modCount 赋值给了expectedModCount,所以一开始expectedModCount=modCount,如果在遍历的过程当中,集合发生了元素数量变化,那么modCount就会变化,那么next()的时候就会抛异常,主要就是用来保障集合在遍历过程中,元素数量不能发生变化。

问题来了,如果想在遍历的时候,删除元素怎么处理,可以通过迭代器的remove方法删除,一起来看看remove方法为什么不会抛异常

public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

荣国上面代码可以看到,在进行remove操作的时候,会重新将expectedModCount赋值成modCount,也就是说,当modCount发生变化,expectedModCount也会相应的发生变化,是满足expectedModCount = =modCount这个等式的,所以不会报错

4 线程安全问题

先说结论,非线程安全,代码演示,多线程将100个1放入ArrayList中存放

@Test
    public void testThreadSafe() {
        List list = new ArrayList();
        for (int i = 0; i < 500; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        // 这里睡眠50ms,是为了等待其它线程并行跑起来,如果不睡眠,add操作太快,很难出现
                        // 线程安全问题
                        TimeUnit.MILLISECONDS.sleep(200);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    list.add(1);
                }
            }).start();
        }
        try {
            // 这里的150000>200*500,保证所有线程都已经run起来
            Thread.sleep(150000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(list.size());
    }
输出结果是495

预期打印结果是500,实际输出是495,说明多线程的时候。有元素被下一个元素给覆盖掉,导致元素添加丢失

可以使用Collections.synchronizedList(list)方法

@Test
    public void testArrayListSafe() {
        List list = new ArrayList();
        List integers = Collections.synchronizedList(list);
        for (int i = 0; i < 500; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        // 这里睡眠50ms,是为了等待其它线程并行跑起来,如果不睡眠,add操作太快,很难出现
                        // 线程安全问题
                        TimeUnit.MILLISECONDS.sleep(200);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    integers.add(1);
                }
            }).start();
        }
        try {
            // 这里的150000>200*500,保证所有线程都已经run起来
            Thread.sleep(150000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(integers.size());
    }
//输出结果是500

最终输出结果是500,符合预期,线程安全,看下线程安全实现原理

public static  List synchronizedList(List list) {
        return (list instanceof RandomAccess ?
                new SynchronizedRandomAccessList<>(list) :
                new SynchronizedList<>(list));
    }

synchronizedList方法,返回了一个新的SynchronizedList对象,先看下这个构造器是否有做什么操作

SynchronizedList(List list) {
    // 查看父构造器
            super(list);
            this.list = list;
        }
// 父构造器给mutex这个变量赋值,是当前对象
SynchronizedCollection(Collection c) {
            this.c = Objects.requireNonNull(c);
            mutex = this;
        }

然后看看这个对象的add方法是怎么实现的

        public void add(int index, E element) {
            synchronized (mutex) {list.add(index, element);}
        }

通过这里我们不难发现,线程安全的实现原理是通拖synchronized实现,使用的锁是当前对象

5 时间复杂度分析

这里采用大O表示法

  • 添加元素

ArrayLis的底层数据结构是Object[],添加元素的时候,因为ArrayList通过size记录了索引的位置,只需要Object[size+1]=newElement,所以时间复杂度是O(1)

  • 查找元素

查找指定索引index的元素,element = Object[index],时间复杂度也是O(1)

插座指定元素data,需要遍历整个数组,然后找到元素为data的位置,时间复杂度是O(n)

  • 删除元素

删除指定索引index的元素,需要将index之后的元素往前移动一个索引位置,这样index位置的元素就被删掉,时间复杂度O(n)

删除指定元素data,需要遍历整个数组,然后找到元素为data的位置index,需要将index之后的元素往前移动一个索引位置,这样index位置的元素就被删掉,时间复杂度O(n),时间复杂度是O(n)

6 面试技巧

问题:说说ArrayList?

  • 将底层数据结构以及扩展原理说清楚,同时也可以强调一下通过位移计算扩容长度的性能优势
  • 突出modCount的作用和不同操作下的时间复杂度,到这里,基本已经回答了面试官对ArrayList的问题,同时还带有一点点扩展,这里停下来,让面试官思考和进一步提问,有经验的面试官接下来就会问到线程安全,正好进入我们准备的知识点
  • 主动说明ArrayList非线程安全,以及怎么实现线程安全
本文由博客一文多发平台 OpenWrite 发布!

你可能感兴趣的:(java)