【问题标题】:Why Doesn't Java 8 Type Inference Consider Exceptions Thrown by Lambdas in Overload Selection?为什么 Java 8 类型推断在重载选择中不考虑 Lambda 抛出的异常?
【发布时间】:2015-09-01 03:29:16
【问题描述】:

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

如果我定义一些方法 foo:

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

然后我得到了能够在大多数情况下为给定的T 编写foo(() -&gt; getTheT()); 的简洁明了的语义。但是,在此示例中,如果我的 getTheT 操作声明它为 throws Exception,则采用 Supplier 的 foo 方法不再编译:get 的 Supplier 方法签名不会引发异常。

似乎解决这个问题的一个不错的方法是重载 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 类型不明确(可能无法在供应商和 ThrowingSupplier 之间解决),这会导致问题。像 foo((ThrowingSupplier)(() -&gt; operationWhichThrows())); 那样进行显式转换会起作用,但它会消除所需语法的大部分简洁性。

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

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

谢谢!

【问题讨论】:

  • 你的问题和我的很相似:stackoverflow.com/questions/14039995/…
  • 当然请注意,接口方法的实现不需要抛出异常,即使该接口中的声明被声明为抛出异常。因此,对于 lambda,不仅仅是抛出的 lambda “应该”被确定使用相应的方法,而是非抛出的 lambda 应该传递给哪个方法的问题?即,() -&gt; nonThrowingOperation 可以是任一 SupplierThrowingSupplier

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


【解决方案1】:

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

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

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

【讨论】:

  • 嗨,布赖恩,感谢您的回复。这种反应是有道理的,可靠性/精度绝对是合理的优先考虑简洁。您是否偶然遇到过方法体修改会默默地改变方法的重载行为的示例?似乎在这些情况下,包含这些 lambda 的方法的主体也需要更改。我确定我错了,但我现在还不能围绕一个具体实例来思考。
  • 这个答案很好,但我认为如果有具体示例以及对 JLS 的引用以及导致此选择的讨论(如果有的话)会更好。
  • 不仅用户应该对此感到高兴,编译器工程师也应该对设计保持原样感到高兴:)
【解决方案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 返回类型;编译器使用返回类型就像使用参数类型来选择最具体的方法一样。这给了我们将值与异常类型包装在一起的想法,如Result&lt;T,X&gt;,正如他们所说的“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");
}

【讨论】:

  • 好吧,添加X 后缀(我的个人约定)与没有 lambda 的不便相比是无法比拟的。
  • 我不明白您的nullFile 示例。我想你可以有一个resultOrNull( ThrowingSupplier ) 方法,并且该方法也适用于非抛出的 lambda 体。
  • 查看Comparator中的方法——在java8的早期,它们被重载了;但他们后来简化了语言规范,重载不再起作用,所以我们有一些奇怪的东西,比如comparingcomparingIntcomparingLong 等。
  • @paul - 有一个解决方案:)见stackoverflow.com/q/30759692/2158288
  • @paul - 等等,这是个糟糕的例子。请参阅我对此答案的编辑。
【解决方案3】:

任何可以被接受为Supplier&lt;T&gt; 的lambda 也可以被接受为ThrowingSupplier&lt;T&gt;。以下编译:

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&lt;T&gt;,您总是可以将抛出异常的方法包装在一个方法中将其清洗为未经检查的异常:

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

【讨论】:

  • 嘿,天哪,谢谢你的建议。不幸的是,虽然这可行,但它消除了 lambda 简洁的好处。从fooChecked(()-&gt;getAnotherString())foo((ThrowingSupplier)(()-&gt;getAnotherString())foo(()-&gt;wrap(getAString())) 的解决方案都有效,但它们消除了所需语法的纯粹简洁性。此外,我看不出有什么正当理由无法推断出适当的类型。不过,感谢您的宝贵时间,您的方式绝对是有效的!
  • @paul:请参阅我刚刚所做的编辑 - 使用 ThrowingSupplier&lt;T&gt; 作为 foo 的输入,让您无需包装方法,保持简洁的 lambda 语法。 (顶部代码示例中的新内容)
  • 再次感谢!不幸的是,这种方法的缺点是它使包含非抛出方法调用的范围处理异常(这是不可能的)。例如,上面的版本编译是因为main 抛出异常。例如,如果我的函数的目的是捕获和吞下异常,则调用处理异常的方法似乎违反直觉。不过,再次感谢您的输入,这种方式绝对适用于可以抛出异常的情况。
  • @paul: main 实际上不需要抛出异常——这是之前遗留下来的。查看最新的编辑:-)。如果需要,您可以捕获并处理 foo 中的异常,并且永远不要让它们逃逸到 main 的外部范围。
  • 在上面的例子中不需要捕获异常的唯一原因是在上面的foo方法中从未调用过ThrowingSupplier&lt;T&gt;。一旦完成,异常就会被抛出,并且需要被捕获。谢谢!
猜你喜欢
  • 1970-01-01
  • 2015-08-11
  • 1970-01-01
  • 2018-10-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多