ArrayList源码解析:在内存只有10M的空间中申请一块5M的数组空间,会导致OOM吗?

面试三连

面试官:使用过集合吗?能说说都使用过哪些吗?

小明:当然使用过,使用比较多的就是ArrayList与HashMap,还有LinkedList、HashTable、ConcurrentHashMap等等。

面试官:用的不少啊,那来说说你对ArrayList的理解吧。

小明:ArrayList是一个基于数组实现的集合,主要特点在于随机访问速度较快,但是插入删除速度较慢。

面试官:那你知道为什么随机访问速度较快,插入删除速度较慢吗?

小明:不知道。

面试官: 现在内存还有10M内存,现在想申请一块5M大小的ArrayList空间,程序会抛出OOM吗?

小明:不会。

面试官:出去的时候记得把门带上,谢谢!

小明在面试在面试的时候被问到了ArrayList,但是他只回答到了一部分,比如刚刚的那个问题:为什么随机访问速度较快,插入删除速度较慢?小明就蒙蔽了,因为小明背面试题的时候只是记住结论,而并没有探索为什么,所以再面试的时候就gg了,这也给了我们一个警告,我们在看资料的时候一定不能只看结论,否则就只能和小明一样回家等通知了。

初识ArrayList

ArrayList就是动态数组,用MSDN中的说法,就是Array的复杂版本,它提供了动态的增加和减少元素,实现了ICollection和IList接口,灵活的设置数组的大小等好处。

也就是说,ArrayList其实就是一个数组,一般的数组长度是不允许发生改变的,但是ArrayList实现了数组的长度改变,所以叫动态数组,那你好奇他是怎么实现的动态数组吗?请随我一起剥开ArrayList的神秘面纱。

我相信很多人在开发中或多或少都会使用到ArrayList,比如接收数据库返回的列表,前端的批量保存等等,所以ArrayList在我们开发中还是比较重要的存在,所以今天我就来讲一讲它的源码解析。

ArrayList源码解析

ArrayList成员变量

/**
     * Default initial capacity.
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * Shared empty array instance used for empty instances.
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
     * Shared empty array instance used for default sized empty instances. We
     * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
     * first element is added.
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     * The array buffer into which the elements of the ArrayList are stored.
     * The capacity of the ArrayList is the length of this array buffer. Any
     * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
     * will be expanded to DEFAULT_CAPACITY when the first element is added.
     */
    transient Object[] elementData; // non-private to simplify nested class access

    /**
     * The size of the ArrayList (the number of elements it contains).
     *
     * @serial
     */
    private int size;

DEFAULT_CAPACITY:数组初始默认大小,大小等于10
EMPTY_ELEMENTDATA:使用有参构造时,但是数组大小为0或者数组为空的时候使用。
DEFAULTCAPACITY_EMPTY_ELEMENTDATA :使用无参构造的默认数组值,也就是elementData
elementData:动态数组
size:数组大小

实例化ArrayList

ArrayList的实例化一共有三种方式

1.无参构造

public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

注意这里的DEFAULTCAPACITY_EMPTY_ELEMENTDATA,是不是我们刚刚说的,无参构造的时候elementData=DEFAULTCAPACITY_EMPTY_ELEMENTDATA。

使用方法

ArrayList<String> list = new ArrayList<String>();
System.out.println("集合:"+list);
集合:[]

Process finished with exit code 0

2.有参构造

有参构造分两种情况,第一种是给定数组的初始化大小,第二种是拷贝其他集合

指定数组大小

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:初始化数组的大小,如果initialCapacity大于o,那么就会创建一个长度为initialCapacity的数组,等于0,就会将EMPTY_ELEMENTDATA赋值给elementData,否则怕抛出异常。

使用方法

//有参构造
        ArrayList<String> list2 = new ArrayList<String>(50);
        System.out.println("集合:" + list2);
集合:[]

Process finished with exit code 0

这里就有疑问了,上面两种创建方式返回的结果都是一样的,为什么ArrayList还要给出一个指定大小的构造呢?肯定是有原因的,这个我们在后面讲。

数组拷贝

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;
        }
    }

我们来看看,首先讲需要拷贝的集合转成数组,然后判断需要拷贝的数组大小是否等于0,等于0直接给一个空数组:EMPTY_ELEMENTDATA,需如果需要拷贝到饿数组大于0并且和当前的数组不是同一个对象,那么就执行拷贝,请注意这里的拷贝属于浅拷贝,为什么这么说呢?请看下面代码

List<User> list3 = new ArrayList<User>();
        //初始化User对象
        User user = new User();
        user.setUserName("小明");
        user.setSex(1);
        user.setAge(18);
        list3.add(user);
        System.out.println("list3:"+list3);
        //集合拷贝
        ArrayList<User> list4 = new ArrayList<User>(list3);
        System.out.println("拷贝完之后的list4:"+list4);
        //集合拷贝完成之后修改User对象的值
        user.setAge(20);
        System.out.println("修改User对象年龄之后的lsit4:"+list4);

明白我为什么这么写吗?因为我刚刚说了这里的集合拷贝指的是浅拷贝,所以我打印了还没有背拷贝的list3、拷贝完之后的list4以及修改User对象年龄之后的lsit4,你能猜到他们对应的输出结果吗?自己可以在脑海中想象一下,然后请看下面输出结果

list3:[User(userName=小明, age=18, sex=1)]
拷贝完之后的list4:[User(userName=小明, age=18, sex=1)]
修改User对象年龄之后的lsit4:[User(userName=小明, age=20, sex=1)]

Process finished with exit code 0

我们可以看到list3和拷贝完之后的list4是一模一样的,但是修改User对象年龄之后的lsit4却发生了改变,那就是年龄变成了20,我们并没有对list4的对象做修改,他为啥改变了呢?这就是java浅拷贝和深拷贝的知识了,如果对这方面不熟悉的可以参考原型模式:如何快速的克隆出一个对象?

ArrayList的构造函数基本上讲的差不多了,但是这里还是没有引出动态数组的概念啊,他还是一个死的,那是什么时候他会变成动态的呢?客官不要心急,我们接着往下看。

ArrayList方法

add()

ArrayList一共给我们提供了两个add(),我们一起来看一下吧。

第一个:add(E e)

public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
//每次添加的时候都需要判断一下数组的长度还够不够,如果不够就需要另外处理
 private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

//数组初始化的大小与需要插入位置的大小比较,返回大的那一个
public static int max(int a, int b) {
        return (a >= b) ? a : b;
    }

//判断是否需要扩容,如果插入的位置已经大于数组的大小,那么进行扩容操作
private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

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

//扩容,将数组扩大原来的1.5倍,并且将原来的数组拷贝到新数组,再将新数组复制给原数组
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);
    }

add()的源码大致就是这样的,每次添加的时候都会判断插入的位置是否大于了数组的大小,如果大于就进行扩容处理,将数组扩大原来的1.5倍( oldCapacity + (oldCapacity >> 1)),但是这里有一点需要特别注意一下,如果扩容的大小已经超过了ArrayList指定的最大数值,那会发生什么呢?

@Native public static final int   MAX_VALUE = 0x7fffffff;

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

如果扩容的大小已经超过了ArrayList指定的最大数值,他会先判断插入的位置是否已经大于了ArrayList允许的最大数值,如果大于,直接返回:MAX_VALUE,否者返回MAX_ARRAY_SIZE,这里一定要注意一个是扩容后的大小,一个是插入位置,一定不要搞错,这里就是ArrayList为什么被称为动态数组。

使用方法

//无参构造
        ArrayList<String> list = new ArrayList<String>();
        list.add("小明");
        list.add("卖托儿索的小火柴");
        System.out.println("list:" + list);
list:[小明, 卖托儿索的小火柴]

Process finished with exit code 0

我这里初始化的时候创建了一个无参构造,所以数组的初始大小为:10,添加两个元素的时候并不会触发扩容机制。

第二种:add(int index, E element)

public void add(int index, E element) {
        rangeCheckForAdd(index);

        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }

故名思意,看参数就应该能大致的猜出这个方法是干什么的,没错,他就是插入指定位置元素,他的插入和第一个差不多,唯一的区别就是第一个是往后添加,这里是按index添加到这指定下表位置,然后将其他的元素往后移,也就是System.arraycopy(elementData, index, elementData, index + 1, size - index)。

 ArrayList<String> list = new ArrayList<String>(10);
        list.add("小明");
        list.add("卖托儿索的小火柴");
        list.add("海阔天空");
        list.add(5,"逆天而行");
        System.out.println("list:" + list);

你们可以猜到执行的结果吗?执行结果就是报错,为什么呢?源码里面有这么一个方法

 private void rangeCheckForAdd(int index) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

判断插入的位置是否大于elementData的数组长度或者是否小于0,由于我这里只添加了两个元素,所以size应该是3,我们添加的下标却是5,所以就会抛出异常

Exception in thread "main" java.lang.IndexOutOfBoundsException: Index: 5, Size: 2
	at java.util.ArrayList.rangeCheckForAdd(ArrayList.java:661)
	at java.util.ArrayList.add(ArrayList.java:473)
	at com.ymy.list.MyArrayList.main(MyArrayList.java:18)

Process finished with exit code 1

我们修改一下代码,将下表修改成1

ArrayList<String> list = new ArrayList<String>(10);
        list.add("小明");
        list.add("卖托儿索的小火柴");
        list.add("海阔天空");
        list.add(1,"逆天而行");
        System.out.println("list:" + list);

这个时候我们再来看运行结果

list:[小明, 逆天而行, 卖托儿索的小火柴, 海阔天空]

Process finished with exit code 0

我们发现逆天而行被添加到了下标为1的位置,而卖托儿索的小火柴,和海阔天空相应的往后移了一位。

trimToSize()

之前没有说清楚size与elementData的关系,size表示的是elementData数组中已经存放了多少元素,而elementData.length表示ArrayList的初始数组大小,请不要搞混.

public void trimToSize() {
        modCount++;
        if (size < elementData.length) {
            elementData = (size == 0)
              ? EMPTY_ELEMENTDATA
              : Arrays.copyOf(elementData, size);
        }
    }

这个方法就是判断已经存在数组中的元素个数(size)和数组初始化的大小(elementData.length)做对比,如果小于初始化值就去掉多余的,返回一个elementData大小等于size,实现的方式就是通过拷贝的形式。

  ArrayList<String> list = new ArrayList<String>(10);
        list.add("小明");
        list.add("卖托儿索的小火柴");
        list.trimToSize();
        System.out.println("list:" + list);

为了能看到我说的,我们断点调试走一波
在这里插入图片描述
在这里插入图片描述
我们发现走到断点的那一行size=2,elementData.length= 10,下面就是判断了,很明显2<10,所以这里会执行数据拷贝,拷贝完成之后我们在看结果
在这里插入图片描述
清除了多余没用的元素下标,但是这个方法大家在使用的时候还是慎重比较好,如果你清除完成之后又想添加数据,这个时候ArrayList就会执行扩容操作了,这是需要进行数据拷贝的,慎重哦。

ensureCapacity(int minCapacity)

源码如下

public void ensureCapacity(int minCapacity) {
        int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
            // any size if not default element table
            ? 0
            // larger than default for default empty table. It's already
            // supposed to be at default size.
            : DEFAULT_CAPACITY;

        if (minCapacity > minExpand) {
            ensureExplicitCapacity(minCapacity);
        }
    }

大致意思:当你初始化了一个大小为10的初始数组之后,并添加了5条数据,这个时候你发现10可能不够,要是数组大小在大一点就好了,ensureCapacity就是解决这个问题的,他会扩大你指定的大小,但是扩大之后数组的大小是不是你指定的大小这个是不确定的,因为ensureExplicitCapacity(minCapacity);的源码在上面也看到了,他会现在原来的数组大小的基础上扩大1.5倍,然后在和你传入的数值做对比,如果大于你传入的,那么使用旧数组(elementData)大小的1.5倍作为新数组的大小,如果小于你传入的数值,这个时候就会以你传入的大小作为数组(elementData)的大小,这点一定要搞清楚哦,不然的话,你会发现你明明设置了值,但是最后数组的大小却和你设置的不一样,就会感觉是你的代码写的有问题。

我们先来看一个扩容小于原数组大小1.5倍的数值:12

ArrayList<String> list = new ArrayList<String>(10);
        list.add("小明");
        list.add("卖托儿索的小火柴");
        list.ensureCapacity(12);
        System.out.println("list:" + list);

在这里插入图片描述
我们发现elementData的大小并不是我们传入的12,而是15,要注意哦

我们再来看看扩容大于原始数组大小1.5倍的数值:20

ArrayList<String> list = new ArrayList<String>(10);
        list.add("小明");
        list.add("卖托儿索的小火柴");
        list.ensureCapacity(20);
        System.out.println("list:" + list);

在这里插入图片描述
在这里我在贴出一下导致这两种原因的代码在哪里
在这里插入图片描述

size()

返回当前ArrayList已经添加了多少条元素,这个不用多说,相信大家都知道。

isEmpty()

public boolean isEmpty() {
        return size == 0;
    }

判断ArrayList是否添加了数据,但是这点需要注意一下,这里只能判断是否存在元素,不能判断ArrayList是否为空,这点需要注意,如果使用这个方法判断空的话就报错哦。

contains(Object o)

public boolean contains(Object o) {
        return indexOf(o) >= 0;
    }

判断ArrayList所有元素中是否存在当前元素,如果是对象,判断的就是引用地址了,这里需要注意,如果我们的ArrayList的泛型是对象,那么最好重写一下equals和hashcode方法,举个例子

没有重写equals()与 hashCode()

 ArrayList<User> list = new ArrayList<User>(10);
        //用户插入集合的数据
        User user1 = new User();
        user1.setUserName("小明");
        user1.setSex(1);
        user1.setAge(18);
        //用于对比的数据
        User user2 = new User();
        user2.setUserName("小明");
        user2.setSex(1);
        user2.setAge(18);

        list.add(user1);
        System.out.println("是否包含user1:" + list.contains(user1));
        System.out.println("是否包含user2:" + list.contains(user2));
是否包含user1:true
是否包含user2:false

Process finished with exit code 0

看到输出结果了吧,判断是否包含user1结果为:true;判断是否包含user2的结果为:false,那是因为往list中添加的是user1,所以比较user1的时候他们都是同一个引用地址,所以返回true,而user2是新new出来的,他们是两个完全不相同的对象,内存地址肯定也不相同,所以这个时候肯定就返回false,因为user1和user2里面存放的数据都是一样的,有时候我们只需要判断内容是否相等,并不需要判断内存地址是否相等的时候需要怎么做呢?

重写equals()与 hashCode()
改造一下我们的User对象

package com.ymy.entity;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

import java.util.Objects;

@Getter
@Setter
@ToString
public class User {

    private String userName;

    private Integer age;

    private Integer sex;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return Objects.equals(userName, user.userName) &&
                Objects.equals(age, user.age) &&
                Objects.equals(sex, user.sex);
    }

    @Override
    public int hashCode() {

        return Objects.hash(userName, age, sex);
    }
}

测试代码还是不变,我们在运行查看结果

是否包含user1:true
是否包含user2:true

Process finished with exit code 0

总结就是一句话,ArrayList引用对象的时候如果没有重写equals()与 hashCode()对比的就是内存地址,如果重写了equals()与 hashCode(),对比的就是实实在在的数据。请拿小本本记好,这个要考。

indexOf(Object o)

查找元素所在的下标,如果查找的是对象,默认比较的是内存地址这点和contains(Object o)一样。

源码如下

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;
    }

如果查找的内容为空,那么这个就会返回第一个元素为空的下标,否者返回数组中第一次出现查找元素的下标。

没有重写equals()与 hashCode()

 ArrayList<User> list = new ArrayList<User>(10);
        //用户插入集合的数据
        User user1 = new User();
        user1.setUserName("小明");
        user1.setSex(1);
        user1.setAge(18);
        //用于对比的数据
        User user2 = new User();
        user2.setUserName("小明");
        user2.setSex(1);
        user2.setAge(18);

        list.add(user1);
        System.out.println("是否包含user1:" + list.indexOf(user1));
        System.out.println("是否包含user2:" + list.indexOf(user2));
是否包含user1:0
是否包含user2:-1

Process finished with exit code 0

很明显查找user1的时候是同一个内存地址,所以返回了对应的下标,而user2与user1不是同一个内存地址,所以返回了-1。

重写equals()与 hashCode()

重写的方法和contains()一样,我们直接看结果即可

是否包含user1:0
是否包含user2:0

Process finished with exit code 0

所以一定要区分你需要查找的是值相同还是地址相同,不然就会导致bug哦。

lastIndexOf(Object o)

与indexOf()效果一样,都是查找元素所在的下标,但是又有一点区别,那就是lastIndexOf()返回的是最后一次出现的下标位置。

public int lastIndexOf(Object o) {
        if (o == null) {
            for (int i = size-1; i >= 0; i--)
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = size-1; i >= 0; i--)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }

这个使用和indexOf一样,这里就不做demo展示了。

clone()

克隆一个ArrayList

源码如下

public Object clone() {
        try {
            ArrayList<?> v = (ArrayList<?>) super.clone();
            v.elementData = Arrays.copyOf(elementData, size);
            v.modCount = 0;
            return v;
        } catch (CloneNotSupportedException e) {
            // this shouldn't happen, since we are Cloneable
            throw new InternalError(e);
        }
    }

使用方式

 ArrayList<User> list = new ArrayList<User>(10);
        //用户插入集合的数据
        User user1 = new User();
        user1.setUserName("小明");
        user1.setSex(1);
        user1.setAge(18);
        list.add(user1);
        System.out.println("list1:"+list);
        ArrayList<User> list2 = (ArrayList<User>) list.clone();
        System.out.println("list2:"+list2);

运行结果

list1:[User(userName=小明, age=18, sex=1)]
list2:[User(userName=小明, age=18, sex=1)]

Process finished with exit code 0

将list拷贝到list2,但是这里需要注意一点,这里的拷贝属于浅拷贝,list2和list1共享一个User对象,这是需要特别注意的。

toArray()

将ArrayList转换成数组

源码

public Object[] toArray() {
        return Arrays.copyOf(elementData, size);
    }

使用方式

ArrayList<String> list = new ArrayList<String>(10);
        list.add("小明");
        list.add("逆天而行");
        System.out.println("list1:" + list);
        Object[] array = list.toArray();
        System.out.println("array:" + array);
        array[2] = "海阔天空";

运行结果

list1:[小明, 逆天而行]
array:[Ljava.lang.Object;@3a71f4dd
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 2
	at com.ymy.list.MyArrayList.main(MyArrayList.java:17)

Process finished with exit code 1

ArrayList转数组没有问题,但是在数组赋值的时候却报错了,这一点需要注意,这里的数组长度就是ArrayList的数组实际长度,ArrayList的长度是2,下标最大为1,但是我们赋值的时候给的下标是2,所以就会抛出数组越界的错误。

get(int index)

根据下表获取元素信息

源码

public E get(int index) {
        rangeCheck(index);

        return elementData(index);
    }

第一步校验下标是否越界,然后返回对应下标元素信息。

使用方法

 ArrayList<String> list = new ArrayList<String>(10);
        list.add("小明");
        list.add("逆天而行");
        System.out.println("list1:" + list);
        String name = list.get(1);
        System.out.println("name:"+name);

运行结果

Connected to the target VM, address: '127.0.0.1:62855', transport: 'socket'
list1:[小明, 逆天而行]
name:逆天而行
Disconnected from the target VM, address: '127.0.0.1:62855', transport: 'socket'

Process finished with exit code 0

这里面的下标一定不能大于elementData的size,否者就会抛出数组越界。

set(int index, E element)

在指定下标添加元素

源码

public E set(int index, E element) {
        rangeCheck(index);

        E oldValue = elementData(index);
        elementData[index] = element;
        return oldValue;
    }

添加的下标不能越界,他会将你的元素添加到数组的指定下标,并且返回被替换的元素,这里是替换哦,被替换的元素不会往后移,这点需要特别注意。

使用方法

ArrayList<String> list = new ArrayList<String>(10);
        list.add("小明");
        list.add("逆天而行");
        System.out.println("list:" + list);
        String name = list.set(1,"海阔天空");
        System.out.println("name:"+name);
        System.out.println("修改后的list:"+list);

结果

list:[小明, 逆天而行]
name:逆天而行
修改后的list:[小明, 海阔天空]

Process finished with exit code 0

remove(int index)

删除指定下标的元素,并返回被删除的元素值

源码

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;
    }

他在删除了指定下标之后,那这个下标的位置就会处于空缺,这个时候ArrayList做了一件事,那就是将数组进行重新排序,实现的方式就是数据拷贝,使用一个新的数组接受两段数据,一段是删除下标之前的数据,一段是删除下表之后的数据,整合到一个新的数组,然后赋值到原数组中。这里只需要了解一下即可,最后返回了被删除的元素值。

使用方法

ArrayList<String> list = new ArrayList<String>(10);
        list.add("小明");
        list.add("逆天而行");
        System.out.println("list:" + list);
        String name = list.remove(1);
        System.out.println("name:"+name);
        System.out.println("删除后的list:"+list);

运行结果

list:[小明, 逆天而行]
name:逆天而行
删除后的list:[小明]

Process finished with exit code 0

remove(Object o)

通过元素值删除数组中存在的元素,这种删除比较耗时间,为什么这么说呢?请看源码

 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++;
        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
    }


首先,他会判断删除的元素是否为空,如果是空,那么它将删除数组中第一个空元素,然后直接返回,如果你要删除的元素不为空,那这个时候就会循环数组,找到你要删除的第一个元素进行删除,但是删除的时候有需要做数据拷贝,如果不做的话,数组下标就会错乱,最后返回删除结果。

使用方法

 ArrayList<String> list = new ArrayList<String>(10);
        list.add("小明");
        list.add("逆天而行");
        System.out.println("list:" + list);
        boolean remove = list.remove("逆天而行");
        System.out.println("是否删除成功:"+remove);
        System.out.println("删除后的list:"+list);

运行结果

list:[小明, 逆天而行]
是否删除成功:true
删除后的list:[小明]

Process finished with exit code 0

clear()

这个方法比较简单,就是将数组中所有的元素都设置为null,然后将size设置为0。

源码

public void clear() {
        modCount++;

        // clear to let GC do its work
        for (int i = 0; i < size; i++)
            elementData[i] = null;

        size = 0;
    }

使用方法

 ArrayList<String> list = new ArrayList<String>(10);
        list.add("小明");
        list.add("逆天而行");
        System.out.println("list:" + list);
        list.clear();
        System.out.println("删除后的list:"+list);

运行结果

list:[小明, 逆天而行]
删除后的list:[]

Process finished with exit code 0

addAll(Collection<? extends E> c)

将其他的集合添加到当前集合,这里添加方式是通过拷贝实现的。

源码如下

 public boolean addAll(Collection<? extends E> c) {
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount
        System.arraycopy(a, 0, elementData, size, numNew);
        size += numNew;
        return numNew != 0;
    }

ensureCapacityInternal(size + numNew);这行代码是不是经常看到,不用我多说想必大家也知道了,没错,就是判断当前的集合是否可以装下这些数据,是否需要扩容,接下来就是数据添加了,添加的方式就是通过数据拷贝,这里的拷贝同样属于浅拷贝。

使用方法

 ArrayList<String> list = new ArrayList<String>(10);
        list.add("小明");
        list.add("逆天而行");
        System.out.println("list:" + list);
        ArrayList<String> list2 = new ArrayList<String>();
        list2.add("随风起舞");
        lis2t.add("穷凶极恶");
        list.addAll(list2);
        System.out.println("添加之后的lsit:"+list);

运行结果

list:[小明, 逆天而行]
添加之后的lsit:[小明, 逆天而行, 随风起舞, 穷凶极恶]

Process finished with exit code 0

addAll(int index, Collection<? extends E> c)

这个方法其实和上面那个也是大同小异,就是添加集合,但是这里的添加方式有点区别,这里可以指定下标。

源码如下

 public boolean addAll(int index, Collection<? extends E> c) {
        rangeCheckForAdd(index);

        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount

        int numMoved = size - index;
        if (numMoved > 0)
            System.arraycopy(elementData, index, elementData, index + numNew,
                             numMoved);

        System.arraycopy(a, 0, elementData, index, numNew);
        size += numNew;
        return numNew != 0;
    }

它会往你指定的下标添加集合元素,原本属于当前下标的元素向后移动,移动方式也是通过数据拷贝事项的。

使用方法

  ArrayList<String> list = new ArrayList<String>(10);
        list.add("小明");
        list.add("逆天而行");
        list.add("铁血无双");
        System.out.println("list:" + list);
        ArrayList<String> list2 = new ArrayList<String>();
        list2.add("随风起舞");
        list2.add("穷凶极恶");
        list.addAll(1,list2);
        System.out.println("添加之后的lsit:"+list);

运行结果

list:[小明, 逆天而行, 铁血无双]
添加之后的lsit:[小明, 随风起舞, 穷凶极恶, 逆天而行, 铁血无双]

Process finished with exit code 0

我们插入的下标位置为1,这个时候ArrayList就将list2这两个元素从下标1开始往后田间,冲突的元素就往后移,直到没有冲突为止。

这里就暂时先说这么多吧,这些都是一些比较常用的方法,看了肯定会对你有所帮助。

开篇解答

再文章开头的时候我们说到小明面试的时候被问到在一块内存只有10M的空间中申请一块5M的数组空间,会导致OOM吗?

这个答案是:不确定,为什么这么说呢?原因很简单,那是因为数组在内存中存放的地址都是连续的,比如:00xx01、00xx02、00xx03 … 00xxnn,虽然说内存还有10M,但是不能保证连续的内存空间还剩5M,如果连续空间不足5M,那么在创建ArrayList的时候就会抛出OOM,这个时候你就会疑问了,既然数组要求内存地址是连续的,那是什么导致内存地址不连续呢?这个就涉及到链表了,链表存储的数据在内存中的地址是随机的,关于链表这个就不展开了,否者又得讲半天,所以只需要记住:数组盛情内存空间的时候要求内存地址是连续的,如果连续的内存地址空间不足,那么在创建数组的时候就会抛出OOM。

总结

虽然我们在日常开发中经常使用ArrayList,但是我们对他的原理熟悉吗?如果不熟悉就因为一个细节就会让你的程序变慢或者内存溢出。

自动扩容:如果我们创建ArrayList的时候知道了大概的长度的时候尽量指明数组长度,否者在数据添加的时候就会频繁出发扩容,然而扩容就会导致数据拷贝,虽然数据拷贝属于浅拷贝,但是频繁的数据拷贝同样会消耗我们的性能,所以在实例化的时候最好给出数组初始长度,避免频繁扩容。

手动扩容(ensureCapacity):手动扩容的时候需要注意一点,手动扩容的最终数组大小有可能不是你指定的大小,他有一个校验规则,第一,将元素组长度扩大1.5倍,然后在和你传入的扩容数值做对比,谁大用谁。

删除、修改(元素删除):这两个相对来说比较耗时,为什么这么说呢?原因就是删除的时候会循环整个数组,最好情况第一次就找到了你要操作的数据,但是最坏情况是你循环了一遍数组才找到你要操作的元素,所以删除、修改的时间复杂度为:O(n),并且操作完成之后还伴随这一次数据拷贝,所以删除的时候能用下标就用下标,是在找不到下标在使用元素删除。

查询:随机访问的速度较快,那是因为根据下标能很快的找到对应的元素,时间复杂度为:O(1)。

线程安全性问题:ArrayList不是线程安全的,这点想必大家都知道,这里就不再啰嗦了。

总的来说就是尽量指定数组长度,避免频繁扩容,少使用元素删除,所以在选型的时候一定要注意使用,虽然ArrayList简单,但是使用不当,也会给项目造成很大损失。

ArrayList的源码解析并没有完全写完,还有一些,我觉得开发中可能使用的不多,所以这里就不打算继续讲了,大家看着也累,后续有时间的话再给补上,还请见谅。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章