是否为HashSet或其他实现使用Java变量类型Collection?

时间:2019-04-09 21:20:57

标签: java collections types interface declaration

我经常看到类中的字段像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();
}

3 个答案:

答案 0 :(得分:5)

由于您的示例使用了私有字段,因此与隐藏实现类型无关紧要。您(或正在维护此类的人)始终可以仅查看该字段的初始化程序以了解其内容。

但是,取决于使用方式,可能值得为该字段声明一个更特定的接口。声明为List表示允许重复,并且顺序很重要。声明为Set表示不允许重复,并且顺序不重要。如果有重要意义,您甚至可以声明该字段具有特定的实现类。例如,声明为LinkedHashSet表示不允许重复,但是的顺序是重要的。

如果类型出现在类的公共API中,以及对该类的兼容性限制是什么,则是否使用接口以及使用哪种接口的选择就变得更加重要。例如,假设有一种方法

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

现在,返回类型的选择会影响其他类。如果您可以更改所有呼叫者,也许没什么大不了的,那么您只需要编辑其他文件即可。但是假设调用者是您无法控制的应用程序。现在,接口的选择非常关键,因为在不破坏应用程序的情况下就无法更改接口。通常,这里的规则是选择最抽象的接口,以支持您希望呼叫者希望执行的操作。

大多数Java SE API返回Collection。这为基础实现提供了一定程度的抽象,但同时也为调用方提供了一组合理的操作。调用者可以迭代,获取大小,进行包含检查或将所有元素复制到另一个集合。

某些代码库使用Iterable作为返回的最抽象的接口。它所做的只是允许调用方进行迭代。有时候,这是必需的,但与Collection相比,可能有些局限。

另一种替代方法是返回Stream。如果您认为调用方可能想使用流的操作(例如过滤器,映射,查找等),而不是迭代或使用收集操作,则这很有用。

请注意,如果您选择返回CollectionIterable,则需要确保返回不可修改的视图或进行防御性复制。否则,调用者可能会修改您班级的内部数据,这可能会导致错误。 (是的,甚至Iterable都可以允许修改!考虑获取Iterator然后调用remove()方法。)如果返回Stream,则不需要不必担心,因为您不能使用Stream来修改基础源。

请注意,我把您关于字段声明的问题变成了关于方法返回类型的声明的问题。这种“编程到接口”的思想在Java中非常普遍。在我看来,对于局部变量来说并没有太大关系(这就是为什么通常使用var的原因),而对于私有字段则无关紧要,因为根据定义,这些(几乎)只影响其中的类。他们被宣布。但是,“接口编程”原则对于API签名非常重要,因此在这些情况下,您确实需要考虑接口类型。私有字段,不是很多。

(最后一点:在某些情况下,您需要关注私有字段的类型,那就是在使用直接操作私有字段的反射框架时。在这种情况下,您需要考虑一下那些字段是公共的-就像方法返回类型一样-即使未声明为public。)

答案 1 :(得分: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 :(得分:2)

这实际上取决于您要对集合对象执行的操作。

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

在这种情况下,您可以执行以下操作:

cSet = cList;

但是,如果您喜欢:

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

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

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

因此,基本上可以根据用途使用CollectionSet界面。