更改两个类型参数边界的顺序会导致Java运行时错误

时间:2016-12-15 15:08:23

标签: java generics lambda java-8 type-bounds

以下代码在运行时崩溃,但是如果您更改那一行以使Model & Serializable变为Serializable & Model,那么它运行正常。谁能解释一下发生了什么?这是Java中的错误吗?看起来类型边界的顺序似乎不重要。

import java.io.Serializable;

interface Model {
    void foo();
}

class ModelA implements Model, Serializable {
    public void foo() {

    }
}

class MemcachedHelper<T extends Serializable> {
    T getCached(String key, Maker<T> make) {
        return make.get();
    }
    interface Maker<U extends Serializable> {
        U get();
    }
}

class Query {
    Object getResult() {
        return new ModelA();
    }
}
public class Main {

    // private static <T extends Serializable & Model>
    private static <T extends Model & Serializable>
    T getModel(Class<T> modelClass, MemcachedHelper<T> cache) {
        String key = "key:" + modelClass.getSimpleName();
        T thing = cache.getCached(key, () -> {
            Query q = new Query();
            return (T)q.getResult();
        });
        return thing;
    }

    public static void main(String[] args) {
        MemcachedHelper<ModelA> cache = new MemcachedHelper<>();
        Model thing = getModel(ModelA.class, cache);
        System.out.printf("Got thing: %s\n", thing);
    }

}

运行时错误是:

Exception in thread "main" java.lang.BootstrapMethodError: call site initialization exception
  at java.lang.invoke.CallSite.makeSite(CallSite.java:341)
  at java.lang.invoke.MethodHandleNatives.linkCallSiteImpl(MethodHandleNatives.java:307)
  at java.lang.invoke.MethodHandleNatives.linkCallSite(MethodHandleNatives.java:297)
  at Main.getModel(Main.java:33)
  at Main.main(Main.java:42)
  ...  
Caused by: java.lang.invoke.LambdaConversionException: Type mismatch for lambda expected return: interface Model is not convertible to interface java.io.Serializable
  at java.lang.invoke.AbstractValidatingLambdaMetafactory.validateMetafactoryArgs(AbstractValidatingLambdaMetafactory.java:286)
  at java.lang.invoke.LambdaMetafactory.metafactory(LambdaMetafactory.java:303)
  at java.lang.invoke.CallSite.makeSite(CallSite.java:302)
  ... 9 more

这是JDK版本1.8.0_101。

1 个答案:

答案 0 :(得分:4)

有一个角落案例,类型边界的顺序很重要。泛型方法的原始方法签名或引用类型变量的方法由第一个类型绑定确定。

因此,如果您将T的{​​{1}}类型参数声明为getModel,则其原始返回类型将为<T extends Model & Serializable>,但当您将其声明为Model时它的原始返回类型为<T extends Serializable & Model>

如果您将其声明为Serializable,则其原始退货类型将为<T extends Object & Serializable & Model>

显然,Object对为返回javac的lambda表达式创建的合成方法使用相同的策略。但是,由于目标类型T具有功能签名Maker<U extends Serializable>,因此其原始签名为() -> U。因此,当您使用导致原始返回类型为() -> SerializablegetModel的{​​{1}}的{​​{1}}声明时,它与指定的目标类型的签名期望不匹配返回类型与T兼容。

为了说明原始签名如何在此处进行交互,如果您将Object的声明更改为Model,其原始功能签名的返回类型将为Serializable,这将与所有Maker的声明变体。

但是,当然,这些都是实施细节。声明它的方式不应该影响代码的正确性,即使原始代码不同,它也不应该突然中断。这可以被认为是编译器错误。正如您在this question中所看到的,交叉类型的错误处理具有更长的传统。如果编译器只选择匹配目标类型的原始功能签名的合成方法的原始返回类型,问题就会消失。