有可能声明一个Supplier<T>需要抛出异常吗?

49

我正在尝试重构以下代码:

/**
 * Returns the duration from the config file.
 * 
 * @return  The duration.
 */
private Duration durationFromConfig() {
    try {
        return durationFromConfigInner();
    } catch (IOException ex) {
        throw new IllegalStateException("The config file (\"" + configFile + "\") has not been found.");
    }
}

/**
 * Returns the duration from the config file.
 * 
 * Searches the log file for the first line indicating the config entry for this instance.
 * 
 * @return  The duration.
 * @throws FileNotFoundException If the config file has not been found.
 */
private Duration durationFromConfigInner() throws IOException {
    String entryKey = subClass.getSimpleName();
    configLastModified = Files.getLastModifiedTime(configFile);
    String entryValue = ConfigFileUtils.readFileEntry(configFile, entryKey);
    return Duration.of(entryValue);
}

我想到了以下内容作为开始:

我想到了以下内容作为开始:

private <T> T getFromConfig(final Supplier<T> supplier) {
    try {
        return supplier.get();
    } catch (IOException ex) {
        throw new IllegalStateException("The config file (\"" + configFile + "\") has not been found.");
    }
}

然而,很显然这段代码无法编译通过,因为Supplier不能抛出IOException异常。有没有任何方法可以将其添加到getFromConfig方法的声明中呢?

或者说唯一的方法是像下面这样做吗?

@FunctionalInterface
public interface SupplierWithIO<T> extends Supplier<T> {
    @Override
    @Deprecated
    default public T get() {
        throw new UnsupportedOperationException();
    }

    public T getWithIO() throws IOException;
}

更新:我刚刚意识到Supplier接口非常简单,只有一个get()方法。我扩展Supplier的原因是为了保留基本功能,例如默认方法。


@MarkoTopolnik,这听起来足够好成为一个答案(最好您能详细解释一下)。我刚刚在下面提供了我的意见,但是您的想法似乎更准确。 - Simon Forsberg
2
@SimonAndréForsberg JB Nizet已经在这里做得很好了:https://dev59.com/OGYq5IYBdhLWcg3w-Vdl#14045585。我只是不想麻烦地包装异常,而是使用`sneakyThrow`。 - Marko Topolnik
@MarkoTopolnik,您能否更详细地解释一下您示例中的uncheckedCallsneakyThrow是什么? - skiwi
2
这个问题在lambda邮件列表中进行了深入讨论。如果您想要查看,那里讨论了一些替代方案。JB Nizet建议的包装器解决方案也在那里讨论过。 - Edwin Dalorzo
显示剩余4条评论
6个回答

27

编辑

正如多次指出的那样,您不需要任何自定义类,而是使用CallableRunnable

错误,过时的解决方案

Consider this generic solution:

// We need to describe supplier which can throw exceptions
@FunctionalInterface
public interface ThrowingSupplier<T> {
    T get() throws Exception;
}

// Now, wrapper
private <T> T callMethod(ThrowingSupplier<T> supplier) {
    try {
        return supplier.get();
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
        return null;
}

// And usage example
String methodThrowsException(String a, String b, String c) throws Exception {
    // do something
}

String result = callMethod(() -> methodThrowsException(x, y, z));

12
您不需要声明ThrowingSupplier。内置类Callable已经实现了您想要的功能。 - Gili
Callable 的一个小“问题”是它会抛出异常。这可能会对引用代码产生连锁反应。使用通用异常的解决方案可能会降级为 RuntimeException,从而避免用户空间中的代码膨胀。 - Adam Kotwasinski

21
  • If you would do that, you wouldn't be able to use it as a Supplier as it would only throw UnsupportedOperationException.

  • Considering the above, why not create a new interface and declare the getWithIO method in it?

    @FunctionalInterface
    public interface SupplierWithIO<T> {
        public T getWithIO() throws IOException;
    }
    
  • Perhaps some things are better of as old-style Java interfaces? Old-style Java isn't gone just because there's Java 8 now.


我刚刚更新了我的帖子,并解释了为什么要进行扩展。如果Supplier添加了新的默认方法,这种方法将会失效。另一方面,升级JVM到该版本也不会破坏我的应用程序,因此没有任何损害。 - skiwi
14
这种方法的问题在于,这个供应商无法与其它 JDK API 一起使用,因为这些 API 都期望一个真正的供应商(Supplier)。 - Edwin Dalorzo
6
不需要声明SupplierWithIO,内置的Callable类已经实现了你想要的功能。 - Gili
3
从技术上讲,Callable声明会抛出更通用的Exception。如果想要在其他JDK API中使用它,可能需要按这种方式操作。 - Simon Forsberg

21
在Lambda邮件列表中,这个问题已经充分讨论过了。正如您所看到的,Brian Goetz建议写一个自己的组合器作为替代方案:

Or you could write your own trivial combinator:

static<T> Block<T> exceptionWrappingBlock(Block<T> b) {
     return e -> {
         try { b.accept(e); }
         catch (Exception e) { throw new RTE(e); }
     };
}

You can write it once, in less that the time it took to write your original e-mail. And similarly once for each kind of SAM you use.

I'd rather we look at this as "glass 99% full" rather than the alternative. Not all problems require new language features as solutions. (Not to mention that new language features always causes new problems.)

那时候消费者界面被称为Block。

我认为这与Marko上面提出的JB Nizet的答案相对应。

后来Brian 解释了为什么要这样设计(问题的原因)。

是的,你需要提供自己的异常SAM。但是使用它们时,Lambda转换将能够正常工作。 EG讨论了为此提供额外语言和库支持的问题,并最终认为这是一种不好的成本/效益权衡。 基于库的解决方案会导致异常与非异常SAM类型扩展2倍,这与原始专门化现有的组合爆炸产生交互作用。 可用的基于语言的解决方案在复杂度/价值权衡方面处于劣势。虽然有一些替代解决方案,我们将继续探索 - 虽然很明显,不适用于8,可能也不适用于9。 同时,你已经有工具可以完成你想要的事情。我知道你希望我们为你提供最后一英里的服务(其次,你请求的实际上是“为什么不放弃检查异常”),但我认为当前状态让你完成了工作。

5

由于我对这个主题有补充说明,所以我决定添加我的答案。

你可以选择编写一个便利方法,该方法可以:

  1. 将抛出已检异常的lambda包装成未经检查的lambda;
  2. 仅调用lambda,取消检查异常。

采用第一种方法时,您需要为每个函数式方法签名编写一个便利方法,而采用第二种方法时,您需要总共两种方法(除了基本类型返回方法):

static <T> T uncheckCall(Callable<T> callable) {
  try { return callable.call(); }
  catch (Exception e) { return sneakyThrow(e); }
}
 static void uncheckRun(RunnableExc r) {
  try { r.run(); } catch (Exception e) { sneakyThrow(e); }
}
interface RunnableExc { void run() throws Exception; }

这样做可以让您插入一个调用其中一个方法的空lambda,但该lambda会关闭传递给原始方法的外部lambda的任何参数。通过这种方式,您可以利用自动lambda转换语言功能而无需编写样板代码。
例如,您可以编写
stream.forEachOrdered(o -> uncheckRun(() -> out.write(o)));

与之相比
stream.forEachOrdered(uncheckWrapOneArg(o -> out.write(o)));

我还发现,对于所有lambda签名重载包装方法名称通常会导致模糊的lambda表达式错误。因此,您需要更长的不同名称,这会导致比上述方法更长的代码字符。最后,请注意,该方法仍然不能防止编写重用uncheckedRun/Call的简单包装方法,但我发现这种方法实际上并不有趣,因为节省的效果在最好的情况下是微不足道的。

4

我添加了自己的解决方案,不一定是对我的问题的直接答案,经过一些修订后引入了以下内容:

@FunctionalInterface
public interface CheckedSupplier<T, E extends Exception> {
    public T get() throws E;
}

private <R> R getFromConfig(final String entryKey, final Function<String, R> converter) throws IOException {
    Objects.requireNonNull(entryKey);
    Objects.requireNonNull(converter);
    configLastModified = Files.getLastModifiedTime(configFile);
    return ConfigFileUtils.readFileEntry(configFile, entryKey, converter);
}

private <T> T handleIOException(final CheckedSupplier<T, IOException> supplier) {
    Objects.requireNonNull(supplier);
    try {
        return supplier.get();
    } catch (IOException ex) {
        throw new IllegalStateException("The config file (\"" + configFile + "\") has not been found.");
    }
}

这些都是一次性声明,现在我添加了两个调用代码的变量:

private Duration durationFromConfig() {
    return handleIOException(() -> getFromConfig(subClass.getSimpleName(), Duration::of));
}

private int threadsFromConfig() {
    return handleIOException(() -> getFromConfig(subClass.getSimpleName() + "Threads", Integer::parseInt));
}

我对将IOException转换为UncheckedIOException并不太满意,原因如下:

  1. 我需要添加每个可能引发IOException的方法的未经检查的变体。
  2. 我更喜欢明显地处理IOException,而不是依赖于希望您不会忘记捕获UncheckedIOException

如果未能捕获 UncheckedIOException,您认为会导致什么典型的不良后果? - Marko Topolnik
@MarkoTopolnik 如果你忘记捕获它们,最终会导致程序失败,在你应该从这种情况中恢复并继续正常程序流程的情况下,如果你知道可能会抛出 IOException,那么就可以避免这种情况。 - skiwi
值得庆幸的是,我看到核心团队明确地感受到了从未来版本的Java中完全删除异常检查的压力。因此,即使核心团队也找不到真正合理的理由去违背潮流。 - Marko Topolnik
@MarkoTopolnik,在这种情况下,没有配置文件。对于其他可能导致IOExceptons的情况,我会有点害怕。出于好奇,您在哪里找到Java未来的消息?我至少想阅读或可能参加讨论。可悲的是,有大量的邮件列表,很难选择正确的一个。 - skiwi
仅在此处由Edwin链接的讨论中。顺便问一下,您真的从未遇到过难以本地化的生产错误吗?这些错误最终源于不适当的已检查异常处理吗?catch (IOException e) { 嗯...现在怎么办?让我们交给Eclipse:e.printStackTrace(); }高级版本:catch (IOException e) { log.error(e); }高级加版:catch (IOException e) { log.error("IO failure", e)---现在我至少可以在日志中获得堆栈跟踪了。 - Marko Topolnik
显示剩余3条评论

0

我们也可以使用这种通用解决方案。任何类型的异常都可以通过这种方法来处理。

    @FunctionalInterface
    public interface CheckedCall<T, E extends Throwable> {
        T call() throws E;
    }

    public <T> T logTime(CheckedCall<T, Exception> block) throws Exception {

        Stopwatch timer = Stopwatch.createStarted();
        try {
            T result = block.call();
            System.out.println(timer.stop().elapsed(TimeUnit.MILLISECONDS));
            return result;
        } catch (Exception e) {
            throw e;
        }
    }


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接