Java:声明可以抛出异常的接口方法的正确方式是什么?

6

假设我有一个接口A,由多个供应商实现:

interface A
{
    void x();
    void y();
}

然而,我希望供应商能够抛出异常以表明某些事情已经失败,并且可能该方法会抛出RuntimeException。在每种情况下,调用这些方法的代码都应该处理失败并继续执行。仅仅因为一个供应商抛出了NPE,我不想让系统崩溃。我想确保每个调用都能通过声明每个方法来捕获所有异常:

void x() throws Exception;

但这通常是不好的做法(PMD不喜欢它,通常我也同意具体方法的规则),所以我想知道这是否是规则的例外还是有更好的方法?
让我明确一下,我正在寻找一种解决方案,其中接口的调用者被强制处理所有异常(包括RuntimeException)。
进一步详细说明我的环境,所有这些都在OSGi框架内运行。因此,每个供应商将其代码打包成一个捆绑包,并且OSGi将处理所有异常以防止整个系统崩溃。我真正关注的是将被某些核心捆绑包调用的OSGi服务接口。我想要确保当我遍历所有服务时,一个服务不会抛出NPE并停止执行的过程。我希望通过捕获从服务中抛出的所有异常来更优雅地处理它,以便仍然可以管理其他提供的服务。

这个问题不仅仅适用于接口,对于抽象方法甚至是可能被覆盖的方法实现也同样适用。 - Ted Hopp
如果这个方法正在执行“某些操作”,那么这个操作是否如此广泛,以至于可能会抛出任何异常? - blank
很难限制异常集合,所以是的。 - Dave H
7个回答

9
创建自己的异常类,例如MySeviceException并从接口中抛出。这里的想法是抛出有意义的异常,因此如果这样做可以提高可读性和可维护性,请不要害怕创建许多自定义异常类。您可以在下游捕获供应商详细的异常,并将它们包装为自定义异常,以便上游流程不必处理供应商特定的异常。
class MySeviceException extends Exception{
    public MySeviceException() {}  
    public MySeviceException(String msg) { super(msg); }  
    public MySeviceException(Throwable cause) { super(cause); }  
    public MySeviceException(String msg, Throwable cause) { super(msg, cause); } 
}

interface A
{
    void x() throws MySeviceExceptionException;
    void y() throws MySeviceExceptionException;
}

作为一个经验法则,永远不要捕获 Errors,而是始终捕获 Exceptions 并处理它!

我喜欢这个想法,但它并没有强制调用者检查RuntimeExceptions。也许我只需要声明同时抛出两者。 - Dave H
4
@Dave H - 你不能(也不应该尝试)强制调用者检查 RuntimeException,这是未经检查的异常的本质。 - Robin

2

供应商可以随心所欲地抛出RuntimeException,因为它们是未经检查的。这意味着您不必在接口中添加throws子句。

这也意味着客户端不会从接口定义中得到任何警告,并且编译器不会强制执行所需的try/catch。他们唯一知道的方法是阅读您的Javadoc。

您可以创建一个扩展java.lang.Exception的自定义异常;这是一个已检查的异常。您将不得不将其添加到throws子句中;编译器将强制在这些方法周围使用try/catch;您的客户端将不得不处理问题。


我正在寻找更多编译器可强制执行的东西,而不是依赖于Javadocs。 - Dave H
他说这并不重要。再次说明,他们可以随时抛出Runtime异常,编译器不会强制使用try/catch。即使在CoolBeans的解决方案中,这是最好的情况,也不会强制供应商将其异常包装在MyServiceException中。这是Java的局限性。如果允许供应商构建可插拔模块,则应协商一项理解,即他们的代码不能引发框架致命异常,并且您的QA过程应确保符合要求。 - nsfyn55
除此之外,您可以将所有服务调用都包装在try{}catch(Exception e)中。 - nsfyn55

1
你可以在一个抽象类中实现你的接口,该抽象类使用模板方法模式来捕获和包装RuntimeExceptions,并使用自定义异常。然而,除了文档之外,没有办法强制厂商使用抽象类。
class MySeviceException extends Exception{
    public MySeviceException() {}  
    public MySeviceException(String msg) { super(msg); }  
    public MySeviceException(Throwable cause) { super(cause); }  
    public MySeviceException(String msg, Throwable cause) { super(msg, cause); } 
}

interface A
{
    void x() throws MySeviceExceptionException;
    void y() throws MySeviceExceptionException;
}

class ABase implements A
{
    public final void x() throws MySeviceExceptionException {
        try {
            doX();
        } catch(RuntimeException ex) {
            throw new MySeviceExceptionException(ex);
        }
    }
    public final void y() throws MySeviceExceptionException {
        try {
            doY();
        } catch(RuntimeException ex) {
            throw new MySeviceExceptionException(ex);
        }
    }

    public abstract void doX() throws MySeviceExceptionException;
    public abstract void doY() throws MySeviceExceptionException;
}

1

我会避免在接口中放置异常抛出,因为这会对实现者造成限制。如果我必须通过实现你的接口来抛出异常,那么这将使得测试时的模拟更加困难。

如果在实现你的接口的代码中出现了问题,那应该是实现类程序员的责任。


仅仅因为接口声明了一个异常被抛出,并不意味着实现必须抛出相同的异常。它可以始终抛出较少的异常。此外,供应商并不总是能够处理如果出现问题,可能需要通知调用者出现了错误。 - Dave H

1

创建一个throws子句,即使使用自定义异常类型,也不是真正的答案。你总是会遇到像NPE这样的问题。即使您指定所有异常都必须包装在您的自定义异常类型中,也会有一些情况下出现错误,导致NPE通过。如果没有错误,就不会有NPE。

在许多情况下,添加"throws Exception"是一个坏主意,但在某些情况下它是可行的。在像Struts和JUnit这样的框架中,您可以添加"throws Exception"而不会出现问题,因为该框架允许这样做。(如果第一个抛出的异常使测试套件停止,JUnit将没有什么用处。)

您可以设计系统,使供应商代码的调用发生在特定模块中,这些模块被插入到系统中,并且由系统处理异常处理,类似于Struts中的操作方式。每个操作都可以抛出任何内容,都有一个异常处理程序记录了出错信息。这样,调用供应商API的业务逻辑就不必被无用的异常捕获样板所困扰,但如果出现问题,程序也不会退出。


我已经编辑了原始问题。我正在使用OSGi,因此应用程序实际上并没有退出,更多的是由于一个坏苹果而中断了当前操作。 - Dave H

1

我同意PMD的观点 - 声明抛出通用的Exception是不好的。我认为最好的做法是:

  • 定义新的、特定于领域的非运行时异常类(如果内置的异常类不能满足需求;如果匹配它们的语义,始终坚持预定义的类),并在可能抛出它们的方法中声明它们。
  • 尽量减少运行时异常。只有当你无法控制如何调用你时,什么都不会发生时,它们才有意义。如果你有任何理由期望它们在你调用的代码片段中被抛出,那么尽早捕获它们,并将它们重新抛出为标准异常(参见上面的点)。

Java的最新版本允许您链接异常。例如,如果您在使用外部库解析File f时遇到NullPointerException ex,您需要捕获它并重新抛出它,比如new FileParsingException("error parsing " + f + ": " + ex.getMessage(), ex);


抱歉,但这太糟糕了,你增加了异常的数量,最终那个自定义异常并没有告诉你实际出了什么问题,所以你必须进行考古挖掘来查找实际原因,而抛出异常会立即告诉你出了什么问题... - Enerccio
我强烈不同意。包装异常 处理这种情况的推荐方式(正如被接受的答案所倡导的那样,该答案写得更好,但与此答案非常相似);因为包装可以添加其他不可用的上下文。在这个答案的例子中,未包装的 NPE 如何帮助您诊断破坏解析器的格式错误文件呢? - tucuxi

1

根据您的JVM设置,任何方法都有可能抛出RuntimeException,无论您是否将其声明为抛出异常。捕获/处理RuntimeException通常是一种不好的做法。虽然在某些有限的情况下可能需要这种行为,但RuntimeException主要是指代码存在问题,而不是产品使用方式的指示器。当然,捕获RuntimeException的一个主要缺点(特别是如果您忽略它们)是系统可能会崩溃,而您却不知道发生了什么...然后突然间,您的系统会输出完全无效的数据,或者因为其他原因而崩溃,使得跟踪根本原因更加困难。

请参阅Sun/Oracle关于异常的教程

http://download.oracle.com/javase/tutorial/essential/exceptions/runtime.html

回答这个问题,除非你确切地知道你可以期望抛出什么异常衍生类,否则你基本上只能抛出 Exception,虽然我对抛出通用的 Exception 类的有用性表示怀疑,除非你只关心记录堆栈跟踪等信息以便知道发生了什么?如果你试图在不知道是什么类型的异常的情况下强化处理异常,那么你可能不会处理它得很好或者足够好。

这更多是确保供应商捆绑包不会阻止系统运行。这是一个插件框架,可以根据需要添加来自多个供应商的插件。 - Dave H

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