为什么这段代码在Java7中可以编译而在Java8中却不能?

9

泛型很棘手。 而且在不同的Java版本中,它们似乎被处理得有所不同。

这段代码能够成功编译Java 7,但在Java 8中无法编译。

import java.util.EnumSet;

public class Main {
  public static void main(String[] args) {
    Enum foo = null;
    tryCompile(EnumSet.of(foo));
  }

  static <C extends Enum<C> & Another> void tryCompile(Iterable<C> i) {}

  static interface Another {}
}

这里是Java 8的一个错误消息。我使用http://www.compilejava.net/编译时出现了这个错误。

/tmp/java_A7GNRg/Main.java:6: error: method tryCompile in class Main cannot be applied to given types;
    tryCompile(EnumSet.of(foo));
    ^
  required: Iterable<C>
  found: EnumSet
  reason: inferred type does not conform to upper bound(s)
    inferred: Enum
    upper bound(s): Enum<Enum>,Another
  where C is a type-variable:
    C extends Enum<C>,Another declared in method <C>tryCompile(Iterable<C>)
/tmp/java_A7GNRg/Main.java:6: warning: [unchecked] unchecked method invocation: method of in class EnumSet is applied to given types
    tryCompile(EnumSet.of(foo));
                         ^
  required: E
  found: Enum
  where E is a type-variable:
    E extends Enum<E> declared in method <E>of(E)
1 error
1 warning

这个问题涉及到Java编译器的版本差异。

10
Java 8 抱怨什么? - MadProgrammer
1
实际上,Java 8中的编译错误应该可以解释为什么它无法编译。而且很可能就是这样。 - Stephen C
你期望这段代码做什么?此外,你的“Enum”是一个原始类型。 - Elliott Frisch
没有错误信息,很难猜测这里发生了什么,但我猜编译器能够(正确地)推断出EnumSet.of(foo)的类型为EnumSet<Enum>,这与边界C extends Enum<C> & Another不兼容。 - Daniel Pryden
完成,但它如何帮助呢? - Timothy Basanov
4个回答

13
Java 7和Java 8之间的主要区别在于目标类型推断。在Java 7中,只考虑方法调用的参数来确定类型参数,而Java 8将使用表达式的目标类型,即嵌套方法调用的参数类型,在初始化或赋值的变量类型,或者在return语句的方法返回类型中。
例如,当编写List<Number> list=Arrays.asList(1, 2, 3, 4);时,Java 7将通过查看方法的参数来推断右侧的类型为List<Integer>并生成错误,而Java 8将使用目标类型List<Number>来推断方法参数必须是Number的实例,这是正确的。因此,在Java 8中是合法的。
如果您对正式细节感兴趣,可以学习“Java语言规范,第18章.类型推断”,特别是§18.5.2.调用类型推断,但那不容易阅读...
那么当您说Enum foo = null; tryCompile(EnumSet.of(foo));时会发生什么?
在Java 7中,表达式EnumSet.of(foo)的类型将通过查看参数foo的类型(即原始类型Enum)进行推断,因此将执行未经检查的操作,结果类型为原始类型EnumSet。该类型实现了原始类型Iterable,因此可以传递给tryCompile形成另一个未经检查的操作。
在Java 8中,EnumSet.of(foo)的目标类型是tryCompile的第一个参数的类型,即Iterable<C extends Enum<C> & Another>,因此不必深入细节,在Java 7中,EnumSet.of将被视为原始类型调用,因为它具有原始类型参数,在Java 8中,它将被视为泛型调用,因为它具有泛型目标类型。通过将其视为泛型调用,编译器将得出找到的类型(Enum)与所需类型C extends Enum<C> & Another不兼容的结论。虽然您可以使用未经检查的警告将原始类型Enum分配给C extends Enum<C>,但它将被认为与Another不兼容(没有类型转换)。
您确实可以插入这样的转换:
Enum foo = null;
tryCompile(EnumSet.of((Enum&Another)foo));

当然,这可以编译,但由于将Enum赋值给C extends Enum<C>,所以会出现未经检查的警告。

您也可以解除目标类型关系,从而执行与Java 7中相同的步骤:

Enum foo = null;
EnumSet set = EnumSet.of(foo);
tryCompile(set);

在这里,原始类型在整个三行中被使用,因此可以通过不带检查的警告编译,并且与Java 7中的implements Another约束一样无知。

这是一个很棒的解释。你知道我可以阅读哪些资料来让我能够脱口而出类似的东西吗?不是JLS,更容易理解的资料。 - Timothy Basanov
1
这里有一个关于目标类型的简短介绍:链接链接。如果您想了解更多 Java 8 的新特性,可以在这里查看:链接。你可能会在互联网上找到许多其他类似的网站,用不同的词汇和更多的示例来描述,但是我不知道是否有一个不太正式的网站能深入讲解而又不过于正式。 - Holger

2
Java 8中的类型推断引擎得到了改进,我认为现在能够确定`C`类型不继承`Another`类型。在Java 7中,类型推断系统无法或者没有关注到`Another`类型缺失,并在编译时给程序员以疑问(此时它会默认假定是正确的)。如果在Java 7中运行时调用`Another`接口上方法,则仍需在运行时支付代价。例如,以下代码:
import java.util.EnumSet;

public class Main {

  static enum Foo {
    BAR
  }

  public static void main(String[] args) {
    Enum foo = Foo.BAR;
    tryCompile(EnumSet.of(foo));
  }

  static <C extends Enum<C> & Another> void tryCompile(Iterable<C> i) {
    i.iterator().next().doSomething();
  }

  static interface Another {
    void doSomething();
  }
}

运行时会产生以下错误:

Exception in thread "main" java.lang.ClassCastException: Main$Foo cannot be cast to Main$Another
    at Main.tryCompile(Main.java:16)
    at Main.main(Main.java:12)

尽管Java 7编译器会编译代码,但仍会发出有关原始类型和未经检查的调用的警告,这应该让您意识到某些问题存在。


这是一个非常简单的示例,不使用枚举,但基于Enum定义,展示了相同的问题。在Java 7中编译时会带有警告,但在Java 8中则不会:

import java.util.Collections;
import java.util.List;

public class Main {

  static class Foo<T extends Foo<T>> {
  }

  static class FooA extends Foo<FooA> {
  }

  public static <T extends Foo<T>> List<T> fooList(T e) {
    return Collections.singletonList(e);
  }


  public static void main(String[] args) {
    Foo foo = new FooA();
    tryCompile(fooList(foo));
  }

  static <C extends Enum<C> & Another> void tryCompile(Iterable<C> i) {
    i.iterator().next().doSomething();
  }

  static interface Another {
    void doSomething();
  }
}

因此,这不是一个特定于Enum的问题,但可能是由于涉及递归类型所导致的。

不,运行时异常是可以预料的。我同意。但是如果你尝试用你的Foo替换Enum,编译会报错,说另外一些问题。所以这是一个特定于Enum的问题,从我所看到的来看。 - Timothy Basanov
1
是的,但编译器可以推断出 Foo 没有实现 Another 接口。我认为这不是枚举特定的问题,而是可能特定于具有类型参数的类。让我确认一下。 - clstrfsck

0

对我来说,这似乎是一个正确的错误:

reason: inferred type does not conform to upper bound(s)
  inferred: Enum
  upper bound(s): Enum<Enum>,Another

EnumSet.of(foo) 的类型为 EnumSet<Enum>,与 C extends Enum<C> & Another 不兼容,原因与 Set<Enum>Set<? extends Enum> 不兼容相同,因为 Java 泛型是不变的。


那在Java 7中它就无法编译。但现在可以了。 - Timothy Basanov
Java 中的枚举类型具有类型 Enum<E extends Enum<E>>,因此据我所知,它与 C extends Enum<C> 匹配。 - Timothy Basanov
1
我目前最好的猜测是这是一个rawtypes问题。如果你将foo明确地类型化为Enum<Enum>,那么它能工作吗? - Daniel Pryden
使用http://www.compilejava.net试一下,它很迷人!Enum <Enum>不是有效的类型,Enum <?>也无法编译。 - Timothy Basanov

-1

这段代码在我的Eclipse Standard/SDK版本:Luna Release (4.4.0) Build id: 20140612-0600中编译通过,同时安装了Eclipse JDT(Java Development Tools)补丁和Java 8支持(适用于Kepler SR2)1.0.0.v20140317-1956 org.eclipse.jdt.java8patch.feature.group。

我收到了一些警告(foo上的原始类型和tryCompile上的未检查调用)。


如果你收到警告,它将会编译为Java 7。你需要完全编译为Java 8。 - NightSkyCode
2
哎呀,看起来我撒了谎。你是正确的,我正在编译到1.7 :( - Ray Tayek

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接