从一个方法返回一个空集合

3
有没有使用第一种方法比第二种方法更具有内存/性能优势?
第一种方法
public List<Integer> getList1(List<Integer> data) {
    List<Integer> list = new ArrayList<Integer>();
    for (Integer element: data) {
        if (element % 2 == 0) {
           list.add(element);
        }
    }
    return list.isEmpty() ? Collections.<Integer>emptyList() : list;
}

第二个

public List<Integer> getList2(List<Integer> data) {
    List<Integer> list = new ArrayList<Integer>();
    for (Integer element: data) {
        if (element % 2 == 0) {
           list.add(element);
        }
    }
    return list;
}

有些人可能认为这是过早的优化。你的第二个片段比第一个片段更简单,因此维护成本更低。 - Steve C
2
如果你能在创建ArrayList之前返回一个emptyList,那么会有所收益。但在你的情况下,这不会有太大的区别。 - assylias
3个回答

8
任何这些事情的重要程度取决于调用 getList1 返回空列表的频率。但是,JavaDoc for Collections#emptyList 在这里提供了洞察力:
实现注意事项:此方法的实现不需要为每个调用创建单独的 List 对象。使用此方法可能具有与使用同名字段相当的成本。(与此方法不同,该字段不提供类型安全性。)
因此,如果使用 emptyList 而不是每次返回一个新的空列表,您可能会在内存中占用更少的对象,因为 Collections#emptyList 可以自由地每次返回相同的 List<Integer>(因为它是不可变的)。
您还可能获得另一种无关的性能提升(至少对于 Oracle 的 JVM)。在某些情况下,Oracle 的 JVM 可以最初将分配给局部变量的对象分配在堆栈上,仅在引用它们在函数调用终止后仍然存在时将它们转移到堆上。基于堆栈的分配非常快,并且当然不会遭受碎片化的影响。因此,在空列表场景中,您不仅避免了将内存填满空列表,而且还获得了速度上的好处(JVM 不必将您创建的空列表从堆栈复制到堆中)。但是,我不知道该 JVM 优化是否一定适用于您的代码。
如果内存优化对您的代码非常重要,并且您不想依赖堆栈分配的事情,那么可以稍微牺牲一些代码清晰度和微小的运行时成本,如果您不需要列表,则可以完全避免分配列表:
public List<Integer> getList1(List<Integer> data) {
    List<Integer> list = null;
    for (Integer element: data) {
        if (element % 2 == 0) {
           if (list == null ) {
             list = new ArrayList<Integer>();
           }
           list.add(element);
        }
    }
    return list == null ? Collections.<Integer>emptyList() : list;
}

注意,尽管两种方式的结果相同,但其中存在重要的语义差异。在第一个代码块中,getList1不一致:它可能返回可以修改的列表(如果不为空),也可能返回不可变的列表(如果为空)。getList1应该始终返回可变或不可变的列表,而不是有时一种,有时另一种。如果它可以是不可变的,你应该使用Collections#unmodifiableList获取非空列表的不可变版本,并返回它。

1
@Dici:是的。由于emptyList返回的列表是不可变的,因此Collections#emptyList可以每次返回相同的列表。在Oracle的JDK实现中,调用Collections。<Integer> emptyList();两次确实两次返回相同的引用。(它不一定要这样做,你不能依赖它,但在Oracle的JDK8中,在简单的测试中确实如此:http://ideone.com/MGMqSf) - T.J. Crowder
2
感谢T.J Crowder。Javadoc指定返回的列表是不可变的,因此根据其用途,OP的两种方法可能不等效,因为在一种情况下它可以返回一个不可变的列表,而第二种方法将始终返回一个可变的列表。 - Dici
@Dici:确实,事实上我正要把那个加到答案里。 - T.J. Crowder
1
@TheLostMind:感谢您提供这个术语!我对这些东西不太熟悉。显然,它是两个东西的组合:标量替换逃逸分析:http://www.stefankrause.net/wp/?p=64,http://www.ibm.com/developerworks/java/library/j-jtp09275/index.html 我不知道OP的代码是否可以通过这种方式进行优化(我现在已经澄清了答案)。我们需要Marko在哪里?! :-) - T.J. Crowder
1
@T.J.Crowder - 你能告诉我你从哪里得知的 - 我相信Oracle的JVM最初(在合理范围内)将分配给本地变量的对象分配到堆栈上,只有在对它们的引用在函数调用终止后仍然存在时才将它们转移到堆上,因为Oracle关于逃逸分析的文档(http://docs.oracle.com/javase/7/docs/technotes/guides/vm/performance-enhancements-7.html#escapeAnalysis)没有提到它。它是否在您最后一条评论中提供的链接之一中提到? :) - TheLostMind
显示剩余3条评论

0

既然你最终还是要返回list,那么条件语句有什么意义呢?这只是多余的。

更倾向于第二种方式。

此外,请查看emptyList()方法文档。

实现注意事项:此方法的实现不需要为每个调用创建单独的List对象。使用此方法可能具有与使用同名字段相当的成本。(与此方法不同,该字段不提供类型安全性。)


0

这里有一个微妙的区别。

如果 getList1 返回一个空列表,那么该列表是不可变的

List<Integer> l1 = getList1();
assertTrue(l1.isEmpty());
l1.add(1); //exception

List<Integer> l2 = getList2();
assertTrue(l2.isEmpty());
l2.add(1); //OK

还有:

List<Integer> l1a = getList1();
List<Integer> l1b = getList1();
assertTrue(l1a.isEmpty());
assertTrue(l1b.isEmpty());
assertSame(l1a, l1b); //same reference so there is some saving in memory

List<Integer> l2a = getList2();
List<Integer> l2b = getList2();
assertTrue(l2a.isEmpty());
assertTrue(l2b.isEmpty());
assertNotSame(l2a, l2b); //not same reference, two unique lists

两者是相互关联的,如果不是不可变的话,共享引用将无法正常工作。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接