首页 > 解决方案 > 为什么 javac 在方法的返回类型上使用有界类型泛型编译此代码?它是无效的类型推断吗?

问题描述

我们有类似于以下的代码:

import java.io.Closeable;

public class Test {
    @FunctionalInterface
    public interface Action1<E extends Exception> {
        void run() throws E;
    }

    public interface Action2 {
        void run();
    }

    static class MyClass implements Action1<Exception>, Action2 {
        @Override
        public void run() {
        }
    }

    public static <C extends Action1<Exception> & Action2> C emptyAction() {
        return (C) new MyClass();
    }

    public static void main(String[] args) throws Exception {
// Compiler says: "error: incompatible types: MyClass cannot be converted to Closeable"
//        Closeable onClose1 = new MyClass();
//        onClose1.close();

// Compiles but ClassCastException at runtime:
// Exception in thread "main" java.lang.ClassCastException: class Test$MyClass cannot be cast to class java.io.Closeable (Test$MyClass is in unnamed module of loader // com.sun.tools.javac.launcher.Main$MemoryClassLoader @62fdb4a6; java.io.Closeable is in module java.base of loader 'bootstrap')
//  at Test.main(Test.java:27)
        Closeable onClose2 = emptyAction();
        onClose2.close();
    }
}

onClose1请注意,编译器如何正确拒绝直接调用构造函数ClassCastExceptiononClose2我使用 Java 11 和 Java 14 进行了测试,两次都得到了相同的结果。

我不明白为什么编译器接受编译onClose2?泛型类型签名并不表示返回的类型确实是Closeable,它也没有创建充当桥梁的 lambda。我什至想知道擦除,但我不知道它可以在这里扮演什么角色......

有人有什么解释吗?

标签: javagenericstype-inferencetype-bounds

解决方案


首先:你正在用界面做奇怪的事情。我强烈建议不要使用具有相同方法名称和返回类型仅在 throws-statement 中不同的接口。默认方法和实现语句的顺序创建了一定程度的自由度。你通过这两个接口表达的合约不再可靠。

MyClass 也不需要实现@FunctionInterface. 这就是 lambdas 的用途:自动检查。

您应该尝试了解代码的两个问题:

  1. 将硬转换为 C 会导致编译器错误,即 MyClass 无法转换为 C。
  2. 使用所有显式类型暴力运行代码仍然会产生类转换异常。

我认为您的代码根本不是用来运行的——您已经指出缺少implements Closeable. 您想了解 lambda 和泛型的含义。这绝对超出了简短回答的可能性,但我可以给你两个重要提示:

  1. 泛型不是继承,因此 aList<Number>永远不会接受 a Long。而 aList<? extends Number>根本不接受任何东西。
  2. 任何具有与 java.util.Function 完全相同签名的方法的类型,无论是接口还是类,都可以用作 Lambda 表达式中的 java.util.Function。(语言规范§15.27.3)

因此,除了类型参数中的相同字符外,您的 C 和 MyClass 不共享任何内容。现在该方法表明,任何分配都将向后传播到 MyClass,就像您尝试使用 Closeable 一样。你的意思是:

public interface SuperInterface extends Action1, Action2 {
}

public static SuperInterface emptyAction() {
   return new MyClass();
}

泛型不能超越经典接口。它们只提供更详细的表达式,例如 aList真正包含的内容。永远不要试图用泛型来表达继承。这就是为什么你不能在Map<String,Factory<T>>一个灵活的朋友中收集工厂课程,这取决于任务。

Angelika Langer有很多关于泛型的文章和资源,以及它们为什么不能按您期望的那样工作。我没有链接任何特定页面,这更像是一次旅程而不是阅读。

对于 lambdas 尝试摄取Java Language Specification。我在#2 中写了相关段落。根本没有 Lambda 表达式。它也不会修复您的代码。Lambda 和异常不能一起工作。除了要抛出异常的本地/最小上下文之外,Lambda 表达式中没有异常路径。


推荐阅读