10

我有一个带有私有可变数据列表的类。

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

  • 列表不应在外部修改;
  • 使用 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();
    }
}

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

4

3 回答 3

7

“最佳”解决方案实际上取决于预期的应用程序模式(而不是“意见”,正如密切投票者所建议的那样)。每个可能的解决方案都有可以客观判断的优缺点(并且必须由开发人员判断)。


编辑:已经有一个问题“我应该返回 Collection 还是 Stream? ”,Brian Goetz 给出了详尽的回答。在做出任何决定之前,您也应该查阅此答案。我的回答不是针对流,而只是针对将数据公开为集合的不同方式,并指出不同方法的优缺点和影响。


返回一个迭代器

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

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...集合中。任何修改返回集合的尝试都将导致UnsupportedOperationException抛出一个,明确指出客户端不应修改数据。

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

private List<String> internalData;

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

或者,您可以不太具体地了解内部数据的类型:

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

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


公开内部数据的副本:

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

private List<String> internalData;

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

这可能具有(可能大且频繁)内存副本的缺点,因此仅应在集合“小”时考虑。

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


暴露一个CopyOnWriteArrayList

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

暴露CopyOnWriteArrayList包装成 a的解决方案Collections.unmodifiableList可能是一种选择。乍一看,它可能看起来像一个多余的防火墙,但它绝对是合理的——请参阅下一段。


一般注意事项

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

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

考虑以下情况:

/**
 * 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();
}

此循环将中断 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);

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

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

但是人们必须考虑是否真的需要对相应的应用案例进行如此多的“安全性”,以及如何以一种仍然允许更改内部实现细节的方式记录这一点。

于 2015-05-12T16:16:14.237 回答
1

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

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

如果您想跳上 Java 8 马车,返回 aStream可能是一种更“现代”的方法。

于 2015-05-12T15:38:29.590 回答
0

通过封装规则,您必须始终返回一个不可修改的列表,在您的情况下是一个设计规则,因此返回 Collections.unmodifiableCollection,并且您不需要将方法命名为 getUnmodifiable,使用 getter 命名约定并使用 Javadoc 告诉其他开发人员您返回什么样的列表以及为什么...粗心的用户将收到异常警报!

于 2015-05-12T15:22:18.643 回答