有没有办法将抛出异常的函数存储在变量中? (java)

Is there a way to store a function that throws an exception in a variable? (java)

在java中,可以将一个函数存储在一个变量中,然后像这样在以后应用它:

import java.util.function.Function;

class Main {
    Function<Integer, Integer> f;

    public Main(){
        f = this::add1;

        f.apply(1); //returns 2

        f.apply(20); //returns 21
    }

    public Integer add1(Integer value){
        return value+1;
    }
}

但是,当我尝试让函数抛出异常然后用 try/catch 捕获该异常时,我得到了一些错误:

import java.util.function.Function;

class NumberTooSmall extends Exception{
    public NumberTooSmall(String message){ super(message); }
}

class Main {
    Function<Integer, Integer> f;

    public Main(){
        f = this::add1OrThrowError; // Unhandled exception

        try {
            //should throw error
            f.apply(1);
        } catch (NumberTooSmall e){ //Exception is never thrown in the corresponding try block
            // should be called here
            caseWhenErrorIsThrown();
        }

        try {
            // should return 21
            f.apply(20);
        } catch (NumberTooSmall e){ //Exception is never thrown in the corresponding try block
            // shouldn't be called here
            caseWhenErrorIsThrown();
        }
    }

    public Integer add1OrThrowError(Integer value) throws NumberTooSmall {
        if(value < 10) {
            throw new NumberTooSmall("Value is too low");
        }
        return value+1;
    }

    public void caseWhenErrorIsThrown(){ 
        // not important
    }
}

如何解决这些错误?我希望我需要更改函数变量 f 的 class,但我找不到它应该是什么。此外,仅正常调用该函数也不是一种选择,因为我希望 f 成为更多方法。

编辑: 异常名称从 Error 更改为 NumberTooSmall 因为我了解到 Error 已经是内置 java class (java.lang.Error) 的名称.

您有两个选择:

  • throw a RuntimeException(或其任何子类):不需要声明它们,因此您可以“静静地”抛出它们

  • 声明您自己的 Function 替代方案,其中包含一个 throws 子句,有点像这样:

    public interface ThrowingFunction<I, O, E extends Throwable> {
        public O apply(I input) throws E;
    }
    

    请注意,某些实用程序库(例如 Apache Commons Lang3)已经具有此类接口。例如 FailableFunction.

还有第三种讨厌的方法,我不推荐这种方法,但为了完整起见,我应该提一下:您可以偷偷地抛出您的方法未声明实际抛出的异常。 ExceptionUtils.rethrow 实现了这个。

“错误”对于您的 Exception 子class 来说是一个非常 糟糕的名字。 JDK 中已经有一个 java.lang.Error class,它“表示合理的应用程序不应尝试捕获的严重问题”,因此将您的 Exception subclass Error 会让代码的读者感到困惑。我会打电话给你的 Exception subclass NumberTooSmallException 来回答剩下的问题。

NumberTooSmallException 继承自 Exception 而不是 RuntimeException,这意味着它是一个 checked exception.

您的 add1OrThrowError 方法被声明为抛出此已检查的异常,因此编译器将在您每次使用 add1OrThrowError 时检查您是否已处理异常,或者您已声明封闭函数 throws NumberTooSmallException 也是。这个想法是为了确保抛出的每个异常都得到处理。

注意 Function.apply 被声明为不抛出任何已检查的异常,所以这是一个错误:

f = this::add1OrThrowError;

f 声明不抛出任何东西,但 this::add1OrThrowError 声明抛出 NumberTooSmallException.

因此,您可以创建自己的 Function 接口版本,该接口的 apply 抛出 NumberTooSmallException:

interface ErrorThrowingFunction<T, R> {
    R apply(T t) throws NumberTooSmallException;
}

(或 Joachim Sauer 的回答中更通用的版本)

或者您可以通过继承 RuntimeException 使 NumberTooSmallException 成为 未检查 异常,这样编译器就不会检查它是否始终被处理。这允许您将 this::add1OrThrowError 分配给 Function,但也允许您在没有 try...catch.

的情况下调用 f.apply
class NumberTooSmallException extends RuntimeException{
    public NumberTooSmallException(String message){ super(message); }
}

'why' 的一些背景知识:

java 中的闭包不透明。

在 java 中,闭包(a -> foo(a)someExpr::someMethodRef)对于以下 3 个概念不透明

  • 已检查异常:闭包抛出的已检查异常列表需要 'handled'(在闭包中捕获,或者 @FunctionalInterface 中定义闭包类型的方法需要是向 throws 声明它)然后在那里,您 不能 依赖 catch 块或 throws 子句,在您进行闭包的上下文中。这就是您 运行 遇到的问题。
  • 非有效最终局部变量:您根本无法从 lambda 外部访问(读取或写入)局部变量,除非它们是 final,或者它们可以 final 而没有编译器错误,在这种情况下 javac 帮了你一个忙,就像你把 final 放在上面一样。
  • 控制流:您不能在闭包中写入 break 并从闭包外部应用于 for/while/switch/等。例如这不起作用:
boolean banned = false;
for (String x : userNames) {
 dbAccess.exec(db -> {
   if (db.selectBool("SELECT banned FROM users WHERE un = ?", x)) {
      banned = true; // won't compile - mutable local var
      break; // won't compile - control flow
   }
 });
}

为什么不呢?

因为这实际上是您想要的,if 它是一个 'travelling' 闭包。不过,使用即丢闭包既烦人又奇怪。因此,要理解这一点,请考虑旅行闭包的概念,然后它就有意义了。

旅行 vs. 用即弃

根据设计,函数是您可以 'ship around' 的东西。它可以'escape your context'。您可以自由地将一个函数存储在一个字段中,或者将它传递给另一个将它存储在一个字段中的方法。然后该字段可以在 5 天后由另一个线程读取(让我们假设一些非常长的 运行ning VM,例如一个 JVM 服务网页)然后代码是 运行.

换句话说,给定:

try {
    // some code that _DEFINES_ a function, such as:
    foo(a -> System.out.println(a));
} catch (Something e) {
}

不能保证这个'makes sense'。也许 foo 会采取闭包,将其存储在一个字段中,并 运行 从现在起 5 天后 - 或者,无论如何,运行 在 [=] 之后78=]上面的代码早就完成了。通过完成那个 catch 块,运行 所需的所有上下文都消失了。没有办法运行啦!当 try 块中包含的代码抛出它时,确实感觉 catch 块包含错误处理程序,但它不是这样工作的,因为在这种情况下 catch 块不再 'exists'。

如果您编写的代码毫无意义,编译器或运行时间会阻止您这样做是个好主意。毕竟,最好快速找到错误并给出一个很好的解释,而不是不得不观察奇怪的行为并继续追逐错误以找出您遗漏了什么!

所以 javac 就是这么做的,不会编译这段代码

即使该代码完全合理并且可以完全按照您的预期工作,如果闭包不运行的话。

因此,您可以将闭包分为两个阵营:

  • 用即弃闭包:它们是 运行(0 到多次),然后在定义它们的词法上下文中被遗忘。 java中很多函数的用法都是这样的,比如list.stream().map(closureHere).filter(anotherClosureHere).collect(),或者list.sort(closureHere).
  • traveling closures:它们被存储并 运行 之后,and/or 发送到其他线程(即使那些线程然后 运行 当场并且代码无法继续直到它们已经完成,例如 fork/join:那些异常仍然不会在您的 catch 块中结束。例如,new TreeSet<>(comparatorHere)new Thread(runnableHere). (note how it doesn't depend on type; with list.sort, we passed a Comparatorwhich is use-it-and-lose-it, but withnew TreeSet, we _also_ passed a Comparator` 但这是一个移动闭包。

如果一个接受函数的方法可以声明它 运行 是在 'use it and lose it' 模式下还是在 'travelling' 模式下运行,那么 javac 可以添加适当的糖来为您提供已检查异常、可变局部变量和控制流的透明度(并且 javac 还可以方便地告诉您是否随后采用该函数 ref 并将其存储在字段中或传递它一个未明确声明将其视为“用即弃”的方法)。也许有一天,因为我很确定 java 可以以向后兼容的方式将其添加到语言中。

因此,您应该考虑将闭包用于内联操作,这是一个小问题。如果你可以在没有的情况下也能写得很好,那么就这样做(如果循环和流同样简单,则它们优于流)。出于同样的原因,我们编写 getter 而不是直接访问字段,出于同样的原因,风格指南倾向于强制使用大括号,即使 javac 不是:你今天可能不需要这 3 个透明胶片中的任何一个,但也许明天你这样做,而且在那个时候不得不重写是很烦人的。