为什么在Java中list.size()>0比list.isEmpty()慢?

59
为什么在Java中,list.size()>0的速度比list.isEmpty()慢?换句话说,为什么isEmpty()优于size()>0
当我查看ArrayList的实现时,它看起来速度应该是一样的: ArrayList.size()
    /**
     * Returns the number of elements in this list.
     *
     * @return the number of elements in this list
     */
    public int size() {
      return size;
    }

ArrayList.isEmpty()

:检查ArrayList是否为空。

    /**
     * Returns <tt>true</tt> if this list contains no elements.
     *
     * @return <tt>true</tt> if this list contains no elements
     */
    public boolean isEmpty() {
        return size == 0;
     }

如果我们编写一个简单的程序来获取两种方法所需的时间,那么在所有情况下,size()方法都需要比isEmpty()方法花费更多的时间。为什么会这样呢?

以下是我的测试代码:

import java.util.List;
import java.util.Vector;

public class Main {
    public static void main(String[] args) {
        List l=new Vector();
        int i=0;
        for(i=0;i<10000;i++){
            l.add(new Integer(i).toString());
        }
        System.out.println(i);
        Long sTime=System.nanoTime();
        l.size();
        Long eTime=System.nanoTime();
        l.isEmpty();
        Long eeTime=System.nanoTime();
        System.out.println(eTime-sTime);
        System.out.println(eeTime-eTime);
    }
}

所有情况下都有 eTime-sTime>eeTime-eTime。为什么?

9个回答

106

对于 ArrayList,你说得对——大致上操作时间是相同的。

对于其他实现的List——例如一个简单的链表*——计算大小可能需要很长时间,而你实际上只关心它是否大于零。

所以,如果你绝对确定列表是ArrayList的实现,并且永远不会更改,那么这并不重要;但是:

  1. 将自己约束于特定的实现是不好的编程习惯。
  2. 如果在几年后代码重构时发生变化,测试将显示“它可以工作”,但效率低于之前。
  3. 即使在最好的情况下,size() == 0也不比isEmpty()更快,因此没有令人信服的理由使用前者。
  4. isEmpty()更清晰地定义了您实际关心和测试的内容,因此使您的代码更易于理解一些。

* 我最初在这里写了LinkedList,隐含地引用了java.util.LinkedList,尽管该特定实现明确存储其大小,使size()成为O(1)操作。简单的链表操作可能不会这样做,在更一般的意义上,List的实现没有效率保证。


6
“LinkedList”(又名“java.util.LinkedList”)也会存储其大小,因此调用“size()”方法与“ArrayList”一样快,但总的来说,这个观点仍然非常正确。 - Joachim Sauer
@dtszzs:运行时间不一样,这就是我测试的结果,让我感到不舒服。 - Ashish Agarwal
4
你是@Sam Rudolph,对于测试方法,有很多方法会导致微基准测试出现错误。 - Joachim Sauer
@Joachim - 是的,关于 java.util.LinkedList 的观点很好,我已经进行了修改,让那部分更加清晰明了。 - Andrzej Doyle

74

你的测试代码有缺陷。

只需反转顺序,即先调用isEmpty,再调用size > 0,就会得到相反的结果。这是由于类加载、缓存等原因造成的。


2
@JLR:是的,我接受你的观点。在两个不同的项目中测试了这两种方法,结果非常相似。感谢你解决我的疑惑。 - Ashish Agarwal

17

10

.size()需要查看整个列表,而.isEmpty()可以在第一个元素处停止。

显然,这取决于实现方式,但是正如之前所说的,如果您不需要知道实际大小,为什么要计算所有元素呢?


6
你说:
在所有情况下,为什么都是 eTime-sTime>eeTime-eTime
首先,这可能与你的测试代码有关。你不能同时测试调用 l.size() 和 l.isEmpty() 的速度,因为它们都查询相同的值。最有可能的情况是,调用 l.size() 已经将列表的大小加载到了 CPU 缓存中,因此调用 l.isEmpty() 会更快。
你可以尝试在两个独立的程序中分别调用 l.size() 几百万次和 l.isEmpty() 几百万次,但理论上编译器可能会优化掉所有这些调用,因为你实际上并没有对结果做任何事情。
无论如何,这两者之间的性能差异将是微不足道的,特别是一旦你进行比较以查看列表是否为空(l.size() == 0)。最有可能生成的代码几乎完全相似。正如其他帖子中提到的,你应该优化可读性,而不是速度。
编辑:我自己测试过了。结果几乎相同。在 Vector 上使用 size() 和 isEmpty() 产生了不同的结果,长时间运行时,两者都没有持续胜出。当在 ArrayList 上运行时,size() 看起来更快,但差别不大。这很可能是因为访问 Vector 是同步的,所以当尝试对这些方法的访问进行基准测试时,你实际上看到的是同步开销,这可能非常敏感。
需要记住的是,当你试图优化一个方法调用的执行时间差几个纳秒时,那么你做错了。首先要做好基础工作,例如在应该使用 long 的地方使用 Long。

没有,实际上我在两个不同的程序中运行了这两种情况,只是为了消除疑虑,结果是一样的。size()方法的运行时间比isEmpty()方法长10倍。 - Ashish Agarwal

2

在链表中计算项数可能会非常慢。


@spdenme:但它没有计数,只是返回一个值? - Ashish Agarwal
在你的例子中,你有一个 ArrayList - Brian Agnew
List是一个接口。它的实现方式会影响这两种方法的相对性能。由于size()是一个经常被调用的方法,大多数实现决定维护一个size字段的成本是值得的。 - Stephen Denne

2

鉴于这两种实现方式,速度应该是相同的,这是真的。

但是,这些方法远远不是唯一可能的实现方式。例如,一个原始的链表(不单独存储大小)可以比 size() 调用更快地回答 isEmpty()

更重要的是:isEmpty() 正好描述了您的意图,而 size()==0 过于复杂(当然并不是极其复杂,但任何不必要的复杂性都应该避免)。


但是如果你看到它们的实现,那些看起来非常非常简单? - Ashish Agarwal
你看到的那些,没错。但正如@dtsazza所解释的那样:实现并不是唯一重要的事情,意图和可读性也很重要。而且可能会有其他Collectons实现,其中size()可能不那么容易实现(例如WeakHashMap或其他动态集合)。 - Joachim Sauer

1
根据 PMD(基于静态规则的 Java 源代码分析器),推荐使用 isEmpty()。您可以在此处找到 PMD 规则集。搜索“UseCollectionIsEmpty”规则。

http://pmd.sourceforge.net/rules/design.html

在我看来,这也有助于保持整个源代码的一致性,而不是一半的人使用isEmpty(),另一半使用size()==0。


0

一般而言,很难说哪个更快,因为它取决于您使用的List接口的实现方式。

假设我们正在谈论ArrayList。查找ArrayList的源代码,您可以在JDK安装目录下的src.zip文件中找到它。方法isEmptysize的源代码如下(适用于Windows的Sun JDK 1.6更新16):

public boolean isEmpty() {
    return size == 0;
}

public int size() {
    return size;
}

你可以轻松地看到,表达式isEmpty()size() == 0都会转化为完全相同的语句,因此它们之间并没有速度上的差异。

如果你对于接口List的其他实现方式感兴趣,可以自行查找源代码并了解相关信息。


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