为什么使用不同的 ArrayList 构造函数会导致内部数组的增长率不同?

2022-09-04 01:56:32

我似乎在实现中偶然发现了一些有趣的事情,我无法理解。下面是一些代码,显示了我的意思:ArrayList

public class Sandbox {

    private static final VarHandle VAR_HANDLE_ARRAY_LIST;

    static {
        try {
            Lookup lookupArrayList = MethodHandles.privateLookupIn(ArrayList.class, MethodHandles.lookup());
            VAR_HANDLE_ARRAY_LIST = lookupArrayList.findVarHandle(ArrayList.class, "elementData", Object[].class);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException();
        }
    }

    public static void main(String[] args) {

        List<String> defaultConstructorList = new ArrayList<>();
        defaultConstructorList.add("one");

        Object[] elementData = (Object[]) VAR_HANDLE_ARRAY_LIST.get(defaultConstructorList);
        System.out.println(elementData.length);

        List<String> zeroConstructorList = new ArrayList<>(0);
        zeroConstructorList.add("one");

        elementData = (Object[]) VAR_HANDLE_ARRAY_LIST.get(zeroConstructorList);
        System.out.println(elementData.length);

    }
}

这个想法是,如果你创建一个这样的:ArrayList

List<String> defaultConstructorList = new ArrayList<>();
defaultConstructorList.add("one");

看看里面的东西(保存所有元素的地方)它将报告。因此,您添加一个元素 - 您将获得9个未使用的额外插槽。elementDataObject[]10

另一方面,如果您这样做:

List<String> zeroConstructorList = new ArrayList<>(0);
zeroConstructorList.add("one");

你添加一个元素,保留的空间只为该元素,仅此而已。

在内部,这是通过两个字段实现的:

/**
 * 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 = {};

当您创建 via 时,将使用 - 。ArrayListnew ArrayList(0)EMPTY_ELEMENTDATA

创建 via 时, 将使用 - 。ArrayListnew Arraylist()DEFAULTCAPACITY_EMPTY_ELEMENTDATA

来自我内心的直观部分 - 简单地尖叫“删除”,让所有案件都得到处理;当然是代码注释:DEFAULTCAPACITY_EMPTY_ELEMENTDATAEMPTY_ELEMENTDATA

我们将其与EMPTY_ELEMENTDATA区分开来,以了解添加第一个元素时要膨胀多少

确实有意义,但是为什么一个会膨胀到(比我要求的多得多),而另一个会膨胀(完全符合我的要求)。101


即使您使用 ,并继续添加元素,最终您也会达到比请求的点更大的点:List<String> zeroConstructorList = new ArrayList<>(0)elementData

    List<String> zeroConstructorList = new ArrayList<>(0);
    zeroConstructorList.add("one");
    zeroConstructorList.add("two");
    zeroConstructorList.add("three");
    zeroConstructorList.add("four");
    zeroConstructorList.add("five"); // elementData will report 6, though there are 5 elements only

但它的增长速度小于默认构造函数的情况。


这让我想起了实现,其中桶的数量几乎总是比您要求的要多;但是这样做是因为需要“两个”桶的“功率,但这里的情况并非如此。HashMap

所以问题是 - 有人可以向我解释这种差异吗?


答案 1

您可以准确地获得您要求的内容,以及指定的内容,即使在实现不同的旧版本中也是如此:

ArrayList()

构造初始容量为 10 的空列表。

ArrayList(int)

构造具有指定初始容量的空列表。

因此,使用默认构造函数构造函数将为您提供初始容量为 10 的构造函数,因此只要列表大小为 10 或更小,就不需要调整大小操作。ArrayListArrayList

相反,具有参数的构造函数将精确地使用指定的容量,具体取决于指定int

除了添加元素具有恒定的摊销时间成本这一事实之外,没有指定增长策略的详细信息。

即使您指定初始容量为零,这也适用。

Java 8 添加了优化,即十元素数组的创建被推迟到添加第一个元素之后。这专门解决了实例(使用默认容量创建)长时间甚至整个生存期保持空的常见情况。此外,当第一个实际操作是 时,它可能会跳过第一个数组大小调整操作。这不会影响具有显式初始容量的列表,因为这些列表通常是仔细选择的。ArrayListaddAll

以下答案所述:

根据我们的性能分析团队,大约 85% 的 ArrayList 实例是以默认大小创建的,因此此优化对绝大多数情况都有效。

动机是精确优化这些方案,而不是触及指定的默认容量,该容量是在创建时定义的。(虽然JDK 1.4是第一个明确指定它的人)ArrayList


答案 2

如果使用默认构造函数,则想法是尝试平衡内存使用和重新分配。因此,使用较小的默认大小(10),对于大多数应用程序来说应该没问题。

如果使用具有显式大小的构造函数,则假定您知道自己在做什么。如果你用0初始化它,你基本上是在说:我很确定这要么保持空,要么不会超出很少的元素。

现在,如果您查看 openjdk (link) 中的实现,您可以看到,只有在您第一次添加项目时,这种差异才会发挥作用:ensureCapacityInternal

private void ensureCapacityInternal(int minCapacity) {
    if (elementData == EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }

    ensureExplicitCapacity(minCapacity);
}

如果使用默认构造函数,则大小将增大为 (10)。这是为了防止在添加多个元素时进行过多的重新分配。但是,如果您显式创建了此大小为 0 的 ArrayList,则在添加的第一个元素上,它只会增长到大小 1。这是因为你告诉它你知道你在做什么。DEFAULT_CAPACITY

ensureExplicitCapacity基本上只是调用(带有一些范围/溢出检查),所以让我们看看: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,它也会增长得越大。这背后的原因很简单:如果列表已经很大,则添加大量项目的可能性更高,反之亦然。这也是为什么您会看到在第 5 个元素之后增长 1,然后增长 2 的原因。minCapacity