在 Java 中使用通配符是否有更好的方法?
Is there a better way to work with Wildcards in Java?
考虑以下 java 构造函数 header...
public FirstPair(Map<Enum, Set<Enum>> f, List<Set<Enum>> fr)
现在,我想通过为泛型添加一些空间来实现 "future safe"。例如,如果我想为 f.
传入一个 Map<Enum, HashSet<Enum>>
怎么办?
...所以,我将构造函数更改为...
public FirstPair(Map<? extends Enum, ? extends Set<? extends Enum>> f,
List<? extends Set<? extends Enum>> fr)
这看起来正确吗?
有没有更简单的方法来解释不同的输入类型?
或者这是最好的方法?
**** 更多细节 ****
考虑以下函数。
static void foo(List<Set<Integer>> listOfSetsOfInts)
下面的代码会报错...
List<HashSet<Integer>> diffSetType = new ArrayList<HashSet<Integer>>();
foo(diffSetType);
原因是哈希集列表不符合集合列表。我想通过更改函数 header 来处理此错误,而不是将负担留给函数调用者...
我能想到的解决错误的唯一方法是将函数header更改为...
static void foo(List<? extends Set<Integer>> listOfSetsOfInts)
// of even more secure...
static void foo(List<? extends Set<? extends Integer>> listOfSetsOfInts)
我的问题是我在这里使用 java 通配符是否正确?我过度使用它们了吗?是否有我遗漏的明确修复方法?
对于您的初始示例,您不需要走得太远来支持您打算支持的内容。
您有:
public FirstPair(Map<Enum, Set<Enum>> f, List<Set<Enum>> fr)
但是 Map
和 Set
是接口,所以如果你打算将它与 HashSet
或 HashMap
一起使用,你可以,因为那些类 分别实现了 Set
和 Map
接口,因此遵守并尊重您为构造函数定义的签名。
如果您考虑要支持的实体的通用接口,它应该会简化您的生活,以及您正在定义的构造函数 and/or methods/functions 的签名。如果您想到它,通常如果您希望允许其他类型互换使用,那是因为您可以以类似的方式对待它们,对于您提供的功能的上下文,因此接口往往会解析立即为您轻松实现,如果没有开箱即用的界面来满足您的需求,请为此目的创建一个。在较长的 运行 中,它可能更容易记录、阅读和维护。希望对您有所帮助。
问题在于构造函数的使用,它(自然地)具有非常特定的类型,然后与您尝试存储它的变量类型不兼容。为了让这个解释更深入地挖掘 Java 类型通配符的乐趣和兴奋,我做了一个类型参数而不是你的 Enum
。 (所有这些都使用 Java 的 var
,以保持代码更简洁。如果您使用 Java11,此代码将编译正常,除了类型错误和我在下面讨论的其他警告。此外,您的原始代码在 Liskov 替换原则方面存在问题。我暂时不考虑它,并在下面 (4) 中修复它。
public interface Foo {
class A { }
class B extends A {}
class C extends B {}
class FooMap<T> {
Map<? extends T, ? extends Set<? extends T>> f; // (1)
List<? extends Set<? extends T>> fr;
public FooMap(
Map<? extends T, ? extends Set<? extends T>> f,
List<? extends Set<? extends T>> fr) {
this.f = f;
this.fr = fr;
}
static void exercise1() {
var hm = new HashMap<B, HashSet<B>>();
var list = new ArrayList<HashSet<B>>(); // (2)
var fm = new FooMap<A>(hm, list);
}
static void foo(
List<? extends Set<? extends Integer>> listOfSetsOfInts) { }
static void exercise2() {
var diffSetType = new ArrayList<HashSet<Integer>>(); // (3)
foo(diffSetType);
}
}
}
评论:
(1) 也可以去掉这些通配符。下面是一些 将 编译的替代代码,但您会收到有关未经检查的类型转换的警告。假设您将地图和列表视为只读,那么您可以替换下面的代码。 (注意:Liskov 替换原则对此有话要说;请参阅下面的 (4)。) 一旦允许突变,事情就会变得更加复杂。
Map<T, Set<T>> f;
List<Set<T>> fr;
public FooMap(
Map<? extends T, ? extends Set<? extends T>> f,
List<? extends Set<? extends T>> fr) {
this.f = (Map<T, Set<T>>) f;
this.fr = (List<Set<T>>) fr;
}
(2) list
的具体类型是ArrayList<HashSet<B>>
,但我们想把它当作List<Set<A>>
来使用,这在FooMap
构造函数中是允许的通配符。请注意,我们向构造函数传递了一个显式类型参数,表示这是一个 FooMap<A>
.
(3) diffSetType
的具体类型为ArrayList<HashSet<Integer>>
,与foo()
的参数类型兼容。
(4) HashMap
的第一个类型参数你需要仔细考虑一下,因为我们要满足里氏代换原则,也就是满足"producer extends, consumer super" ( PECS)规则。 Map
的第一个参数是"consumer"类型,所以我们真的应该这样写代码:
class FooMap<T> {
Map<T, Set<T>> f;
List<Set<T>> fr;
public FooMap(
Map<? super T, ? extends Set<? extends T>> f,
List<? extends Set<? extends T>> fr) {
this.f = (Map<T, Set<T>>) f;
this.fr = (List<Set<T>>) fr;
}
static void exercise1() {
var hmA = new HashMap<A, HashSet<B>>();
var hmB = new HashMap<B, HashSet<B>>();
var hmC = new HashMap<C, HashSet<B>>();
var list = new ArrayList<HashSet<B>>();
var fm1 = new FooMap<A>(hmA, list); // okay
var fm2 = new FooMap<B>(hmA, list); // okay
var fm3 = new FooMap<C>(hmA, list); // type error (5)
var fm4 = new FooMap<B>(hmB, list); // okay
var fm5 = new FooMap<B>(hmC, list); // type error (6)
var fm6 = new FooMap<C>(hmC, list); // type error (7)
}
(5) 来自 list
的 HashSet<B>
与 ? extends Set<? extends C>
不匹配
(6) hmC
中的 HashMap<C, HashSet<B>>
不匹配 Map<? super B, ? extends Set<? extends B>>
因为 C
不匹配 ? super B
.
(7) hmC
是 HashMap<C, HashSet<B>>
; HashSet<B>
不匹配 ? extends Set<? extends C>
考虑以下 java 构造函数 header...
public FirstPair(Map<Enum, Set<Enum>> f, List<Set<Enum>> fr)
现在,我想通过为泛型添加一些空间来实现 "future safe"。例如,如果我想为 f.
传入一个Map<Enum, HashSet<Enum>>
怎么办?
...所以,我将构造函数更改为...
public FirstPair(Map<? extends Enum, ? extends Set<? extends Enum>> f,
List<? extends Set<? extends Enum>> fr)
这看起来正确吗? 有没有更简单的方法来解释不同的输入类型? 或者这是最好的方法?
**** 更多细节 ****
考虑以下函数。
static void foo(List<Set<Integer>> listOfSetsOfInts)
下面的代码会报错...
List<HashSet<Integer>> diffSetType = new ArrayList<HashSet<Integer>>();
foo(diffSetType);
原因是哈希集列表不符合集合列表。我想通过更改函数 header 来处理此错误,而不是将负担留给函数调用者...
我能想到的解决错误的唯一方法是将函数header更改为...
static void foo(List<? extends Set<Integer>> listOfSetsOfInts)
// of even more secure...
static void foo(List<? extends Set<? extends Integer>> listOfSetsOfInts)
我的问题是我在这里使用 java 通配符是否正确?我过度使用它们了吗?是否有我遗漏的明确修复方法?
对于您的初始示例,您不需要走得太远来支持您打算支持的内容。
您有:
public FirstPair(Map<Enum, Set<Enum>> f, List<Set<Enum>> fr)
但是 Map
和 Set
是接口,所以如果你打算将它与 HashSet
或 HashMap
一起使用,你可以,因为那些类 分别实现了 Set
和 Map
接口,因此遵守并尊重您为构造函数定义的签名。
如果您考虑要支持的实体的通用接口,它应该会简化您的生活,以及您正在定义的构造函数 and/or methods/functions 的签名。如果您想到它,通常如果您希望允许其他类型互换使用,那是因为您可以以类似的方式对待它们,对于您提供的功能的上下文,因此接口往往会解析立即为您轻松实现,如果没有开箱即用的界面来满足您的需求,请为此目的创建一个。在较长的 运行 中,它可能更容易记录、阅读和维护。希望对您有所帮助。
问题在于构造函数的使用,它(自然地)具有非常特定的类型,然后与您尝试存储它的变量类型不兼容。为了让这个解释更深入地挖掘 Java 类型通配符的乐趣和兴奋,我做了一个类型参数而不是你的 Enum
。 (所有这些都使用 Java 的 var
,以保持代码更简洁。如果您使用 Java11,此代码将编译正常,除了类型错误和我在下面讨论的其他警告。此外,您的原始代码在 Liskov 替换原则方面存在问题。我暂时不考虑它,并在下面 (4) 中修复它。
public interface Foo {
class A { }
class B extends A {}
class C extends B {}
class FooMap<T> {
Map<? extends T, ? extends Set<? extends T>> f; // (1)
List<? extends Set<? extends T>> fr;
public FooMap(
Map<? extends T, ? extends Set<? extends T>> f,
List<? extends Set<? extends T>> fr) {
this.f = f;
this.fr = fr;
}
static void exercise1() {
var hm = new HashMap<B, HashSet<B>>();
var list = new ArrayList<HashSet<B>>(); // (2)
var fm = new FooMap<A>(hm, list);
}
static void foo(
List<? extends Set<? extends Integer>> listOfSetsOfInts) { }
static void exercise2() {
var diffSetType = new ArrayList<HashSet<Integer>>(); // (3)
foo(diffSetType);
}
}
}
评论:
(1) 也可以去掉这些通配符。下面是一些 将 编译的替代代码,但您会收到有关未经检查的类型转换的警告。假设您将地图和列表视为只读,那么您可以替换下面的代码。 (注意:Liskov 替换原则对此有话要说;请参阅下面的 (4)。) 一旦允许突变,事情就会变得更加复杂。
Map<T, Set<T>> f;
List<Set<T>> fr;
public FooMap(
Map<? extends T, ? extends Set<? extends T>> f,
List<? extends Set<? extends T>> fr) {
this.f = (Map<T, Set<T>>) f;
this.fr = (List<Set<T>>) fr;
}
(2) list
的具体类型是ArrayList<HashSet<B>>
,但我们想把它当作List<Set<A>>
来使用,这在FooMap
构造函数中是允许的通配符。请注意,我们向构造函数传递了一个显式类型参数,表示这是一个 FooMap<A>
.
(3) diffSetType
的具体类型为ArrayList<HashSet<Integer>>
,与foo()
的参数类型兼容。
(4) HashMap
的第一个类型参数你需要仔细考虑一下,因为我们要满足里氏代换原则,也就是满足"producer extends, consumer super" ( PECS)规则。 Map
的第一个参数是"consumer"类型,所以我们真的应该这样写代码:
class FooMap<T> {
Map<T, Set<T>> f;
List<Set<T>> fr;
public FooMap(
Map<? super T, ? extends Set<? extends T>> f,
List<? extends Set<? extends T>> fr) {
this.f = (Map<T, Set<T>>) f;
this.fr = (List<Set<T>>) fr;
}
static void exercise1() {
var hmA = new HashMap<A, HashSet<B>>();
var hmB = new HashMap<B, HashSet<B>>();
var hmC = new HashMap<C, HashSet<B>>();
var list = new ArrayList<HashSet<B>>();
var fm1 = new FooMap<A>(hmA, list); // okay
var fm2 = new FooMap<B>(hmA, list); // okay
var fm3 = new FooMap<C>(hmA, list); // type error (5)
var fm4 = new FooMap<B>(hmB, list); // okay
var fm5 = new FooMap<B>(hmC, list); // type error (6)
var fm6 = new FooMap<C>(hmC, list); // type error (7)
}
(5) 来自 list
的 HashSet<B>
与 ? extends Set<? extends C>
(6) hmC
中的 HashMap<C, HashSet<B>>
不匹配 Map<? super B, ? extends Set<? extends B>>
因为 C
不匹配 ? super B
.
(7) hmC
是 HashMap<C, HashSet<B>>
; HashSet<B>
不匹配 ? extends Set<? extends C>