Java不应该在变量声明中允许泛型类型声明的任何原因?

时间:2013-04-08 19:29:17

标签: java generics language-design

假设我们有这样一个类:

public class xx {

    public interface Foo<T> {
        T getValue();
        void setValue(T value);
    }

    public void resetFoos(Iterable<Foo<?>> foos) {
        for (Foo<?> foo : foos)
            foo.setValue(foo.getValue());
    }
}

即使它看起来像“应该”,它也无法编译:

xx.java:10: setValue(capture#496 of ?) in xx.Foo<capture#496 of ?> cannot be applied to (java.lang.Object)
        foo.setValue(foo.getValue());

原因是foo没有绑定泛型类型,因此编译器不会“知道”foo.getValue()的输出与foo.setValue()的输入兼容。

所以要解决这个问题,你必须创建一个新方法,只是为了在for()循环中绑定泛型类型参数:

public class xx {

    public interface Foo<T> {
        T getValue();
        void setValue(T value);
    }

    public void resetFoos(Iterable<Foo<?>> foos) {
        for (Foo<?> foo : foos)
            this.resetFoo(foo);
    }

    // stupid extra method here just to bind <T>
    private <T> void resetFoo(Foo<T> foo) {
        foo.setValue(foo.getValue());
    }
}

这一直让我恼火。此外,似乎可以有一个简单的解决方案。

我的问题:为什么不应该扩展java语言以允许变量声明的泛型类型声明有什么“好”的理由?例如:

public class xx {

    public interface Foo<T> {
        T getValue();
        void setValue(T value);
    }

    public void resetFoos(Iterable<Foo<?>> foos) {
        for (Foo<?> foo : foos) {
            final <T> Foo<T> typedFoo = foo;
            foo.setValue(foo.getValue());
        }
    }
}

或者更简洁地说明for()循环:

public class xx {

    public interface Foo<T> {
        T getValue();
        void setValue(T value);
    }

    public void resetFoos(Iterable<Foo<?>> foos) {
        for (<T> Foo<?> foo : foos)
            foo.setValue(foo.getValue());
    }
}

我想知道某些编译器向导是否可以解释为什么这会太难,或者可以(而且应该)完成。

修改

回应这个建议的解决方案:

public <T> void resetFoos(Iterable<Foo<T>> foos) {
    for (Foo<T> foo : foos) {
        foo.setValue(foo.getValue());
    }
}

此方法签名不允许将具有各种泛型类型的Foo重置为一起。换句话说,尝试传入Iterable<Foo<?>>会导致编译错误。

This example演示了这个问题:

public static class FooImpl<T> implements Foo<T> {

    private T value;

    public FooImpl(T value) { this.value = value; }

    @Override public T getValue() { return value; }

    @Override public void setValue(T value) { this.value = value; }
}

public static <T> void resetFoos(Iterable<Foo<T>> foos) {
    for (Foo<T> foo : foos) {
        foo.setValue(foo.getValue());
    }
}

public static void main(String[] args) {

    final Foo<Object> objFoo = new FooImpl<>(new Object());
    final Foo<Integer> numFoo = new FooImpl<>(new Integer(42));
    final Foo<String> strFoo = new FooImpl<>("asdf");

    List<Foo<?>> foos = new ArrayList<>(3);
    foos.add(objFoo);
    foos.add(numFoo);
    foos.add(strFoo);

    resetFoos(foos); // compile error

    System.out.println("done");
}

编译错误如下:

  

方法resetFoos无法应用于给定类型;

     

必填:Iterable<Foo<T>>

     

找到:List<Foo<?>>

     

原因:没有类型变量T的实例存在,因此参数类型List<Foo<?>>符合形式参数类型Iterable<Foo<T>>     其中T是一个类型变量:       方法T extends Object

中声明的<T>resetFoos(Iterable<Foo<T>>)

(通过ideone.com使用sun-jdk-1.7.0_10)

5 个答案:

答案 0 :(得分:2)

您可以将resetFoos方法本身设为通用:

public <T> void resetFoos(Iterable<Foo<T>> foos) {
    for ( Foo<T> foo : foos)
        foo.setValue(foo.getValue());
}

这样编译器就知道来自T的{​​{1}}与foo.getValue()的{​​{1}}相同。

这只是解决方法。我不知道为什么编译器不允许你在诸如T之类的变量级别声明泛型;我只知道你不能在变量级别声明它。但是,这里的方法水平就足够了。

答案 1 :(得分:2)

可能有可能完成,可能还有一些边缘情况。但实际上,它没有完成的原因仅仅是JLS人员没有做到这一点。

类型系统无法推断关于您的程序的所有内容。它会做一堆,但总会有一些地方,编译器/类型检查器不会知道人类可以证明的东西。因此,设计编译器和类型检查器的人必须决定在推断和聪明方面走多远 - 无论他们停在哪里,总会能够提出类似这样的问题:“为什么没有”他们采取了这个额外的步骤吗?“

我的猜测 - 这只是一个猜测 - 是这个推论没有成功,因为(a)它很容易解决(如rgettman所示),(b)它使语言变得复杂, (b.1)它引入了棘手的边缘情况,其中一些甚至可能与该语言的其他特征不相容,(c)JLS的设计者并不觉得他们有时间研究它。

答案 2 :(得分:2)

对Java语言的每次更改都会非常困难。

您的示例并非真正需要类型变量。编译器已经通过通配符捕获创建了一个类型变量,只是程序员无法使用类型变量。可能有几种补救措施

  1. 让程序员访问类型变量。这不是一件容易的事。我们需要发明一些bizzare语法和语义。奇怪和复杂性可能无法证明其好处。

  2. 共享捕获转换。现在,每个表达式都分别通过捕获转换。规范无法识别foofoo,因此两个表达式应共享相同的捕获转换。在一些简单的情况下它是可行的,例如,有效的最终局部变量应该只被捕获转换一次,而不是每次访问。

  3. 推断变量类型 - var foo2 = foo;从右侧推断foo2的类型,实际上首先进行捕获转换,因此foo2的类型将为{ {1}},带有新的类型变量x(仍然是不可否认的)。或者直接在Foo<x> for some x - var上使用foo。推断变量类型是一种成熟/经过测试的技术,其优势远远超过解决此问题的范围。所以我会投票支持这个。如果我们可以活得那么久,它可能会出现在Java 9中。

答案 3 :(得分:2)

基本上,类型变量目前在Java中只能有两个范围:1)类范围,或2)方法范围。你问,为什么不允许另一个范围 - 本地代码块的范围(在这种情况下,是for循环的内部。

是的,在某些情况下,这会有所帮助。将它添加到语言中并不太难。然而,这些是非常罕见的情况,并且更有可能使人混淆而不是帮助。此外,正如您已经发现的那样,存在一个相对简单有效的解决方法 - 将本地块作用域移动到私有辅助函数,然后可以使用方法作用域类型变量:

public void resetFoos(Iterable<Foo<?>> foos) {
    for (Foo<?> foo : foos) {
        resetFoo(foo);
    }
}

private <T> void resetFoo(Foo<T> foo) {
    foo.setValue(foo.getValue());
}

是的,这可能会通过额外的方法调用降低代码效率,但这只是一个小问题。

答案 4 :(得分:1)

问题在于有问题的陈述:

foo.setValue(foo.getValue());

?的两次出现(就编译器所知)可以绑定到不同的类型。编译器无法确定?(由getValue()返回)与?中预期的setValue不同,这似乎很愚蠢}。但在其他情况下,结果并不那么明确。如果类型参数应该相同,则该语言要求类型参数的名称相同。

rgettman's answer中提供了一个很好的解决方法。