在 Java 8 中,为啥 ArrayList 的默认容量现在为零?

Posted

技术标签:

【中文标题】在 Java 8 中,为啥 ArrayList 的默认容量现在为零?【英文标题】:In Java 8, why is the default capacity of ArrayList now zero?在 Java 8 中,为什么 ArrayList 的默认容量现在为零? 【发布时间】:2016-03-18 22:34:24 【问题描述】:

我记得在 Java 8 之前,ArrayList 的默认容量是 10。

令人惊讶的是,默认(void)构造函数的注释仍然显示:Constructs an empty list with an initial capacity of ten.

来自ArrayList.java

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

...

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

【问题讨论】:

【参考方案1】:

从技术上讲,它是10,而不是零,如果您承认支持数组的延迟初始化。见:

public boolean add(E e) 
    ensureCapacityInternal(size + 1);
    elementData[size++] = e;
    return true;


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

    ensureExplicitCapacity(minCapacity);

在哪里

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

您所指的只是在所有最初为空的ArrayList 对象之间共享的零大小的初始数组对象。 IE。 10 的容量得到保证惰性,Java 7 中也存在这种优化。

诚然,构造函数合同并不完全准确。也许这就是这里混乱的根源。

背景

这是 Mike Duigou 的电子邮件

我已经发布了空 ArrayList 和 HashMap 补丁的更新版本。

http://cr.openjdk.java.net/~mduigou/JDK-7143928/1/webrev/

这个修改后的实现为任一类引入了没有新字段。对于 ArrayList,仅当以默认大小创建列表时,才会发生后备数组的延迟分配。根据我们的性能分析团队的说法,大约 85% 的 ArrayList 实例是以默认大小创建的,因此这种优化对于绝大多数情况都是有效的。

对于 HashMap,创造性地使用阈值字段来跟踪请求的初始大小,直到需要存储桶数组。在读取端,使用 isEmpty() 测试空地图案例。在写入大小上,使用 (table == EMPTY_TABLE) 的比较来检测是否需要对存储桶数组进行膨胀。在 readObject 中,尝试选择有效的初始容量需要做更多的工作。

发件人:http://mail.openjdk.java.net/pipermail/core-libs-dev/2013-April/015585.html

【讨论】:

根据bugs.java.com/bugdatabase/view_bug.do?bug_id=7143928,它可以减少堆使用并缩短响应时间(显示了两个应用程序的数字) @khelwood:ArrayList 并没有真正“报告”它的容量,除了通过这个 Javadoc:没有getCapacity() 方法,或类似的东西。 (也就是说,ensureCapacity(7) 之类的东西对于默认初始化的 ArrayList 来说是无操作的,所以我想我们真的应该表现得好像它的初始容量真的是 10 ......) 不错的挖掘。默认的初始容量确实不是零,而是 10,默认情况下被延迟分配作为特例。如果您重复将元素添加到使用无参数构造函数创建的 ArrayList 与将零传递给 int 构造函数,并且如果您反射性地或在调试器中查看内部数组大小,您可以观察到这一点。在默认情况下,数组从长度 0 跳到 10,然后按照 1.5 倍的增长率跳到 15、22。将零作为初始容量会导致从 0 增长到 1、2、3、4、6、9、13、19.... 我是 Mike Duigou,更改和引用的电子邮件的作者,我同意此消息。 ? 正如 Stuart 所说,动机主要是为了节省空间而不是性能,尽管由于经常避免创建后备数组,也有轻微的性能优势。 @assylias: ;^) 不,它仍然有其作为单例 emptyList() 的位置,仍然比几个空的 ArrayList 实例消耗更少的内存。它现在不太重要,因此在每个地方都不需要,尤其是在以后添加元素的可能性更高的地方。另外请记住,您有时想要一个不可变的空列表,然后emptyList() 是要走的路。【参考方案2】:

在 java 8 中,ArrayList 的默认容量为 0,直到我们将至少一个对象添加到 ArrayList 对象中(您可以称之为延迟初始化)。

现在的问题是为什么在 JAVA 8 中进行了这种更改?

答案是节省内存消耗。数以百万计的数组列表对象是在实时 Java 应用程序中创建的。 10 个对象的默认大小意味着我们在创建时为底层数组分配 10 个指针(40 或 80 字节)并用空值填充它们。 一个空数组(用空值填充)占用大量内存。

延迟初始化将内存消耗推迟到您实际使用数组列表的那一刻。

请参阅下面的代码以获取帮助。

ArrayList al = new ArrayList();          //Size:  0, Capacity:  0
ArrayList al = new ArrayList(5);         //Size:  0, Capacity:  5
ArrayList al = new ArrayList(new ArrayList(5)); //Size:  0, Capacity:  0
al.add( "shailesh" );                    //Size:  1, Capacity: 10

public static void main( String[] args )
        throws Exception
    
        ArrayList al = new ArrayList();
        getCapacity( al );
        al.add( "shailesh" );
        getCapacity( al );
    

    static void getCapacity( ArrayList<?> l )
        throws Exception
    
        Field dataField = ArrayList.class.getDeclaredField( "elementData" );
        dataField.setAccessible( true );
        System.out.format( "Size: %2d, Capacity: %2d%n", l.size(), ( (Object[]) dataField.get( l ) ).length );


Response: - 
Size:  0, Capacity:  0
Size:  1, Capacity: 10

文章Default capacity of ArrayList in Java 8详细解释。

【讨论】:

【参考方案3】:

如果对 ArrayList 进行的第一个操作是传递 addAll 一个包含十多个元素的集合,那么任何创建初始十元素数组来保存 ArrayList 内容的努力都将被丢弃窗户。每当向 ArrayList 添加某些内容时,都需要测试结果列表的大小是否会超过后备存储的大小;允许初始后备存储的大小为零而不是十将导致此测试在列表的生命周期内额外失败一次,该列表的第一个操作是“添加”,这将需要创建初始的十项数组,但成本是低于创建一个永远不会被使用的十项数组的成本。

话虽如此,如果“addAll”重载指定了在当前项之后可能添加多少项(如果有的话),则在某些情况下可能会进一步提高性能,并且可以使用它来影响其分配行为。在某些情况下,将最后几项添加到列表中的代码将有一个很好的想法,即列表永远不需要除此之外的任何空间。在许多情况下,列表将被填充一次,之后就不再修改。如果此时代码知道列表的最终大小将是 170 个元素,它有 150 个元素和大小为 160 的后备存储,将后备存储增加到 320 大小将无济于事,并将其保留为 320 大小或将其修剪为170 的效率将低于简单地将下一个分配增加到 170。

【讨论】:

关于addAll()的非常好的观点。这是提高第一个 malloc 效率的又一个机会。 @kevinarpe:我希望 Java 库能够以更多方式设计,让程序能够指示事物可能会被如何使用。例如,旧样式的子字符串在某些用例中很糟糕,但在其他用例中却非常出色。如果有单独的函数用于“可能比原始子串更持久”和“子串不太可能比原始子串更持久”,并且代码在 90% 的情况下使用了正确的函数,我认为这些函数的性能可能会大大优于旧的或新的字符串实现。【参考方案4】:

问题是“为什么?”。

内存分析检查(例如 (https://www.yourkit.com/docs/java/help/inspections_mem.jsp#sparse_arrays) 显示空(填充有 null)数组占用大量内存。

10 个对象的默认大小意味着我们在创建时为底层数组分配 10 个指针(40 或 80 字节)并用空值填充它们。真正的 java 应用程序会创建数百万个数组列表。

引入的修改删除了^W将内存消耗推迟到您实际使用数组列表的那一刻。

【讨论】:

请用“浪费”更正“消耗”。您提供的链接并不意味着它们开始到处吞噬内存,只是具有空元素的数组不成比例地浪费了为它们分配的内存。 “消耗”意味着他们神奇地使用超出分配范围的内存,但事实并非如此。【参考方案5】:

经过上述问题,我浏览了 Java 8 的 ArrayList 文档。我发现默认大小仍然只有 10。

【讨论】:

【参考方案6】:

ArrayList 在 JAVA 8 中的默认大小仍然是 10。在 JAVA 8 中所做的唯一更改是,如果编码器添加了小于 10 的元素,则剩余的 arraylist 空白位置不会指定为 null。这么说是因为我自己也经历过这种情况,而 eclipse 让我研究了 JAVA 8 的这种变化。

您可以通过查看下面的屏幕截图来证明此更改的合理性。在其中您可以看到 ArrayList 的大小在 Object[10] 中指定为 10,但显示的元素数量仅为 7。此处不显示其余空值元素。在 JAVA 7 中,下面的屏幕截图与只有一个更改相同,即还显示空值元素,如果编码人员正在迭代完整的数组列表,则需要为此编写处理空值的代码,而在 JAVA 8 中,这个负担被消除了编码员/开发人员的负责人。

Screen shot link.

【讨论】:

以上是关于在 Java 8 中,为啥 ArrayList 的默认容量现在为零?的主要内容,如果未能解决你的问题,请参考以下文章

为啥我的 ArrayList 没有打印出 Java 所需的输出?

为啥这个方法返回一个包含所有相同对象的 ArrayList? JAVA

为啥 java.util.Stack 是使用 Vector 而不是 Arraylist 实现的

为啥 Arrays.asList() 返回自己的 ArrayList 实现

在 Java 8 中使用 Lambda 对 ArrayList 进行排序

java Arraylist扩容为啥是1.5倍+1