针对异常的Java接口的正确设计

时间:2013-11-30 20:28:04

标签: java exception interface

我试图理解如何在语言(和运行时)中如何处理异常来正确定义Java中的接口。

注意:也许这个问题已经被问及并得到了充分的回答,但是我对SO(和其他网站)的搜索并没有找到任何直接解决设计问题的问题以及我提出的权衡问题。如果可以找到回答我问题的类似问题,我会很乐意删除这个问题。我为这个冗长的问题提前道歉,但我想尽可能清楚地说明问题。我也知道controversy around checked exceptions,但由于我无法改变语言并且仍然必须在其中工作,我想选择会导致的方法从长远来看,疼痛最小。

我遇到的反复出现的问题是,当我尝试定义自己的接口时,我面临着如何指定允许接口成员使用哪些(如果有)异常的选择。麻烦的是我无法知道哪些异常是有意义的,因为我事先并不知道我的界面的所有可能实现可能是什么。为了举个例子,假设我们有以下接口:

// purely hypothetical interface meant to illustrate the example...
public interface SegmentPartition {
    // option 1
    bool hasSegment(Segment s);
    // option 2
    void addSegment(Segment s) throws Exception;
    // option 3
    bool tryAddSegment(Segment s) throws TimeoutException, IOException, ParseException, XmlException, SQLException;
    // option 4
    void optimizeSegments() throws SegmentPartitionException;
}

此接口应预先声明哪些异常类型?我看到的选项是:

  1. 不要声明任何异常,实现只能抛出RuntimeException'
  2. 将每个方法声明为抛出Exception
  3. 尝试预测对此接口有意义的每种可能的异常类型,并将它们声明为可抛出的异常。
  4. 创建我自己的异常类型(例如SegmentException)并要求根据需要抛出内部异常以获取更多详细信息。
  5. 这些方法都存在问题,并且不完全清楚所有权衡可能是什么,我已经针对每种情况提到了一些。理想情况下,我不希望实施引发任何例外情况,但这并不总是一个实际的限制。

    对于选项1 ,问题是许多可能抛出的异常类型不是从RuntimeException派生的(例如IOException或TimeoutException)。当然,我可以将这些特定的异常添加到接口,然后允许抛出其他RuntimeExceptions,但那么用户定义的异常呢?这似乎是一个糟糕的选择。

    使用选项2 ,我们最终得到一个界面,看起来它可能导致任何异常(我认为是真的),并且没有提供实际应该抛出的实现者的指导。界面不再是自我记录。更糟糕的是,此接口的每个调用站点必须声明为抛出异常或将每个调用包装到try{} catch(Exception)中的此接口的方法,或者创建一个捕获异常的块并尝试判断它是否被抛出接口实现或该块中的其他内容。呸。或者,使用此接口的代码可以捕获实现可能抛出的特定异常,但是它违反了以接口开头的目的(因为它需要调用者知道实现的运行时类型)。在实践中,这是最接近我所知道的关于异常(C ++,C#,Scala等)的大多数其他语言的选项。

    使用选项3 我必须特别预测可以为我的界面定义的潜在实现,并声明最适合它们的异常。因此,如果我预计我的接口可能通过远程,不可靠的连接实现,我会添加TimeoutException,如果我预计它可能使用外部存储实现,我会包含IOException,依此类推。我几乎可以肯定会弄错...这意味着我将继续搅拌界面,因为新的实现范式浮出水面(当然,它会破坏所有现有的实现并要求它们将非敏感的异常声明为抛出)。这种方法的另一个问题是导致丑陋,重复的代码...特别是对于具有许多方法的大型接口(是的,有时需要这些),然后需要在每个实现点重复。

    使用选项4 我必须创建自己的异常类型,以及随之而来的所有行李(可序列化,嵌套和内部异常,提出合理的层次结构等)。这里不可避免地发生的事情是你最终为每种接口类型都有一个异常类型:InterfaceFoo + InterfaceFooException。除了需要的代码膨胀之外,真正的问题是异常类型并不代表任何东西......它只是一种用于将特定接口的所有错误条件捆绑在一起的类型。在某些情况下,也许您可​​以创建一些异常类型的子类来指示特定的失败模式,但即使这样也是有问题的。

    那么在这里做什么是正确的呢?我意识到可能没有一个削减和干燥的规则,但我对Java开发人员选择的经验感到好奇。

    非常感谢。

3 个答案:

答案 0 :(得分:1)

在接口中使用throws Exception(它是一个已检查的异常)意味着您知道在什么条件下应该抛出这样一个已检查的异常。这意味着接口已经具有一些实现的知识。不应该是这种情况。因此,我会反对界面中的任何throws Exception。如果接口抛出一个已检查的Exception,我建议将其简化,将异常逻辑留给实现。

答案 1 :(得分:1)

一个可能的简单答案是 - 应该声明那些可以并且通常将在执行期间处理的异常 - 即在异常之后传播,记录或退出。除了出错之外,我认为异常是无条件返回的结果类型。

如果接口将在内部使用,即在数据库中使用,一种可能的方法是抛出您专门处理/正在处理的异常。

如果它是公共图书馆,即ExecutorService,则可能需要进行一些深入分析。

示例 - 来自ExecutorService的JavaDoc:

 /**
 * ...
 * @return the result returned by one of the tasks
 * @throws InterruptedException if interrupted while waiting
 * @throws TimeoutException if the given timeout elapses before
 *         any task successfully completes
 * @throws ExecutionException if no task successfully completes
 * @throws RejectedExecutionException if tasks cannot be scheduled
 *         for execution
 */
<T> T invokeAny(Collection<? extends Callable<T>> tasks,
                long timeout, TimeUnit unit)
    throws InterruptedException, ExecutionException, TimeoutException;

所有这些例外都是针对特定领域的,可以根据其一般含义做出决定。因此,您只能在invokeAny方法的上下文中猜测它们的含义,只给出了此方法的描述。

这意味着可能需要一些原型实现来了解您的域是什么,典型的用例是什么以及可以抛出哪些典型错误。我不知道Java库是如何设计的,在我的例子中,我为我的原型实现做了很多重构,通过这样做我理解了域。

更多要点:

  • Java的库记录了可以抛出的运行时异常,因此完全可以使用它们。
  • 因此,如果您忘记其中一个案例,实现类可能会抛出特定于域的运行时异常。
  • 可以记录将未声明的异常包装到其中一个返回的类型异常中,即转换为ExecutionException - 这就是java.util.concurrent的完成方式。

答案 2 :(得分:0)

一般情况下?根据我的经验,考虑到工程问题的复杂性,并且考虑到在给定工程环境中可用的选项,最优先考虑的答案是最优先的。

优雅是一个意见问题。然而,在添加时抛出自定义SegmentException似乎是最优雅的(如果这是可能的话),并尝试最小化异常的可能性。