首页 > 解决方案 > 有没有更好的方法在 Java 中使用通配符?

问题描述

考虑以下 java 构造函数头...

public FirstPair(Map<Enum, Set<Enum>> f, List<Set<Enum>> fr) 

现在,我想通过为泛型添加一些空间来使这个“未来安全”。例如,如果我想传入 a Map<Enum, HashSet<Enum>>for f 怎么办?

...所以,我将构造函数更改为...

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

原因是 HashSets 列表不符合 Sets 列表。我想通过更改函数头来处理这个错误,而不是把负担留给函数调用者......

我能想到解决错误的唯一方法是将函数头更改为此...

static void foo(List<? extends Set<Integer>> listOfSetsOfInts)
// of even more secure...
static void foo(List<? extends Set<? extends Integer>> listOfSetsOfInts)

我的问题是我在这里使用 java 通配符是否正确?我是否过度使用它们?我缺少一个明确的解决方法吗?

标签: javagenericswildcard

解决方案


问题是构造函数的使用,它(自然)具有非常特定的类型,然后与您尝试存储它的变量的类型不兼容。为了让这个解释更深入地挖掘 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) 也可以去掉这些通配符。这是一些可以编译的替代代码,但您会收到有关未经检查的类型转换的警告。假设您将地图和列表视为只读,那么您可以替换下面的代码。(注意:里氏替换原则对此有话要说;参见下面的(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;
    }

list(2) is的具体类型ArrayList<HashSet<B>>,但我们希望像使用它一样使用它,这在构造函数的通配符List<Set<A>>中是允许的。FooMap请注意,我们向构造函数传递了一个显式类型参数来表示 this 是一个FooMap<A>.

(3) 的具体类型diffSetTypeArrayList<HashSet<Integer>>,与 的参数类型兼容foo()

(4) 您需要更仔细地考虑 的第一个类型参数HashMap,因为我们要满足 Liskov 替换原则,这意味着满足“生产者扩展,消费者超级”(PECS)规则。的第一个参数Map是“消费者”类型,所以我们真的应该这样写代码:

  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) HashSet<B>fromlist不匹配? extends Set<? extends C>

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

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


推荐阅读