从ArrayList说起

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

ArrayList内存机制

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

空间分配

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

ArrayList list = new ArrayList();

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

空间扩展

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

//保证空间至少为minCapacity
public 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新增方法如下表所示:

方法参数描述
forEachConsumer < ? super E> action遍历每个元素做指定操作
spliterator返回一个Spliterator
removeIfPredicate< ? super E> filter判断条件是否满足,满足则删除
replaceAllUnaryOperator< E > operator根据operator进行替换
sortComparator< ? 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新增的方法。仅当学习,记录。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值