从ArrayList说起

来源:互联网 发布:linux dd克隆系统 编辑:程序博客网 时间:2024/04/29 10:39

从何说起呢?ArrayList简单易用及强大的功能,注定了我们使用java语言编程时,用得最多的容器类非它莫属。正是因为它的简单,很多并没有认真了解过它,对它的理解并不深刻。也正是因为它是最常用,最基础的容器类,所以就从ArrayList说起吧!本文不会逐一去解释ArrayList的所有方法,仅挑选当中几个我们平时容易忽略的方面进行说明。

ArrayList内存机制

ArrayList,顾名思义,这个list是通过Array进行存储。那么它占用的内存空间是怎么进行分配,扩展以及收缩的呢?

空间分配

首先,每当JVM执行以下这条语句时:

ArrayList list = new ArrayList();

分配给这个list的初始空间大小(DEFAULT_CAPACITY)为10。

空间扩展

每当往list中添加对象时,即调用addaddAll时,为了保证空间足够使用而不至于抛出异常,都需要对空间大小进行判断,如果现有空间足够大,那么直接插入,否则需要先扩展内存。ArrayList中判断内存是否足够以及进行内存扩展的方法有:

//保证空间至少为minCapacitypublic void ensureCapacity(int minCapacity) {    int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)        ? 0        : DEFAULT_CAPACITY;    if (minCapacity > minExpand) {        ensureExplicitCapacity(minCapacity);    }}private void ensureExplicitCapacity(int minCapacity) {    modCount++;    //当所需最小空间大于现有空间大小时,需要进行内存扩展    if (minCapacity - elementData.length > 0)        grow(minCapacity);}private void grow(int minCapacity) {    int oldCapacity = elementData.length;    //计算新的内存大小    int newCapacity = oldCapacity + (oldCapacity >> 1);    //如果计算所得内存大小仍小于所需最小值,则将新的内存大小设为所需最小值,    //一般只有addAll会用到    if (newCapacity - minCapacity < 0)        newCapacity = minCapacity;    //如果新的大小超过最大可分配空间,则将其设置为最大可分配空间    if (newCapacity - MAX_ARRAY_SIZE > 0)        newCapacity = hugeCapacity(minCapacity);    //内存中开辟一片新的大小为newCapacity的空间,并将现有内容拷贝过去    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;}

从上面这段代码,我们可以得出结论:

  1. newCapacity=min(max(3/2*oldCapacity, minCapacity), MAX_ARRAY_SIZE);
  2. 为了保证数组内存空间的连续性,每次进行空间扩展都是将原有数据拷贝到新的内存地址上,久的内存由gc(垃圾回收器)回收;
  3. ArrayList的最大存储量为Integer.MAX_VALUE,即 2147483647。

如果逐个往list中添加对象,那么list的空间大小呈阶梯状增长,如下图所示:
arraylist size&capacity

空间收缩

值得注意的是,当我们调用ArrayList的remove,removeAll以及retainAll方法时,这些方法都是在原本的内存空间上进行操作(下文会详述)。因此,如若我们删除一些元素后,空间并没有得到释放。如果我们需要对空间进行回收,那么ArrayList提供了以下方法供我们使用。

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

此方法可以将list占用内存大小压缩到list中元素个数。同样也是通过Arrays.copyof方法将元素拷贝到一片新的内寸空间,老的空间由gc回收。

removeAll & retainAll

此处专门设章节写这两个方法主要原因是它们的实现较为巧妙,值得一说。
如果是我们自己实现removeAll,最简单的方法就是循环调用remove方法,将需要删除的元素逐个删除;而retailAll则逐个查找需要保留的元素,并拷贝到一片新的空间上。这也许是最朴素,最简单粗暴的方法了。
JDK如何巧妙实现呢,先看看源码:

public boolean removeAll(Collection<?> c) {    Objects.requireNonNull(c);    return batchRemove(c, false);}public boolean retainAll(Collection<?> c) {    Objects.requireNonNull(c);    return batchRemove(c, true);}private boolean batchRemove(Collection<?> c, boolean complement) {    final Object[] elementData = this.elementData;    int r = 0, w = 0;    boolean modified = false;    try {        for (; r < size; r++)            if (c.contains(elementData[r]) == complement)                elementData[w++] = elementData[r];    } finally {        if (r != size) {            System.arraycopy(elementData, r,                             elementData, w,                             size - r);            w += size - r;        }        if (w != size) {            for (int i = w; i < size; i++)                elementData[i] = null;            modCount += size - w;            size = w;            modified = true;        }    }    return modified;}

从源码可以看出,这无论是removeAll还是retainAll,它们都通过调用batchRemove这一个方法实现。它们的实现方式类似于jvm中垃圾回收机制中的整理-清除方法,即先将需要保留的元素紧凑地移到一块儿,剩下的即为可以删除的元素。整个删除和保留过程如下图所示:
元素整理-清除过程
这种方法非常快速并节约地删除或保留元素,较为巧妙。

安全的toArray

toArray也没有什么神奇之处,无非就是返回ArrayList中自己保存的数组对象,这里单独描述,只是为了说明这个方法是安全的,我们无需担心对得到的数组进行操作会对原本的arraylist对象产生影响,它用于桥接基于数组和基于容器的两种API模式。源码如下:

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

源码非常简洁,可以看出返回的数组对象是通过Arrays.copyOf拷贝出来的一份对象,和原本的对象不属于同一内存空间。因此,得到的数组对象是独立的,可以放心使用。

Java8 中新增方法

java 8 中,ArrayList新增方法如下表所示:

方法 参数 描述 forEach Consumer < ? super E> action 遍历每个元素做指定操作 spliterator 返回一个Spliterator removeIf Predicate< ? super E> filter 判断条件是否满足,满足则删除 replaceAll UnaryOperator< E > operator 根据operator进行替换 sort Comparator< ? super E> c 对list中元素根据Comparator指定规则进行排序

对于新增方法,我们将从如何使用它们的角度出发,进行描述。
首先我们定义一个员工类(Employee):

public class Employee {    private String name;    private String Dept;    private Integer age;    private Double salary;    //getter and setter    ......}
  • forEach
    在java8之前,如果我们要对一个遍历员工数组,输出每个员工的姓名,那么我们需要这么做:
    for (Employee emp:employees) {        System.out.println(emp.getName());    }

如今,我们可以这么做:

    employees.forEach(e -> System.out.println(e.getName()));

也可以这么做:

employees.forEach(new Consumer<Employee>() {    @Override    public void accept(Employee t) {        System.out.println(t.getName());    }});
  • spliterator
    这个用于返回一个ArrayListSpliterator,此处不做具体介绍;如需了解可以参考:这儿
  • removeIf
    加入现在公司财务紧张,需要减员,假设需要裁去IT部门中工资超过30000的员工,可以这么办:
    employees.removeIf(e -> e.getDept().equals("IT") && e.getSalary() > 30000d);
  • replaceAll
    现在公司重新整理架构,需要讲IT部门合并进RD部门,现在可以直接操作:
employees.replaceAll(new UnaryOperator<Employee>() {                @Override    public Employee apply(Employee t) {        if (t.getDept().equals("IT")) {                             t.setDept("RD");        }        return t;    }});
  • sort
    公司HR发工资时,需要根据员工工资倒序排序,过去常用的方法是使用Collections的静态方法sort:
Collections.sort(employees, new Comparator<Employee>() {    @Override    public int compare(Employee o1, Employee o2) {        return (int) (o2.getSalary() - o1.getSalary());    }});

现在可以直接进行排序:

employees.sort((a, b) -> a.getSalary().compareTo(b.getSalary()));

总结

本文主要分析了ArrayList几个常常容易忽略方法的源码,总结了java8中ArrayList新增的方法。仅当学习,记录。

0 0