8

假设我们有一个这样的类:

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具有各种泛型类型的 s 一起重置。换句话说,尝试传入 anIterable<Foo<?>>会导致编译错误。

这个例子演示了这个问题:

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>> where Tis a type-variable: T extends Object在方法中声明<T>resetFoos(Iterable<Foo<T>>)

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

4

5 回答 5

2

您可以使resetFoos方法本身通用:

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

这样编译器就知道Tfromfoo.getValue()T.foo.setValue()

这只是解决方法。我不知道为什么编译器不允许您在变量级别声明泛型,例如final <T> Foo<T> typedFoo = foo;;我只知道你不能在变量级别声明它。但是,在这里,方法级别就足够了。

于 2013-04-08T19:31:23.600 回答
2

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

类型系统将无法推断有关您的程序的所有内容。它会做很多事情,但总会有一些地方编译器/类型检查器不知道你人类可以证明的东西。所以,设计编译器和类型检查器的人必须决定在推理和聪明方面能走多远——而且无论他们停在哪里,总有人能够提出这样一个问题:“为什么没有”他们不会多走这一步吗?”

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

于 2013-04-08T19:37:49.033 回答
2

对 java 语言的每次更改都将非常困难。

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

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

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

  3. 推断变量类型 -var foo2 = foo;的类型foo2是从右侧推断的,它实际上首先进行捕获转换,因此foo2的类型将是Foo<x> for some x,具有新的类型变量 x (仍然无法表示)。或者var直接在foo-上使用for(var foo: foos)。推断变量类型是一种成熟/经过测试的技术,它的好处远比解决这个问题要广泛。所以我会投票给这个。如果我们能活那么久,它可能会出现在 java 9 中。

于 2013-04-08T21:04:19.520 回答
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());
}

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

于 2013-04-09T18:23:31.423 回答
1

问题是在有问题的陈述中:

foo.setValue(foo.getValue());

can的两次出现?(据编译器所知)绑定到不同的类型。编译器无法确定一个?(由返回的getValue()?setValue. 但在其他情况下,结果并不那么明确。该语言要求类型参数命名相同,如果它们应该是相同的。

rgettman 的回答中提供了一个很好的解决方法。

于 2013-04-08T19:38:15.390 回答