最近,我对一些 Java 集合没有方法 size() 的恒定时间操作这一事实感到惊讶。
虽然我了解到集合的并发实现为了获得并发性(ConcurrentLinkedQueue、ConcurrentSkipListSet、LinkedTransferQueue 等中的大小为 O(n))而做出了一些妥协,但好消息是这在 API 文档中得到了正确记录。
让我担心的是某些集合的方法返回的视图上的方法大小的性能。例如,TreeSet.tailSet返回其元素大于或等于 fromElement 的支持集部分的视图。令我惊讶的是,在返回的 SortedSet 上调用 size 在时间上是线性的,即 O(n)。至少那是我设法从 OpenJDK 的源代码中挖掘出来的:在 TreeSet 中作为 TreeMap 的包装器实现,在 TreeMap 中,有一个 EntrySetView 类,其大小方法如下:
abstract class EntrySetView extends AbstractSet<Map.Entry<K,V>> {
private transient int size = -1, sizeModCount;
public int size() {
if (fromStart && toEnd)
return m.size();
if (size == -1 || sizeModCount != m.modCount) {
sizeModCount = m.modCount;
size = 0;
Iterator i = iterator();
while (i.hasNext()) {
size++;
i.next();
}
}
return size;
}
....
}
这意味着第一次调用大小是 O(n),然后只要不修改支持映射,它就会被缓存。我无法在 API 文档中找到这个事实。更有效的实现将是 O(log n),在子树大小的缓存中进行内存权衡。由于为避免代码重复(TreeSet 作为 TreeMap 的包装器)而进行了此类权衡,因此我看不出出于性能原因不应制作它们的原因。
不管我对 TreeSet 的 OpenJDK 实现的(非常简短的)分析是对还是错,我想知道是否有关于许多此类操作的性能的详细而完整的文档,尤其是那些完全出乎意料的操作?