公开内部集合项时应该使用 Iterator 还是 Iterable?

Should Iterator or Iterable be used when exposing internal collection items?

我有一个 class,其中包含私有可变数据列表。

我需要在满足以下条件的情况下公开列表项:

哪个 getter 函数应该标记为推荐方法?或者您能提供更好的解决方案吗?

class DataProcessor {
    private final ArrayList<String> simpleData = new ArrayList<>();
    private final CopyOnWriteArrayList<String> copyData = new CopyOnWriteArrayList<>();

    public void modifyData() {
        ...
    }

    public Iterable<String> getUnmodifiableIterable() {
        return Collections.unmodifiableCollection(simpleData);
    }

    public Iterator<String> getUnmodifiableIterator() {
        return Collections.unmodifiableCollection(simpleData).iterator();
    }

    public Iterable<String> getCopyIterable() {
        return copyData;
    }

    public Iterator<String> getCopyIterator() {
        return copyData.iterator();
    }
}

UPD:这个问题来自关于列表 getter 实施的最佳实践的真实代码审查讨论

根据封装规则,你必须始终 return 一个不可修改的列表,在你的情况下是一个设计规则,所以 return Collections.unmodifiableCollection,你不需要命名方法为 getUnmodifiable,使用 getter 命名约定并使用 Javadoc 告诉其他开发人员您 return 的列表类型以及原因...粗心的用户将收到异常警告!!

通常,Iterator 仅与 Iterable 一起使用,用于 for-each 循环。看到一个非 Iterable 类型包含一个返回 Iterator 的方法会很奇怪,而且它不能在 for-each 循环中使用可能会让用户感到不安。

所以我建议在这种情况下使用 Iterable。如果有意义的话,您甚至可以拥有 class implements Iterable

如果你想跳上 Java 8 马车,返回 Stream 可能是更 "modern" 的方法。

"best" 解决方案实际上取决于预期的应用程序模式(而不是 "opinions",正如接近投票者所建议的那样)。每种可能的解决方案都有利有弊,可以客观地判断(而有待开发者判断)。


Edit: There already was a question "Should I return a Collection or a Stream?", with an elaborate answers by Brian Goetz. You should consult this answers as well before making any decision. My answer does not refer to streams, but only to different ways of exposing the data as a collection, pointing out the pros, cons and implications of the different approaches.


返回迭代器

只返回一个 Iterator 是不方便的,不管进一步的细节,例如是否允许修改。 foreach 循环中不能单独使用 Iterator。所以客户必须写

Iterator<String> it = data.getUnmodifiableIterator();
while (it.hasNext()) {
    String s = it.next();
    process(s);
}

而基本上所有其他解决方案都允许他们只写

for (String s : data.getUnmodifiableIterable()) {
    process(s);
}

公开内部数据的 Collections.unmodifiable... 视图:

您可以公开内部数据结构,包装到相应的 Collections.unmodifiable... 集合中。任何修改 returned 集合的尝试都会导致抛出 UnsupportedOperationException,明确指出客户端不应修改数据。

设计中的一个自由度space这里是你是否隐藏额外的信息:当你有一个List时,你可以提供一个方法

private List<String> internalData;

List<String> getData() {
    return Collections.unmodifiableList(internalData);
}

或者,您可以不那么具体地说明内部数据的类型:

  • 如果调用者不能使用 List#get(int index) 方法进行索引访问,那么您可以将此方法的 return 类型更改为 Collection<String>.
  • 如果调用者另外不应该能够通过调用 Collection'size() 获得 returned 序列的 size,那么你可以 return一个Iterable<String>

还要考虑到,当公开不太具体的接口时,您稍后可以选择将内部数据的类型更改为 Set<String>,例如。如果您 保证 到 return 一个 List<String>,那么稍后更改此设置可能会引起一些麻烦。


公开一份内部数据:

一个非常简单的解决方案是 return 列表的副本:

private List<String> internalData;

List<String> getData() {
    return new ArrayList<String>(internalData);
}

这可能有内存副本(可能很大且频繁)的缺点,因此只有在集合为 "small" 时才应考虑。

此外,调用者将能够修改列表,并且他可能希望更改反映在内部状态中(事实并非如此)。通过将 new 列表额外包装到 Collections.unmodifiableList 中可以缓解此问题。


暴露一个CopyOnWriteArrayList

通过 Iterator 或作为 Iterable 公开 CopyOnWriteArrayList 可能不是一个好主意:调用者可以选择通过 Iterator#remove 调用来修改它,并且您明确希望避免这种情况。

将包裹在 Collections.unmodifiableList 中的 CopyOnWriteArrayList 公开的解决方案可能是一个选项。乍一看,它可能看起来像一个过厚的防火墙,但它绝对是合理的 - 请参阅下一段。


一般注意事项

无论如何,您都应该记录 虔诚的行为。特别是,您应该记录调用者 而不是 应该以任何方式更改 returned 数据(无论是否可能而不会导致异常)。

除此之外,还有一个令人不安的权衡:您可以在文档中做到精确,或者避免在文档中公开实施细节。

考虑以下情况:

/**
 * Returns the data. The returned list is unmodifiable. 
 */
List<String> getData() {
    return Collections.unmodifiableList(internalData);
}

这里的文档应该实际上也声明...

/* ...
 * The returned list is a VIEW on the internal data. 
 * Changes in the internal data will be visible in 
 * the returned list.
 */

考虑到线程安全和迭代期间的行为,这可能是一个重要信息。考虑一个迭代内部数据不可修改视图的循环。并考虑在这个循环中,有人调用了一个导致内部数据修改的函数:

for (String s : data.getData()) {
    ...
    data.changeInternalData();
}

此循环将以 ConcurrentModificationException 中断,因为内部数据在迭代时被修改。

此处关于文档的权衡指的是,一旦指定某种行为,客户将依赖这种行为。想象一下客户这样做:

List<String> list = data.getList();
int oldSize = list.size();
data.insertElementToInternalData();

// Here, the client relies on the fact that he received
// a VIEW on the internal data:
int newSize = list.size();
assertTrue(newSize == oldSize+1);

如果内部数据的真实副本已被 return 编辑,或者通过使用 CopyOnWriteArrayList(每个包装成 Collections.unmodifiableList).这将是 "safest" 解决方案,在这方面:

  • 调用者不能修改returned列表
  • 调用者不能直接修改内部状态
  • 如果调用者间接修改内部状态,那么迭代仍然有效

但是必须考虑各个应用案例是否真的需要这么多 "safety",以及如何以一种仍然允许更改内部实现细节的方式记录下来。