在暴露内部收集物品时是否应使用Iterator或Iterable?

kza*_*kza 10 java iterator iterable arraylist copyonwritearraylist

我有一个包含私有可变数据列表的类.

我需要在以下条件下公开列表项:

  • 清单不应在外面修改;
  • 应该清楚使用getter函数的开发人员无法修改他们获得的列表.

应该将哪个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();
    }
}
Run Code Online (Sandbox Code Playgroud)

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

Mar*_*o13 7

"最佳"解决方案实际上取决于预期的应用模式(而不是关于"意见",如关闭选民所建议的那样).每种可能的解决方案都有可以客观判断的优点和缺点(并且必须由开发人员进行判断).


编辑:已经有一个问题" 我应该返回一个集合还是一个流? ",Brian Goetz给出了精心解答的答案.在做出任何决定之前,您也应该查阅这些答案.我的回答并不是指流,而只是指将数据暴露为集合的不同方式,指出了不同方法的优缺点和含义.


返回迭代器

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

Iterator<String> it = data.getUnmodifiableIterator();
while (it.hasNext()) {
    String s = it.next();
    process(s);
}
Run Code Online (Sandbox Code Playgroud)

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

for (String s : data.getUnmodifiableIterable()) {
    process(s);
}
Run Code Online (Sandbox Code Playgroud)

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

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

这里设计空间的一个自由度是你是否隐藏了其他信息:当你有一个时List,你可以提供一种方法

private List<String> internalData;

List<String> getData() {
    return Collections.unmodifiableList(internalData);
}
Run Code Online (Sandbox Code Playgroud)

或者,您可能不太关注内部数据的类型:

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

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


公开内部数据的副本:

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

private List<String> internalData;

List<String> getData() {
    return new ArrayList<String>(internalData);
}
Run Code Online (Sandbox Code Playgroud)

这可能具有(可能大且频繁)存储器副本的缺点,因此仅在集合"小"时才应考虑.

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


暴露一个 CopyOnWriteArrayList

暴露一个CopyOnWriteArrayList通过它Iterator或作为一个Iterable可能不是一个好主意:调用者可以选择通过Iterator#remove调用修改它,你明确想避免这种情况.

暴露CopyOnWriteArrayList包裹成a 的解决方案Collections.unmodifiableList可能是一种选择.乍一看,它可能看起来像一个多余的厚防火墙,但它肯定是合理的 - 见下一段.


一般考虑

无论如何,你应该虔诚地记录这种行为.特别是,您应该记录调用者应该以任何方式更改返回的数据(无论是否可以在不引起异常的情况下).

除此之外,还有一个令人不安的权衡:您可以在文档中准确,也可以避免在文档中公开实现细节.

考虑以下情况:

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

这里的文档实际上也说明......

/* ...
 * The returned list is a VIEW on the internal data. 
 * Changes in the internal data will be visible in 
 * the returned list.
 */
Run Code Online (Sandbox Code Playgroud)

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

for (String s : data.getData()) {
    ...
    data.changeInternalData();
}
Run Code Online (Sandbox Code Playgroud)

这个循环将以a打破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);
Run Code Online (Sandbox Code Playgroud)

ConcurrentModificationException如果返回了内部数据的真实副本,或者使用a CopyOnWriteArrayList(每个包装成a Collections.unmodifiableList),就可以避免类似的事情.在这方面,这将是"最安全"的解决方案:

  • 调用者无法修改返回的列表
  • 调用者无法直接修改内部状态
  • 如果调用者间接修改内部状态,则迭代仍然有效

但是,必须考虑相应的应用案例是否真的需要这么多"安全",以及如何以一种仍然允许更改内部实现细节的方式记录它.