有没有办法让Runnable的run()方法抛出异常?

97
在实现Runnable接口的类中,我正在调用run()方法,该方法被设计为抛出异常。但是Java编译器不允许我这样做,并建议我使用try/catch来包围它。问题是,通过使用try/catch来包围它,我使得该特定的run()变得无用。我确实想要抛出那个异常。如果我为run()本身指定了throws,编译器会抱怨Exception is not compatible with throws clause in Runnable.run()

通常情况下,我完全不介意不让 run() 抛出异常。但是我有一个独特的情况,必须拥有这个功能。

如何解决这个限制?


除了其他答案,为了跟踪任务的进度,您可以使用FutureTask类。 - JProgrammer
1
非Android Java问题:https://dev59.com/WHM_5IYBdhLWcg3wdy5g?lq=1 - Ciro Santilli OurBigBook.com
10个回答

95

您可以使用Callable,将其提交给一个ExecutorService,并通过ExecutorService.submit()方法等待结果,使用FutureTask.isDone()来判断是否已完成。

isDone()返回true时,调用FutureTask.get()获取结果。如果您的Callable抛出了一个Exception,那么FutureTask.get()也会抛出一个Exception,而您可以通过Exception.getCause()访问原始的异常信息。


44

如果你想将实现Runnable接口的类传递到Thread框架中,那么你必须遵守该框架的规则。请参考Ernest Friedman-Hill的回答,了解为什么以其他方式做这件事是不明智的。

不过我猜你想在你的代码中直接调用run方法,以便你的调用代码可以处理异常。

解决这个问题很容易。不要使用Thread库中的Runnable接口,而是创建一个具有修改后签名的自己的接口,允许抛出已检查异常,例如:

public interface MyRunnable
{
    void myRun ( ) throws MyException;
}

你甚至可以创建一个适配器来将此接口转换为适合在 Thread 框架中使用的真正的 Runnable(通过处理已检查异常)。


通过不局限于固有思维,就能得出这样一个简单的解决方案。当然,Runnable 只是一个简单的接口,我们可以自己定义。虽然对于线程使用场景并不太有用,但对于传递不同的“可运行代码块”,它非常完美。 - Richard Le Mesurier
这意味着我们必须创建另一个MyTimerTask或者MyThread来使用MyRunnable.... - Shrdi

28
如果run()抛出了一个已检查的异常,那么谁来捕获它呢?你无法将该run()调用放在一个处理程序中,因为你不编写调用它的代码。
你可以在run()方法中捕获你的已检查异常,并抛出一个未经检查的异常(即RuntimeException)代替。这将以堆栈跟踪的形式终止线程,也许这正是你想要的结果。
如果相反,你希望你的run()方法在某个地方报告错误,那么你只需为run()方法的catch块提供一个回调方法;该方法可以在某个位置存储异常对象,然后你感兴趣的线程就可以在那个位置找到该对象。

4
第一部分不是一个好的论点。“如果main()抛出了一个已检查的异常,那么谁会捕获它?”“如果run()抛出了一个未检查的异常,那么谁会捕获它?” - Christian Hujer

24

是的,有一种方法可以从run()方法抛出一个checked异常,但它非常糟糕,我不会分享它。

相反,您可以使用与运行时异常相同的机制来执行以下操作:

@Override
public void run() {
  try {
    /* Do your thing. */
    ...
  } catch (Exception ex) {
    Thread t = Thread.currentThread();
    t.getUncaughtExceptionHandler().uncaughtException(t, ex);
  }
}

正如其他人所指出的,如果您的run()方法确实是Thread的目标,那么抛出异常是没有意义的,因为它是不可观察的;抛出异常与不抛出异常(没有)具有相同的效果。
如果它不是Thread的目标,请不要使用Runnable。例如,也许Callable更合适。

但是当它抛出异常时,这会导致进程崩溃吗? - Dinesh
@DineshVG 不,只有JVM中的一个错误才会导致真正的崩溃。默认的异常处理程序只是打印异常。如果你习惯于在此之后看到进程退出,那是因为该线程是唯一运行的线程,并且已经终止了。 - erickson
我在Android测试用例中尝试使用这个方法进行SOAP调用,如果从SOAP调用中得到400,则抛出异常。该SOAP调用是在启动测试用例时从线程中调用的。该线程使用t.getUncaughtExceptionHandler().uncaughtException(t, ex);将其抛出到测试用例中。添加这一行代码会导致进程崩溃!不知道为什么。 - Dinesh
在那个环境中,Thread.getDefaultUncaughtExceptionHandler()返回null吗?如果不是,结果的类型是什么?如果你没有调用uncaughtException()而是将已检查的异常放入RuntimeException中并抛出会发生什么? - erickson
RuntimeException 也会导致应用程序崩溃。 - Dinesh
1
@DineshVG Android 可能正在设置一个默认的未捕获异常处理程序来执行此操作。这就是为什么我问 Thread.getDefaultUncaughtExceptionHandler() 是否返回 null;如果不是,则 Android 提供了一个默认值,以提供报告等功能。但是您可以将其设置为执行您想要的操作。更多信息请参见此处 - erickson

13
@FunctionalInterface
public interface CheckedRunnable<E extends Exception> extends Runnable {

    @Override
    default void run() throws RuntimeException {
        try {
            runThrows();
        }
        catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    }

    void runThrows() throws E;

}

5
有些人会试图说服你必须遵守规则。听取建议,但是否遵守应该基于你的情况自行决定。事实上,“你应该遵守规则”(而不是“你必须遵守规则”)。只要意识到,如果你不遵守规则,可能会有后果。
这种情况不仅适用于Runnable的情况,而且在Java 8中,在引入功能接口而没有处理已检查异常的可能性的上下文中,也经常出现。例如,ConsumerSupplierFunctionBiFunction等都声明了不能处理已检查异常的功能接口。
那么,有哪些情况和选择呢?
在下面的文本中,Runnable代表任何未声明异常或声明异常过于有限以致无法处理的功能接口。
1. 你自己声明了Runnable,可以用其他东西替换Runnable
- 考虑将Runnable替换为Callable<Void>。基本相同,但允许抛出异常;并且最终必须返回null,这是一种轻微的麻烦。 - 考虑用自己的自定义@FunctionalInterface替换Runnable,该接口可以抛出你想要的异常。
2. 你使用了一个API,有可用的替代品。例如,某些Java API被重载,因此你可以使用Callable<Void>代替Runnable
3. 你使用了一个API,没有替代品。在这种情况下,你仍然有选择。
- 你可以将异常包装在RuntimeException中。 - 通过使用未检查的转换,你可以将异常强制转换为RuntimeException。
你可以尝试以下内容。这是一种技巧,但有时我们需要的就是技巧。因为异常应该是已检查还是未检查是由其类型定义的,但实际上应该由情况来定义。
@FunctionalInterface
public interface ThrowingRunnable extends Runnable {
    @Override
    default void run() {
        try {
            tryRun();
        } catch (final Throwable t) {
            throwUnchecked(t);
        }
    }

    private static <E extends RuntimeException> void throwUnchecked(Throwable t) {
        throw (E) t;
    }

    void tryRun() throws Throwable;
}

我更喜欢使用 new Throwable(t),因为它的堆栈跟踪更短。

现在你可以这样做:

executorService.submit((ThrowingRunnable) () -> {throw new Exception()});

免责声明:在未来版本的Java中,当泛型类型信息不仅在编译时而且在运行时也被处理时,以这种方式执行未经检查的转换的能力可能会被删除。


0

是的,你可以从run()方法中抛出已检查异常。通过欺骗编译器,可以使用泛型来完成这个操作。看一下这段代码:

public static void main(String[] args) {

    new Main().throwException();

}

public void throwException() {
    Runnable runnable = () -> throwAs(new Exception());

    new Thread(runnable).start();
}

private  <T extends Throwable> void throwAs(Throwable t) throws T {
    throw ( T ) t;
}

如果你想从可运行的run()方法中抛出已检查异常,这可能会有所帮助


0

你的需求没有任何意义。如果你想通知线程调用者发生了异常,你可以通过回调机制来实现。这可以通过Handler、广播或其他你能想到的方式来实现。


0

我认为监听器模式可能会帮助您解决这种情况。如果在您的run()方法中发生异常,请使用try-catch块,在catch中发送异常事件的通知。然后处理您的通知事件。我认为这将是一种更清晰的方法。此SO链接可以为您提供有用的指针。


-1

最简单的方法是定义自己的异常对象,它继承了RuntimeException类而不是Exception类。


当这个RuntimeException发生时,你如何获取它,亲爱的先生? - Dormouse
那仅仅不能回答这个问题。 - Kalle Richter

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