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

2022-09-01 03:17:54

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

如果我定义一些方法 foo:

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

然后我得到了一个漂亮而简洁的语义,能够在大多数情况下为给定的.但是,在此示例中,如果我的操作声明它,则我采用 Supplier 的方法不再编译:Supplier 方法签名不会引发异常。foo(() -> getTheT());TgetTheTthrows Exceptionfooget

似乎解决这个问题的一个体面的方法是重载foo接受任何一个选项,重载的定义是:

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

其中 ThrowingSupplier 被定义为

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

通过这种方式,我们有一个会引发异常的供应商类型,另一个不会引发异常。所需的语法将如下所示:

foo(() -> operationWhichDoesntThrow()); //Doesn't throw, handled by Supplier
foo(() -> operationWhichThrows()); //Does throw, handled by ThrowingSupplier

但是,由于 lambda 类型不明确(可能无法在 Supplier 和 ThrowingSupplier 之间解决),这会导致问题。做一个显式的转换 la 会起作用,但它摆脱了所需语法的大部分简洁性。foo((ThrowingSupplier)(() -> operationWhichThrows()));

我想潜在的问题是:如果Java编译器能够解决我的一个lambda不兼容的事实,因为它在仅限供应商的情况下抛出异常,为什么它不能使用相同的信息来派生辅助类型推断情况下的lambda类型?

任何人都可以向我指出的任何信息或资源同样非常感谢,因为我不太确定在哪里寻找有关此事的更多信息。

谢谢!


答案 1

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

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

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


答案 2

首先,您不必超载: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

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

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");
}

推荐