在 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)

但是 MapSet 是接口,所以如果你打算将它与 HashSetHashMap 一起使用,你可以,因为那些类 分别实现了 SetMap 接口,因此遵守并尊重您为构造函数定义的签名。

如果您考虑要支持的实体的通用接口,它应该会简化您的生活,以及您正在定义的构造函数 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) 来自 listHashSet<B>? extends Set<? extends C>

不匹配

(6) hmC 中的 HashMap<C, HashSet<B>> 不匹配 Map<? super B, ? extends Set<? extends B>> 因为 C 不匹配 ? super B.

(7) hmCHashMap<C, HashSet<B>>HashSet<B> 不匹配 ? extends Set<? extends C>