JLS如何指定不能在方法中正式使用通配符?

时间:2012-07-16 08:12:09

标签: java generics wildcard jls

我一直想知道Java泛型的一些奇怪方面和通配符的使用。比方说,我有以下API:

public interface X<E> {
    E get();
    E set(E e);
}

然后,假设我们声明了以下方法:

public class Foo {
    public void foo(X<?> x) {
        // This does not compile:
        x.set(x.get());
    }

    public <T> void bar(X<T> x) {
        // This compiles:
        x.set(x.get());
    }
}

根据我的“直观”理解,X<?>实际上与X<T>相同,只是未知的<T> 正式未知客户端代码。但在foo()内部,我猜测编译器可以推断出(伪JLS代码)<T0> := <?>[0], <T1> := <?>[1], etc...。这是大多数程序员明确而直观地做的事情。他们委托私人助手方法,导致许多无用的锅炉板代码:

public class Foo {
    public void foo(X<?> x) {
        foo0(x);
    }

    private <T> void foo0(X<T> x) {
        x.set(x.get());
    }
}

另一个例子:

public class Foo {
    public void foo() {
        // Assuming I could instanciate X
        X<?> x = new X<Object>();
        // Here, I cannot simply add a generic type to foo():
        // I have no choice but to introduce a useless foo0() helper method
        x.set(x.get());
    }
}

换句话说,编译器知道x.set()中的通配符与x.get()中的通配符正式相同。为什么不能使用这些信息? JLS中是否有正式的方面,这解释了这个缺乏编译器的“功能”?

3 个答案:

答案 0 :(得分:5)

你想知道为什么不支持?一个同样有效的问题是,为什么要支持它?

通配符的工作方式,给定声明X<? extends Y> x,表达式x.get()的类型不是? extends Y或编译器推断的某种类型,而是Y。现在,Y无法分配给? extends Y,因此您无法将x.get()传递给有效签名为x.set的{​​{1}}。您唯一可以传递给它的是void set(? extends Y e),它可以分配给每个引用类型。

我非常密切地关注JDK开发人员的邮件列表,而且一般的精神是JLS指定的每个功能都应该自己承担权重 - 成本/收益比在利益方面应该很重要。现在,跟踪一个值的来源是很有可能的,但它只能解决一个特殊情况,它有几种其他方法可以解决它。考虑这些类:

null

这显然无法编译,但是根据您提议的增强功能,它确实可以。但是,我们可以在不更改JLS的情况下编译四种方法中的三种:

  1. 对于class ClientOfX { X<?> member; void a(X<?> param) { param.set(param.get()); } void b() { X<?> local = new XImpl<Object>(); local.set(local.get()); } void c() { member.set(member.get()); } void d() { ClassWeCantChange.x.set(ClassWeCantChange.x.get()); } } class ClassWeCantChange { public static X<?> x; } ,当我们收到通用对象作为参数时,我们可以将该方法设为通用,并接收a而不是X<T>
  2. 对于X<?>b,当使用本地声明的引用时,我们不需要使用通配符声明引用(Microsoft的C#编译器团队的Eric Lippert调用这种情况“如果你这样做会有伤害,那么不要这样做!“)。
  3. 对于c,当使用我们无法控制其声明的引用时,除了编写辅助方法之外别无选择。
  4. 因此,更智能的类型系统实际上只对我们无法控制变量声明的情况有所帮助。在这种情况下,做你想要做的事情的情况有点粗略 - 通配类型的通配行为通常意味着该对象要么仅用作生产者(d)或者对象的消费者(? extends Something),而不是两者。

    TL; DR版本是它带来的好处(并且要求通配符不是它们现在的东西!),同时增加了JLS的复杂性。如果有人提出一个非常有说服力的案例来添加它,那么规范可能会延长 - 但一旦它存在,就会永远存在,并且它可能会导致无法预料的问题,所以在你真正需要它之前就把它留下来。 p>

    编辑:评论中包含更多关于通配符是什么的讨论。

答案 1 :(得分:2)

我刚看到Collections.swap()的这种特殊实现:

public static void swap(List<?> list, int i, int j) {
    final List l = list;
    l.set(i, l.set(j, l.get(i)));
}

JDK人员采用原始类型,以便在本地处理此问题。对我来说,这是一个强有力的声明,表明这可能应该得到编译器的支持,但由于某种原因(例如,没有时间正式指定这一点)只是没有完成

答案 2 :(得分:1)

没有参与编写JLS,我最好的猜测是简单性。想象一下愚蠢但(语法上)有效的表达式,例如(x = new X<Integer>()).set(x.get());。跟踪捕获的通配符可能会变得棘手。

我认为捕捉通配符不是样板,而是做了正确的事情。这表明您的API没有客户端代码不需要的类型参数。

编辑:当前行为在JLS §4.5.2中指定。 x会针对每个成员访问进行捕获转换 - 一次为x.set(...),一次为x.get()。捕获是不同的,因此不知道它们代表相同的类型。