Java HashSet 或其他实现的变量类型 Collection?
Java variable type Collection for HashSet or other implementations?
我经常看到 classes 中字段的 List<String> list = new ArrayList<>();
或 Set<String> set = new HashSet<>();
声明。对我来说,使用变量类型的接口来提供实现的灵活性是非常有意义的。上面的示例仍然定义了必须使用哪种 Collection
,分别允许哪些操作以及在某些情况下它应该如何表现(由于文档)。
现在考虑这样一种情况,实际上只需要 Collection
(甚至 Iterable
)接口的功能来使用 class 中的字段和 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();
}
这实际上取决于您要对集合对象执行的操作。
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);
所以基本上根据用途,您可以使用 Collection
或 Set
界面。
与所有事情一样,这是一个权衡的问题。有两种相反的力量。
类型越通用,实现的自由度就越大。如果您使用 Collection
,您可以自由使用 ArrayList
、HashSet
或 LinkedList
,而不会影响 user/caller.
return 类型越通用,user/caller 可用的功能就越少。 List
提供基于索引的查找。 SortedSet
可以很容易地通过 headSet
、tailSet
和 subSet
获得连续的子集。 NavigableSet
提供高效的 O(log n) 二进制搜索查找方法。如果您 return Collection
,其中 none 可用。只能使用最通用的访问函数。
此外,子类型保证了 Collection
没有的特殊属性:Set
s 拥有独特的项目。 SortedSet
已排序。 List
s有订单;它们不是未排序的物品袋。如果您使用 Collection
,则 user/caller 不一定会假设这些属性成立。他们可能被迫进行防御性编码,例如,处理重复项目,即使您知道不会有重复项目。
合理的决策过程可能是:
- 如果保证 O(1) 索引访问,使用
List
.
- 如果元素已排序且唯一,请使用
SortedSet
或 NavigableSet
。
- 如果元素唯一性得到保证而顺序不保证,则使用
Set
。
- 否则,使用
Collection
。
由于您的示例使用 私有字段,因此隐藏实现类型并不重要。您(或维护此 class 的任何人)总是可以查看该字段的初始化程序以了解它是什么。
不过,根据它的使用方式,可能值得为该字段声明一个更具体的接口。将其声明为 List
表示允许重复并且排序很重要。将其声明为 Set
表示不允许重复且排序不重要。你甚至可以声明该字段有一个特定的实现 class 如果它有一些重要的东西。例如,将其声明为 LinkedHashSet
表示不允许重复,但排序 是 重要。
如果类型出现在 class 的 public API 中,则选择是否使用接口以及使用什么接口变得更加重要此 class 的兼容性限制是什么。例如,假设有一个方法
public ??? getRegisteredListeners() {
return ...
}
现在 return 类型的选择会影响其他 class 类型。如果你能改变所有的来电者,也许这没什么大不了的,你只需要编辑其他文件。但是假设调用者是一个您无法控制的应用程序。现在接口的选择很关键,因为您不能在不破坏应用程序的情况下更改它。这里的规则通常是选择最抽象的接口,支持您希望调用者执行的操作。
大多数 Java SE APIs return Collection
。这为底层实现提供了相当程度的抽象,但它也为调用者提供了一组合理的操作。调用者可以迭代、获取大小、进行包含检查或将所有元素复制到另一个集合。
一些代码库使用 Iterable
作为 return 的最抽象接口。它所做的只是允许调用者迭代。有时这就是所有必要的,但与 Collection
.
相比可能有些限制
另一个选择是return一个Stream
。如果您认为调用者可能想要使用流的操作(例如过滤器、映射、查找等)而不是迭代或使用集合操作,这将很有帮助。
请注意,如果您选择 return Collection
或 Iterable
,您需要确保您 return 一个不可修改的视图或制作一个防御性副本。否则,呼叫者可能会修改您的 class 的内部数据,这可能会导致错误。 (是的,即使 Iterable
也可以允许修改!考虑得到一个 Iterator
然后调用 remove()
方法。)如果你 return 一个 Stream
,你不不必担心这一点,因为您不能使用 Stream
修改基础源。
请注意,我将您关于字段声明的问题变成了关于方法 return 类型声明的问题。 "program to the interface" 这种想法在 Java 中非常普遍。在我看来,局部变量并不重要(这就是为什么通常可以使用 var
),而对于私有字段则无关紧要,因为那些(几乎)根据定义只影响 class 在其中声明它们。但是,"program to the interface" 原则对于 API 签名来说 非常 重要,因此在这些情况下您确实需要考虑接口类型。私有字段,没那么多。
(最后一点:有一种情况你需要关注私有字段的类型,那就是当你使用一个直接操作私有字段的反射框架时。在这种情况下,你需要将这些字段视为 public——就像方法 return 类型一样——即使它们没有声明 public
。)
我经常看到 classes 中字段的 List<String> list = new ArrayList<>();
或 Set<String> set = new HashSet<>();
声明。对我来说,使用变量类型的接口来提供实现的灵活性是非常有意义的。上面的示例仍然定义了必须使用哪种 Collection
,分别允许哪些操作以及在某些情况下它应该如何表现(由于文档)。
现在考虑这样一种情况,实际上只需要 Collection
(甚至 Iterable
)接口的功能来使用 class 中的字段和 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();
}
这实际上取决于您要对集合对象执行的操作。
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);
所以基本上根据用途,您可以使用 Collection
或 Set
界面。
与所有事情一样,这是一个权衡的问题。有两种相反的力量。
类型越通用,实现的自由度就越大。如果您使用
Collection
,您可以自由使用ArrayList
、HashSet
或LinkedList
,而不会影响 user/caller.return 类型越通用,user/caller 可用的功能就越少。
List
提供基于索引的查找。SortedSet
可以很容易地通过headSet
、tailSet
和subSet
获得连续的子集。NavigableSet
提供高效的 O(log n) 二进制搜索查找方法。如果您 returnCollection
,其中 none 可用。只能使用最通用的访问函数。
此外,子类型保证了 Collection
没有的特殊属性:Set
s 拥有独特的项目。 SortedSet
已排序。 List
s有订单;它们不是未排序的物品袋。如果您使用 Collection
,则 user/caller 不一定会假设这些属性成立。他们可能被迫进行防御性编码,例如,处理重复项目,即使您知道不会有重复项目。
合理的决策过程可能是:
- 如果保证 O(1) 索引访问,使用
List
. - 如果元素已排序且唯一,请使用
SortedSet
或NavigableSet
。 - 如果元素唯一性得到保证而顺序不保证,则使用
Set
。 - 否则,使用
Collection
。
由于您的示例使用 私有字段,因此隐藏实现类型并不重要。您(或维护此 class 的任何人)总是可以查看该字段的初始化程序以了解它是什么。
不过,根据它的使用方式,可能值得为该字段声明一个更具体的接口。将其声明为 List
表示允许重复并且排序很重要。将其声明为 Set
表示不允许重复且排序不重要。你甚至可以声明该字段有一个特定的实现 class 如果它有一些重要的东西。例如,将其声明为 LinkedHashSet
表示不允许重复,但排序 是 重要。
如果类型出现在 class 的 public API 中,则选择是否使用接口以及使用什么接口变得更加重要此 class 的兼容性限制是什么。例如,假设有一个方法
public ??? getRegisteredListeners() {
return ...
}
现在 return 类型的选择会影响其他 class 类型。如果你能改变所有的来电者,也许这没什么大不了的,你只需要编辑其他文件。但是假设调用者是一个您无法控制的应用程序。现在接口的选择很关键,因为您不能在不破坏应用程序的情况下更改它。这里的规则通常是选择最抽象的接口,支持您希望调用者执行的操作。
大多数 Java SE APIs return Collection
。这为底层实现提供了相当程度的抽象,但它也为调用者提供了一组合理的操作。调用者可以迭代、获取大小、进行包含检查或将所有元素复制到另一个集合。
一些代码库使用 Iterable
作为 return 的最抽象接口。它所做的只是允许调用者迭代。有时这就是所有必要的,但与 Collection
.
另一个选择是return一个Stream
。如果您认为调用者可能想要使用流的操作(例如过滤器、映射、查找等)而不是迭代或使用集合操作,这将很有帮助。
请注意,如果您选择 return Collection
或 Iterable
,您需要确保您 return 一个不可修改的视图或制作一个防御性副本。否则,呼叫者可能会修改您的 class 的内部数据,这可能会导致错误。 (是的,即使 Iterable
也可以允许修改!考虑得到一个 Iterator
然后调用 remove()
方法。)如果你 return 一个 Stream
,你不不必担心这一点,因为您不能使用 Stream
修改基础源。
请注意,我将您关于字段声明的问题变成了关于方法 return 类型声明的问题。 "program to the interface" 这种想法在 Java 中非常普遍。在我看来,局部变量并不重要(这就是为什么通常可以使用 var
),而对于私有字段则无关紧要,因为那些(几乎)根据定义只影响 class 在其中声明它们。但是,"program to the interface" 原则对于 API 签名来说 非常 重要,因此在这些情况下您确实需要考虑接口类型。私有字段,没那么多。
(最后一点:有一种情况你需要关注私有字段的类型,那就是当你使用一个直接操作私有字段的反射框架时。在这种情况下,你需要将这些字段视为 public——就像方法 return 类型一样——即使它们没有声明 public
。)