为什么Java 8类型推断不会考虑Lambdas在重载选择中引发的异常?

时间:2015-09-01 03:29:17

标签: java exception lambda java-8 type-inference

关于lambdas及其相关的异常签名,我有一个关于Java 8推断的问题。

如果我定义了一些方法foo:

public static <T> void foo(Supplier<T> supplier) {
    //some logic
    ...
}

然后,我得到了在大多数情况下能够为给定foo(() -> getTheT());编写T的简洁语义。但是,在此示例中,如果我的getTheT操作声明它throws Exception,那么我的foo方法会使供应商不再编译:{{1​​}}的供应商方法签名不会# 39;抛出异常。

这似乎是解决这个问题的一个好方法,就是重载foo以接受任一选项,重载定义为:

get

其中ThrowingSupplier定义为

public static <T> void foo(ThrowingSupplier<T> supplier) {
   //same logic as other one
   ...
}

通过这种方式,我们有一个推出例外的供应商类型和一个不存在例外情况的供应商类型。所需的语法是这样的:

public interface ThrowingSupplier<T> {
   public T get() throws Exception;
}

但是,这会导致问题,因为lambda类型不明确(可能无法在Supplier和ThrowingSupplier之间解决)。做一个明确的演员la foo(() -> operationWhichDoesntThrow()); //Doesn't throw, handled by Supplier foo(() -> operationWhichThrows()); //Does throw, handled by ThrowingSupplier 会起作用,但它摆脱了所需语法的大部分简洁。

我想底层的问题是:如果Java编译器能够解决我的一个lambda因为它在仅供应商案例中抛出异常而不兼容的事实,为什么它不能使用相同的信息来导出次要的类型推理案例中的lambda类型?

任何人都可以指出的任何信息或资源同样值得赞赏,因为我不太确定在哪里可以找到有关此事的更多信息。

谢谢!

3 个答案:

答案 0 :(得分:30)

如果它让你感觉更好,那么在JSR-335设计过程中确实仔细考虑了这个主题。

问题不是“为什么不能”,而是“为什么我们选择不这样做”。当我们发现多个可能适用的重载时,我们当然可以选择在每组签名下推测性地归属lambda主体,并修剪lambda主体未能进行类型检查的候选者。

然而,我们的结论是,这样做可能弊大于利;例如,这意味着,根据此规则,对方法主体的微小更改可能会导致某些方法过载选择决策在没有用户打算这样做的情况下进行静默更改。最后,我们得出结论,使用方法体中存在的错误来丢弃可能适用的候选者会导致更多的混淆而不是利益,特别是考虑到有一个简单而安全的解决方法 - 提供目标类型。我们认为这里的可靠性和可预测性超过了最佳的简洁性。

答案 1 :(得分:5)

首先,你不必过载:D - 重载永远不是必需的;使用2个不同的方法名称,例如foofooX

其次,我不明白你为什么需要2种方法。如果要以不同方式处理已检查和未检查的异常,可以在运行时完成。要实现“异常透明度”,您可以执行

interface SupplierX<T, X extends Throwable>
{
    T get() throws X;
}

<T, X extends Throwable> void foo(Supplier<T, X> supplier)  throws X { .. }


foo( ()->"" );  // throws RuntimeException

foo( ()->{ throw new IOException(); } );  // X=IOException

最后,可以实现抛弃lambda返回类型的歧义;编译器使用返回类型,就像使用参数类型选择最具体的方法一样。这让我们有了将值与异常类型一起包装起来的想法,如Result<T,X>,就像他们所说的“monad”。

interface Result<T, X extends Throwable>
{
    T get() throws X;
}

// error type embedded in return type, not in `throws` clause

static Result<String,        Exception> m1(){ return ()->{ throw new Exception();};  }
static Result<String, RuntimeException> m2(){ return ()->{ return "str";         };  }

  // better to have some factory method, e.g. return Result.success("str");

public static void main(String[] args)
{
    foo(()->m1());  // foo#2 is not applicable
    foo(()->m2());  // both applicable; foo#2 is more specific
}



interface S1<T> { T get(); }  

static <T> void foo(S1<Result<T, ? extends        Exception>> s)
{
    System.out.println("s1");}
}


interface S2<T> { T get(); }  // can't have two foo(S1) due to erasure

static <T> void foo(S2<Result<T, ? extends RuntimeException>> s)
{
    System.out.println("s2");
}

答案 2 :(得分:2)

任何可以作为Supplier<T>接受的lambda也可以被接受为ThrowingSupplier<T>。以下编译:

public static interface ThrowingSupplier<T>{
    public T get() throws Exception;
}

public static <T> void foo(ThrowingSupplier<T> supplier) {

}

public static String getAString(){
    return "Hello";
}

public static String getAnotherString() throws Exception{
    return "World";
}

public static void main(String[] args) {
    foo(()->getAString());
    foo(()->getAnotherString());
} 

鉴于上述情况,您可能不需要这样,但如果foo 必须接受非投掷Supplier<T>,您始终可以包装异常抛出方法在一个方法中将其清洗成未经检查的异常:

public static <T> void foo(Supplier<T> supplier) {

}

public static String getAString(){
    return "Hello";
}

public static String getAnotherString() throws Exception{
    return "World";
}

public static String getAnotherStringUnchecked(){
    try{
        return getAnotherString();
    } catch(Exception e){
        throw new RuntimeException("Error getting another string",e);
    }
}   

public static void main(String[] args) throws Exception{
    foo(()->getAString());
    foo(()->getAnotherStringUnchecked());
}