【问题标题】:Should the following code compile under Java 1.8以下代码是否应该在 Java 1.8 下编译
【发布时间】:2015-06-24 09:02:15
【问题描述】:

给定以下课程:

public class FooTest {

    public static class Base {
    }

    public static class Derived extends Base {
    }

    public interface Service<T extends Base> {
        void service(T value);
    }

    public abstract class AbstractService<T extends Derived> implements  Service<T> {
        public void service(T value) {
        }
    }

    private AbstractService service;

    public void bar(Base base) {
        if(base instanceof Derived) {
            service.service(base); // compile error at this line
        }
    }
}

使用以下pom.xml 构建类时:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.mgm-tp</groupId>
    <artifactId>java-compiler-test</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.3</version>
                    <configuration>
                        <source>1.8</source>
                        <target>1.8</target>
                        <compilerId>eclipse</compilerId>
                    </configuration>
                    <dependencies>
                        <dependency>
                            <groupId>org.codehaus.plexus</groupId>
                            <artifactId>plexus-compiler-eclipse</artifactId>
                            <version>2.5</version>
                        </dependency>
                    </dependencies>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>
</project>

在 maven 3.4 中会产生以下编译错误:

[ERROR] 无法在项目 java-compiler-test 上执行目标 org.apache.maven.plugins:maven-compiler-plugin:3.3:compile (default-compile):编译失败 [错误] C:\Users\abrieg\workingcopy\java-compiler-test\src\main\java\FooTest.java:[25] FooTest.Service 类型中的方法 service(FooTest.Base) 不适用于参数(FooTest.Base)

将eclipse编译器的源和目标级别设置为1.7或使用javac作为编译器时,不会报告编译错误。

问题是 JLS 1.8 是否对类型推断更具体,以至于 eclipse 编译器对 java 1.8 的假设确实不允许使用此代码,或者这是否是 eclipse 编译器中的回归。

根据编译器错误的文本,我倾向于说它是一种回归,但我不确定。

我已经确定了以下两个已经报告给 jdt 的错误,但我认为它们并不完全适用:
https://bugs.eclipse.org/bugs/show_bug.cgi?id=432603 https://bugs.eclipse.org/bugs/show_bug.cgi?id=430987

如果这是回归,是否已经向 jdt 报告过?

【问题讨论】:

  • 用 javac 编译得很好 - 问题可能是 eclipse 特有的......
  • 但是为什么呢?服务需要一个 T 扩展 Derived,但得到一个 Base 对象(尽管它被检查为实际上是 Derived 的实例,并且可能被强制转换。
  • @baraber 在语句 service.service(base); 中,编译器可以推断出 base 的类型为 Derived,因为前面的 if 语句带有 base instanceof Derived 表达式。因此,将问题改写为“编译器可以/应该/必须使用它从 instanceof 表达式中获得的信息吗?”
  • @SpaceTrucker 我不知道这里发生了什么(它为我编译),但方法service 重载。如果你不相信我,你可以这样做for (Method method : FooTest.AbstractService.class.getMethods()) if (method.getName().equals("service")) System.out.println(method);。由于AbstractService 实现了Service,并且Service 有一个方法service 接受Base,因此在AbstractService 接受Base 中创建了对此的综合覆盖。这是接受 Derivedservice 方法的重载。
  • 它为我编译,但 IntelliJ 显示为红色。我在 Java 1.8.0_45 上。

标签: java eclipse java-8 language-lawyer jls


【解决方案1】:

据我了解,这段代码应该可以编译,但当然不能没有未经检查的警告。

您声明了一个 原始类型 AbstractService 的变量 service,它是 原始类型 Service 的子类型,它有一个方法 void service(Base)这是void service(T)的擦除。

所以调用service.service(base) 可能会调用Service 中声明的方法void service(Base),当然,带有unchecked 警告,因为该方法是泛型的并且没有验证类型参数@987654329 @发生了。

这可能是违反直觉的,因为 AbstractService 类型会使用擦除为 void service(Derived) 的方法覆盖该方法,但此方法只能在 generic 上下文中覆盖另一个方法,而不是在原始类型继承关系。

或者,换句话说,一个类型不能覆盖一个方法,它在参数类型方面比被覆盖的超类型方法更严格。

这也适用于泛型类型继承,但结果不同。如果您的变量具有AbstractService&lt;X&gt; 类型,那么由于类型参数的约束,X 必须可分配给Derived。这种类型AbstractService&lt;X&gt;Service&lt;X&gt; 的子类型,它有一个void service(X) 方法(如T := X),它被AbstractService&lt;X&gt; 覆盖(实现)了一个方法void service(X),它接受相同的参数类型。


由于您的网站似乎有些混乱,我想强调这与您的if(… instanceof Derived) 声明无关。如上所述,这种行为是由于 raw type 使用造成的,这意味着您使用的 AbstractService 没有实际的类型参数,并且基本上关闭了泛型类型检查。如果你写了,这甚至可以工作

public void bar(Base base) {
    service.service(base); // UNCHECKED invocation
}

如果您将变量的声明更改为

private AbstractService<Derived> service;

它不再是原始类型,类型检查将发生,service.service(base) 将产生编译器错误,无论你是否用if(base instanceof Derived) { … } 括起来。 p>

原始类型的存在只是为了与预泛型代码兼容,您应该避免使用它们,也不要忽略由原始类型使用引起的警告。

【讨论】:

  • 如果您可以为您的陈述引用 jls 的适当部分,那就太好了。然而,编译器是否可以/应该/必须使用它从包含 instanceof 表达式的 if 语句中获得的信息仍然存在问题。
  • 我浏览了规范,但找不到一个简洁的段落来链接(链接到 20 个地方并说“得出结论”有点毫无意义)。关于if的说法,答案很简单:没有半点影响。只有接收者的编译时类型(这里是变量service)很重要。顺便提一句。如果删除 if 语句,很容易检查所有编译器是否显示相同的行为。
  • 但显然javac 确实利用了 if 语句中的信息,因为它编译代码时不会出错。所以javac 自 1.5 以来可能是错误的,而 eclipse jdt 编译器是正确的,因为它支持 java 1.8?
  • @SpaceTrucker:请仔细阅读我回答的第一部分并尝试理解。并不是任何编译器都知道base 包含Derived 的实例。相反,将调用方法Service.service(Base),因为由于使用了原始类型,导致不可能删除的信息。
  • 原始类型的使用是AbstractService.service (&lt;T extends Derived&gt; base),因此编译器至少需要转换为Derived。
【解决方案2】:

这当然是关于类型安全的

正如其他人所说,您需要将 base 转换为 Derived 以使其满足服务(T 值)的要求,因为编译器知道 AbstractService.service 的参数必须扩展 Derived ,因此如果它不是 Derived 它不适合.

不幸的是,这消除了编译器错误,但并不能解决问题。这就是您收到类型安全警告的原因。

由于AbstractService 的定义,service 实际上是AbstractService&lt;? extends Derived&gt;,如果你修正了这个遗漏,你只会得到错误。

这是因为 AbstractService&lt;? extends Derived&gt; 没有扩展 AbstractService&lt;Derived&gt; 并且将 base 转换为 Derived 解决了 AbstractService&lt;Derived&gt; 的问题。

查看下面的类以查看此示例。 ConcreteService 不扩展 AbstractService&lt;Derived&gt; 它扩展 AbstractService&lt;RealDerived&gt;。您不能将任何Derived 传递给ConcreteService.service(RealDerived base),例如,您不能传递FalseDerived 对象,因为它不满足参数类型。

事实是,必须在某个地方实际定义 T 是什么,这样我们才能解决类型安全问题。

这样做通常需要一些技巧,例如将参数的实际类型与服务的实际类型联系在一起的类,以便可以以类型安全的方式进行转换。

如下

public class FooTest {

    public static class Base {
    }

    public static class Derived extends Base {
    }

    public interface Service<T extends Base> {
        void service(T value);

    }    

    public abstract static class AbstractService<T extends Derived> implements  Service<T> {

        public void service(T value) {
        }

    }

    // The following class defines an argument B that and ties together the type of the service with the type of the referenced stored class which enables casting using the class object 
    public abstract static class Barrer<B extends Derived>{

        private AbstractService<B> service;

        private Class<B> handledClass;


        protected Barrer(Class<B> handledClass, AbstractService<B> service){
            this.handledClass = handledClass;
            this.service = service;
        }

        public  void bar(Base base) {
            if(handledClass.isAssignableFrom(base.getClass())) {
                service.service(handledClass.cast(base)); // compile error at this line
            }
        }

    }

// the following classes provide concrete implementations and the concrete class to perform the casting.   

    public static class RealDerived extends Derived{}

    public static class FalseDerived extends Derived{}

    public static class ConcreteService extends AbstractService<RealDerived>{
    }


    public static class ConcreteBarrer extends Barrer<RealDerived> {
        protected ConcreteBarrer() {
            super(RealDerived.class,new ConcreteService());
        }

    }
}

【讨论】:

    猜你喜欢
    • 2017-04-29
    • 1970-01-01
    • 2019-10-15
    • 2021-06-14
    • 1970-01-01
    • 1970-01-01
    • 2016-11-27
    • 1970-01-01
    • 2016-05-11
    相关资源
    最近更新 更多