说一说ArrayLisst,并基于ArrayList说一下其创建和扩容问题

说明

   在一次的笔试过程中,我遇到一个题目,ArrayList list = new ArrayList(20);这行代码ArrayList底层会扩容几次,这个时候我就懵了,因为我对ArrayList的底层不太理解。虽然我们不理解,但是没有关系,我们把它给学起来就可以了。
 
 

浅谈Array和List

   说ArrayList之前,我们先把ArrayList拆开来,就说一下Array和List。
 

Array
   我们可以先来说一说数组,Array数组,是一种基于索引的数据结构,使用索引来搜索和获取数据是很快的,Array检索和获取数据的时间复杂度是O(1),但是要删除数据的开销却很大,因为删除数据之后需要对数组的数据进行重新排列,删数据之后,后面的元素需要移动到前面。

而我们的数组有一个缺点,就说初始化时,必须指定长度,否则就会报错。
 

List
   然后是我们的List,List是Collection的子接口,是一个有序、可重复的集合,List在Java中有两种实现方式,一种是LinkedList,而另一种就是我们的ArrayList,ArrayList是基于索引结构的实现。通常如果我们使用的话,如果我们在不明确插入多少条数据的情况下,那么使用数组就很尴尬了,因为你不知道初始化数组大小为多少。ArrayList可以使用默认大小,当元素达到一定个数后,会实现自动扩容,我们可以这样理解,数组是定死的,而ArrayList是动态的数组。

 
 

浅谈ArrayList

   我们在学习Java的时候,我们都需要深入底层去学习,这有利于我们的提升,我们来看一下ArrayList的底层原理。

在ArrayList底层源码中有几个我们需要注意的属性:

  1. Default_capacity (int)
  2. Empty_elementdata (Obejct[ ]{ })
  3. DefaultCapacity_empty_elementdata(Object[ ]{ })
  4. elementData(Object[ ])
  5. size (int)
     

在这里插入图片描述

 

我们先来看一下几个构造器



    /**
     * Constructs an empty list with an initial capacity of ten.
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

从源码中我们可以看出,当我们直接创建ArrayList,elementData被赋予了默认空容量的Object[ ]数组,所以此时ArrayList数组是空的、固定长度的,也就是说其容量此时是0,元素个数size为默认值0。

 




    /**
     * Constructs an empty list with the specified initial capacity.
     *
     * @param  initialCapacity  the initial capacity of the list
     * @throws IllegalArgumentException if the specified initial capacity
     *         is negative
     */
    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);
        }
    }

从该构造器可以看出:

  • 如果initialCapacity > 0 ,那么在堆上new 一个Object类型的数组,大小为initialCapacity,并将其引用到elementData上,此时ArrayList的容量为initialCapacity,元素个数大小size为0;
  • 如果initialCapacity = 0 ,那么将属性中定义的空的数组赋值给elementData,此时ArrayList的容量为0,元素个数大小size为0;
  • 如果initialCapacity > 0,那么直接抛出异常;

从这里我们可以看出:如果我们在创建ArraylList实例过程中我们使用初始化容量的构造器,那么,它底层只是直接在堆上new 一个Object类型的数组,大小为initialCapacity,并没有进行扩容。
 

笔试题:ArrayList list = new ArrayList(20);
底层扩容了几次?
答案:0次。
下面的构造器感兴趣的自己去看看源码,这里我们来说一个另一个重要的方法:add(Element e);
 


    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 {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

 
 

ArrayList的扩容机制

我们来说一下ArrayList的一个方法:add()
add():从底层源码可以看出,add()首次扩容会从0扩容成10,再次扩容到上次扩容的1.5倍,比如0,10,15,22,33…

add(),添加元素到集合的尾部,如果要增加的数据量很大的话,我们需要调用一下ensureCapicity(int minSize)。

ensureCapicity(int minSize)

ensureCapacity(int n)方法可以对ArrayList底层的数组进行扩容。

  • 若参数值大于底层数组长度的1.5倍,则数组的长度就扩容为这个参数值大小。
  • 若小于底层数组长度的1.5倍,则数组长度就扩容为底层数组长度的1.5倍。

使用它是因为它可以一次性将我们的ArraylIst扩容到指定的容量,而如果我们需要加入大量元素而不进行指定,那么可能ArrayList会进行多次扩容,这样子会消耗很多性能的。而使用ensureCapicity()方法的作用是预先设定ArrayList的大小,这样我们可以大大提高初始化速度和性能了。我们来看一个例子。

public class EnsureCapacityTest {
    public static void main(String[] args) {
        ArrayList<Object> list = new ArrayList<Object>();
        final int N =10000000;
        long startTime = System.currentTimeMillis();
        for(int i = 0;i<N;i++) {
            list.add(i);
        }
        long endTime = System.currentTimeMillis();
        System.out.println(endTime-startTime);

        list = new ArrayList<Object>();
        long startTime1 = System.currentTimeMillis();
        list.ensureCapacity(N);
        for(int i = 0;i<N;i++) {
            list.add(i);
        }
        long endTime1 = System.currentTimeMillis();
        System.out.println(endTime1-startTime1);
    }
}

结果:

4747
233

 
 

ArrayList的线程安全问题

   我们来说一个著名的异常:ConcurrentModificationException并发修改异常。我们来写一个ArrayList并发修改异常的例子,并引出著名的fail-fast机制。

            List<Integer> list = new ArrayList<>();
            list.add(1);
            list.add(2);
            list.add(3);
            Iterator<Integer> iterator = list.iterator();
            while (iterator.hasNext()) {
                  Integer next = iterator.next();
                  if (next.equals(1)) {
                        list.remove(1);
                  }
            }

异常信息如下:
 
在这里插入图片描述
 

关于Fail-Fast机制和ConcurrentModificationException具体见我另一篇文章:Fail-Fast机制和ConcurrentModificationException并发修改异常

在线程不安全的情况下,如果我们在遍历集合的同时,需要修改集合中的元素,那么在绝大多数情况下就很可能会出现ConcurrentModificationException并发修改异常 。那么我们需要如何解决快速失败机制中的并发修改异常呢?
这个其实很简单的,我们只需要将java.util包下的类改成java.util.concurrent包下的类就可以了,具体的话:
ArrayList ---- CopyOnWriteArrayList
HashMap — ConcurrentHashMap

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值