4

我一直想知道 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());
    }
}

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

4

3 回答 3

5

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

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

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

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

这显然不能编译,但是通过您提出的增强,它可以编译。但是,我们可以在不更改 JLS 的情况下编译四种方法中的三种:

  1. 因为a,当我们接收泛型对象作为参数时,我们可以使方法泛型并接收一个X<T>代替X<?>
  2. 对于band c,当使用本地声明的引用时,我们不需要使用通配符来声明引用(微软 C# 编译器团队的Eric Lippert称这种情况为“如果这样做会造成伤害,那就不要这样做! ” )。
  3. 对于d,当使用我们无法控制其声明的引用时,我们别无选择,只能编写一个辅助方法。

因此,更智能的类型系统只会在我们无法控制变量声明的情况下有所帮助。在那种情况下,做你想做的事情的情况有点粗略——通配泛型类型的行为通常意味着对象要么仅作为对象的生产者(? extends Something)或消费者(? super Something)工作, 不是都。

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

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

于 2012-07-16T09:04:28.157 回答
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 家伙求助于原始类型,以便在本地处理这个问题。对我来说,这是一个强有力的声明,表明编译器可能应该支持这一点,但由于某种原因(例如,没有时间正式指定这一点)还没有完成

于 2012-08-16T07:32:51.063 回答
1

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

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

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

于 2012-07-16T09:00:35.460 回答