Java变量类型:HashSet或其他实现的集合?

8
我经常看到类中的字段声明如 List<String> list = new ArrayList<>();Set<String> set = new HashSet<>();。对我来说,使用接口作为变量类型以提供实现的灵活性是很有道理的。上述示例仍然定义了必须使用哪种Collection,或者允许哪些操作以及在某些情况下应该如何行为(由于文档)。
现在考虑这种情况:实际上只需要使用Collection(甚至是Iterable)接口的功能来使用类中的字段,并且Collection的类型并不重要,或者我不想过度指定它。因此,我选择例如HashSet作为实现,并将字段声明为Collection<String> collection = new HashSet<>();
在这种情况下,该字段是否应该是Set类型?如果是,这种声明方式是否是不良实践?如果是,为什么?或者,尽可能少地指定实际类型(仍然提供所有所需方法)是好的实践。我之所以问这个问题,是因为我几乎从未见过这样的声明,最近我越来越多地发现自己只需要指定Collection接口的功能。
// Only need Collection features, but decided to use a LinkedList
private final Collection<Listener> registeredListeners = new LinkedList<>();

public void init() {
    ExampleListener listener = new ExampleListener();
    registerListenerSomewhere(listener);
    registeredListeners.add(listener);
    listener = new ExampleListener();
    registerListenerSomewhere(listener);
    registeredListeners.add(listener);
}

public void reset() {
    for (Listener listener : registeredListeners) {
        unregisterListenerSomewhere(listener);
    }

    registeredListeners.clear();
}

2
很棒的问题。这在很大程度上是个人品味和约定俗成的问题。在大多数情况下,我发现List或Set更加简洁清晰,除了方法输入外,通常应该尽可能使用通用类型。 - shmosel
2
问题已经在更广泛的意义上得到了回答,所以作为一个看似过于具体的问题,与您的特定示例有关:当有人调用init()方法两次时会发生什么?只需考虑这里SetList的选择如何干扰registerListenerSomewhere方法的行为:它将在那里存储在Set还是List中?如果它在那里存储在List中,但在您的类中存储在Set中,则调用reset将仅删除一个侦听器实例,而不是所有侦听器实例。这很困难... - Marco13
3个回答

5

由于您的示例使用了一个私有字段,因此隐藏实现类型并不那么重要。您(或者维护此类的人)可以随时查看字段的初始化程序来查看它是什么。

不过,根据它的用法,声明一个更具体的接口可能是值得的。将其声明为List表示允许重复项,并且排序很重要。将其声明为Set表示不允许重复项,并且排序不重要。如果有一些重要的内容,甚至可以声明该字段具有特定的实现类。例如,将其声明为LinkedHashSet表示不允许重复项,但排序重要的。

是否使用接口以及要使用哪个接口的选择在类型出现在类的公共API中以及此类上的兼容性约束方面变得更加重要。例如,假设有一个方法

public ??? getRegisteredListeners() {
    return ...
}

现在返回类型的选择会影响其他类。如果你可以更改所有的调用者,也许问题不大,只需要编辑其他文件即可。但是假设调用者是一个您无法控制的应用程序。现在接口的选择非常重要,因为您不能更改它,否则可能会破坏应用程序。通常的规则是选择支持您期望调用者执行的操作的最抽象的接口。
大多数Java SE API返回Collection。这提供了一定程度的抽象,使其与底层实现分离,同时还为调用者提供了一组合理的操作。调用者可以遍历、获取大小、进行包含检查或将所有元素复制到另一集合中。
有些代码库使用Iterable作为最抽象的接口返回。它只允许调用者迭代。有时这是必需的,但与Collection相比,可能有一定的限制。
另一种选择是返回Stream。如果您认为调用者可能想要使用流操作(例如filter、map、find等)而不是迭代或使用集合操作,则此选项很有帮助。
请注意,如果您选择返回Collection或Iterable,则需要确保返回一个不可修改的视图或进行防御性复制。否则,调用者可能会修改您的类的内部数据,这可能会导致错误。(是的,即使Iterable也可以允许修改!考虑获取Iterator,然后调用remove()方法。)如果您返回一个Stream,则不需要担心这个问题,因为您不能使用Stream修改底层源。
请注意,我将您关于字段声明的问题转化为了有关方法返回类型的问题。在Java中,“按接口编程”的概念非常普遍。我认为对于局部变量来说这并不太重要(这就是为什么通常可以使用var),对于私有字段来说也没什么关系,因为这些字段(几乎)只影响它们所在的类。然而,在API签名中,“按接口编程”原则非常重要,因此这些情况是您真正需要考虑接口类型的情况。私有字段则不太重要。
(最后一条说明:有一种情况需要关注私有字段的类型,那就是当您使用一个反射框架直接操作私有字段时。在这种情况下,您需要将这些字段视为公共字段 - 就像方法返回类型一样 - 即使它们没有声明为public。)

优秀的回答。关键在于“通常的规则是选择支持调用者期望执行的操作的最抽象接口。” - Thiyagu

3

就像所有事情一样,这是一个权衡的问题。有两个对立的力量。

  • 类型越通用,实现的自由度就越大。如果你使用 Collection ,你可以自由地使用 ArrayListHashSetLinkedList ,而不会影响用户/调用者。

  • 返回类型越通用,可供用户/调用者使用的功能就越少。 List 提供基于索引的查找。 SortedSet 通过 headSettailSetsubSet 等方法轻松获取连续的子集。 NavigableSet 提供高效的 O(log n) 二分查找方法。如果返回 Collection ,则这些功能都不可用。只能使用最通用的访问函数。

此外,子类型保证了 Collection 不具备的特殊属性:Set 包含唯一项。SortedSet 排序.List 具有顺序;它们不是无序的物品包。如果使用 Collection ,则用户/调用者不能保证这些属性成立。他们可能被迫进行防御性编程,并且例如处理重复项,即使你知道不会有重复项。

一个合理的决策过程可能是:

  1. 如果保证 O(1) 索引访问,请使用 List
  2. 如果元素排序且唯一,请使用 SortedSetNavigableSet
  3. 如果元素唯一性得到保证而顺序不重要,请使用 Set
  4. 否则,请使用 Collection

2

这取决于您想要使用集合对象做什么。

最初的回答:

这取决于你对集合对象的具体需求。

Collection<String> cSet = new HashSet<>();
Collection<String> cList = new ArrayList<>();

在这种情况下,如果您愿意,可以做以下操作:

在这种情况下,如果您愿意,可以执行以下操作:

cSet = cList;

But if you do like :

Set<String> cSet = new HashSet<>(); 

尽管您可以使用构造函数构建新列表,但上述操作是不允许的。

"Original Answer"翻译成中文是"最初的回答"。

 Set<String> set = new HashSet<>();
 List<String> list = new ArrayList<>();
 list = new ArrayList<>(set);

基本上,根据使用情况,您可以使用CollectionSet接口。


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