在无法到达的代码中抛出新的RuntimeExceptions是否不好?


31

我被指派去维护一个由更熟练的开发人员编写的应用程序。我遇到了这段代码:

public Configuration retrieveUserMailConfiguration(Long id) throws MailException {
        try {
            return translate(mailManagementService.retrieveUserMailConfiguration(id));
        } catch (Exception e) {
            rethrow(e);
        }
        throw new RuntimeException("cannot reach here");
    }

我很好奇投掷RuntimeException("cannot reach here")是否合理。知道这段代码来自经验丰富的同事,我可能会漏掉一些明显的东西。
编辑:这是一些答案所指的扔尸体。我认为这个问题并不重要。

private void rethrow(Exception e) throws MailException {
        if (e instanceof InvalidDataException) {
            InvalidDataException ex = (InvalidDataException) e;
            rethrow(ex);
        }
        if (e instanceof EntityAlreadyExistsException) {
            EntityAlreadyExistsException ex = (EntityAlreadyExistsException) e;
            rethrow(ex);
        }
        if (e instanceof EntityNotFoundException) {
            EntityNotFoundException ex = (EntityNotFoundException) e;
            rethrow(ex);
        }
        if (e instanceof NoPermissionException) {
            NoPermissionException ex = (NoPermissionException) e;
            rethrow(ex);
        }
        if (e instanceof ServiceUnavailableException) {
            ServiceUnavailableException ex = (ServiceUnavailableException) e;
            rethrow(ex);
        }
        LOG.error("internal error, original exception", e);
        throw new MailUnexpectedException();
    }


private void rethrow(ServiceUnavailableException e) throws
            MailServiceUnavailableException {
        throw new MailServiceUnavailableException();
    }

private void rethrow(NoPermissionException e) throws PersonNotAuthorizedException {
    throw new PersonNotAuthorizedException();
}

private void rethrow(InvalidDataException e) throws
        MailInvalidIdException, MailLoginNotAvailableException,
        MailInvalidLoginException, MailInvalidPasswordException,
        MailInvalidEmailException {
    switch (e.getDetail()) {
        case ID_INVALID:
            throw new MailInvalidIdException();
        case LOGIN_INVALID:
            throw new MailInvalidLoginException();
        case LOGIN_NOT_ALLOWED:
            throw new MailLoginNotAvailableException();
        case PASSWORD_INVALID:
            throw new MailInvalidPasswordException();
        case EMAIL_INVALID:
            throw new MailInvalidEmailException();
    }
}

private void rethrow(EntityAlreadyExistsException e)
        throws MailLoginNotAvailableException, MailEmailAddressAlreadyForwardedToException {
    switch (e.getDetail()) {
        case LOGIN_ALREADY_TAKEN:
            throw new MailLoginNotAvailableException();
        case EMAIL_ADDRESS_ALREADY_FORWARDED_TO:
            throw new MailEmailAddressAlreadyForwardedToException();
    }
}

private void rethrow(EntityNotFoundException e) throws
        MailAccountNotCreatedException,
        MailAliasNotCreatedException {
    switch (e.getDetail()) {
        case ACCOUNT_NOT_FOUND:
            throw new MailAccountNotCreatedException();
        case ALIAS_NOT_FOUND:
            throw new MailAliasNotCreatedException();
    }
}

12
如果rethrow实际上没有throw发生异常,则从技术上讲是可以实现的。(如果实现发生更改,则有一天可能会发生)
njzk2

34
顺便说一句,AssertionError在语义上可能比RuntimeException更好。
JvR 2015年

1
最后一掷是多余的。如果启用findbugs或其他静态分析工具,它将标记这些类型的行以进行删除。
Bon Ami 2015年

7
现在,我看到了其余的代码,我在想,也许您应该将“技能更强的开发人员”更改为“更高端的开发人员”。代码审查很乐于解释原因。
JvR

3
我试图创建假设示例,说明异常为何如此可怕。但是我从来没有做过这样的事情。
Paul Draper 2015年

Answers:


36

首先,感谢您对问题的回答,并向我们展示了rethrow它的作用。因此,实际上,它的作用是将具有属性的异常转换为更细粒度的异常类。稍后再详细介绍。

因为我最初并没有真正回答主要问题,所以可以解决:是的,在无法访问的代码中抛出运行时异常通常是不好的风格;您最好使用断言,甚至更好地避免问题。正如已经指出的,这里的编译器不能确保代码永远不会走出代码try/catch块。您可以利用以下优势来重构代码:

错误就是价值

(毫不奇怪,它是众所周知的

让我们使用一个更简单的示例,即我在编辑之前使用的示例:想象一下您正在记录某些内容并构建一个包装异常,例如Konrad的answer。叫它logAndWrap

可以将异常作为副作用进行处理logAndWrap,而不是将异常作为的副作用抛出,并使其返回异常(至少是输入中给出的异常)。您不需要使用泛型,只需使用基本功能即可:

private Exception logAndWrap(Exception exception) {
    // or whatever it actually does
    Log.e("Ouch! " + exception.getMessage());
    return new CustomWrapperException(exception);
}

然后,您throw显式地使编译器满意:

try {
     return translate(mailManagementService.retrieveUserMailConfiguration(id));
} catch (Exception e) {
     throw logAndWrap(e);
}

如果您忘记了该throw怎么办?

正如上文Joe23的评论,一个防御性编程的方式来确保异常始终抛出就在于明确地做一个throw new CustomWrapperException(exception)在年底logAndWrap,因为它是由做Guava.Throwables。这样,您就知道将引发异常,并且类型分析器很高兴。但是,您的自定义异常需要是未检查的异常,这并不总是可能的。另外,我将开发人员丢失编写的风险评估为throw非常低:开发人员必须忘记它并且周围的方法不应返回任何内容,否则编译器将检测到丢失的返回。这是对抗类型系统的一种有趣方式,但是它确实有效。

重新投掷

实际rethrow也可以编写为一个函数,但是我目前的实现存在问题:

  • 有很多无用的演员,例如 实际上需要强制转换(请参阅评论):

    if (e instanceof ServiceUnavailableException) {
        ServiceUnavailableException ex = (ServiceUnavailableException) e;
        rethrow(ex);
    }
  • 当抛出/返回新的异常时,旧的异常被丢弃;在以下代码中,a MailLoginNotAvailableException不允许我知道哪个登录不可用,这很不方便;此外,堆栈跟踪将不完整:

    private void rethrow(EntityAlreadyExistsException e)
        throws MailLoginNotAvailableException, MailEmailAddressAlreadyForwardedToException {
        switch (e.getDetail()) {
            case LOGIN_ALREADY_TAKEN:
                throw new MailLoginNotAvailableException();
            case EMAIL_ADDRESS_ALREADY_FORWARDED_TO:
                throw new MailEmailAddressAlreadyForwardedToException();
        }
    }
  • 为什么原始代码不首先抛出那些专门的异常?我怀疑这rethrow被用作(邮件)子系统和业务逻辑之间的兼容层(也许目的是通过用自定义异常替换它们来隐藏诸如抛出的异常之类的实现细节)。即使我同意像Pete Becker的回答中所建议的那样具有更好的捕获性代码会更好,但我认为如果不进行重大重构,您将没有机会在此处删除catchrethrow代码。


1
演员不是没有用的。它们是必需的,因为没有基于参数类型的动态分配。必须在编译时知道参数类型,才能调用正确的方法。
乔8:51

在您的logAndWrap方法中,您可以抛出异常而不是返回异常,但是保留返回类型(Runtime)Exception。这样,catch块可以保持您编写它的方式(不会抛出无法到达的代码)。但是它还有一个额外的优点,那就是您不能忘记此功能throw。如果您返回异常而忘记了抛出,则将导致异常被默默吞下。具有返回类型RuntimeException时抛出该异常将使编译器满意,同时还避免了这些错误。这就是Guava Throwables.propagate的实现方式。
2015年

@ Joe23感谢您对静态分派的澄清。关于在函数内部抛出异常:您是说所描述的可能错误是一个catch块,logAndWrap但该异常会调用,但返回的异常不执行任何操作?那很有意思。在必须返回值的函数内部,编译器会注意到有一条路径没有返回值,但这对于不返回任何内容的方法很有用。再次感谢。
coredump

1
我就是这么想的。只是要再次强调这一点:这不是我想出并值得称赞的东西,而是从Guava资料中收集的。
2015年

@ Joe23我添加了对库的明确引用
coredump

52

rethrow(e);函数违反了以下原则:在正常情况下,一个函数将返回,而在特殊情况下,一个函数将引发异常。此功能在正常情况下会引发异常,从而违反了该原理。这就是所有困惑的根源。

编译器假定此函数将在正常情况下返回,因此,就编译器所知,执行可能会到达retrieveUserMailConfiguration函数的末尾,此时,没有return语句是错误的。该RuntimeException抛出那里应该减轻编译器的这个问题,但它是这样做的一个非常笨拙的方法。防止function must return a value错误的另一种方法是添加一条return null; //to keep the compiler happy语句,但是在我看来,这同样笨拙。

因此,就我个人而言,我将替换为:

rethrow(e);

有了这个:

report(e); 
throw e;

或者更好的是(如coredump建议的那样):

throw reportAndTransform(e);

因此,控制流对于编译器throw new RuntimeException("cannot reach here");将变得显而易见,因此最终编译器不仅变得多余,而且实际上甚至不可编译,因为编译器会将其标记为不可访问的代码。

这是摆脱这种丑陋状况的最优雅,实际上也是最简单的方法。


1
-1建议将函数拆分为两个语句,这可能由于不良的复制/粘贴而导致编程错误;我也略有担心,您编辑你的问题建议的事实throw reportAndTransform(e)两分钟,我贴我自己的答案后。也许您独立地修改了自己的问题,如果是这样的话,我会提前道歉,否则,通常人们会为此付出一点荣誉。
coredump

4
@coredump我确实阅读了您的建议,甚至阅读了另一个将该建议归因于您的答案,并且我记得我实际上实际上已经采用了这样的构造,所以我决定将其包含在我的答案中。我认为添加归因并不重要,因为在这里我们不是在讨论原始想法,但是如果您对此表示怀疑,那么我不想让您感到烦恼,因此归因现在就在这里,完整的链接。干杯。
Mike Nakis

这并不是说我拥有这种结构的专利,这很普遍。但是想象一下您写了一个答案,不久之后,它又被另一个“同化”了。因此,归因非常感激。谢谢。
coredump

3
没有错 没有返回的函数本身。它一直在发生,例如在实时系统中。最终的问题是Java中的一个缺陷,即该语言分析并增强了语言的正确性概念,但是该语言没有提供某种方式来注释函数不返回的机制。但是,有些设计模式可以解决此问题,例如 throw reportAndTransform(e)。许多设计模式都是补丁,缺少语言功能。这就是其中之一。
大卫·哈门

2
另一方面,许多现代语言足够复杂。将每种设计模式变成一种核心语言功能,将会使它们进一步膨胀。这是不属于核心语言的设计模式的一个很好的例子。如所写,不仅编译器而且很多其他必须解析代码的工具都很好地理解了它。
MSalters

7

throw可能已添加,以绕过“方法必须返回一个值”可能发生的错误-在Java数据流分析仪是足够聪明,明白,没有return必要经过throw,但没有你的自定义后rethrow()的方法,并没有@NoReturn标注您可以用来解决此问题。

但是,在无法访问的代码中创建新的Exception似乎是多余的。我会简单地写return null,知道它确实不会发生。


你是对的。我检查了注释throw new...行会发生什么,并且抱怨没有返回声明。编程不好吗?是否有关于取代无法到达的退货声明的约定?我将在一段时间内不回答这个问题,以鼓励更多的投入。不过,感谢您的回答。
Sok Pomaranczowy,2015年

3
@SokPomaranczowy是的,这是不好的编程。该rethrow()方法掩盖了事实,catch即抛出了异常。我会避免这样做。另外,当然,rethrow()实际上可能无法重新抛出异常,在这种情况下,还会发生其他情况。
罗斯·帕特森

26
请不要将替换为return null。除异常外,如果将来有人破坏了代码,因此最后一行达到了某些可达性,则抛出异常时将立即清除。如果改为return null,则null您的应用程序中会有一个意料之外的值。谁知道应用程序中NullPointerException会出现什么?除非您很幸运,而且此函数刚好在调用此函数之后,否则将很难找到真正的问题。
unholysampler 2015年

5
@MatthewRead执行旨在无法访问的代码是一个严重的错误,return null很可能掩盖该错误,因为您无法区分其他情况是否会null从此错误中返回。
CodesInChaos

4
此外,如果函数在某些情况下已经返回了null,并且在这种情况下您还使用了null返回,那么用户现在有两种可能的情况,即他们在返回null时可能会处于这种情况。有时这无关紧要,因为null返回值已经意味着“没有对象,并且我没有指定原因”,因此添加一个可能的原因之所以没有什么区别。但是经常增加歧义性是不好的,这肯定意味着错误最初将被视为“某些情况”,而不是立即看起来像“已被破坏”。早点失败。
史蒂夫·杰索普

6

throw new RuntimeException("cannot reach here");

陈述清楚地表明 PERSON读书是怎么回事的代码,所以是好多了,然后例如返回null。如果以意外方式更改了代码,这也使调试变得更容易。

但是rethrow(e)只是看起来错了!因此,就您而言,我认为重构代码是一个更好的选择。有关整理代码的方法,请参见其他答案(我最喜欢coredump)。


4

我不知道有没有约定。

无论如何,另一个技巧就是这样做:

private <T> T rethrow(Exception exception) {
    // or whatever it actually does
    Log.e("Ouch! " + exception.getMessage());
    throw new CustomWrapperException(exception);
}

考虑到这一点:

try {
     return translate(mailManagementService.retrieveUserMailConfiguration(id));
} catch (Exception e) {
     return rethrow(e);
}

RuntimeException不再需要人工操作。即使rethrow从不实际返回任何值,对于现在的编译器来说已经足够了。

它确实会返回一个理论上的值(方法签名),然后实际上将其免除,因为它引发了异常。

是的,看起来可能很奇怪,但是再说一次-抛出一个幻影RuntimeException,或者返回在这个世界上永远不会出现的null-也不完全是美。

为了提高可读性,您可以重命名rethrow并添加以下内容:

} catch (Exception e) {
     return nothingJustRethrow(e);
}

2

如果您try完全删除了该块,则不需要rethrowthrow。此代码与原始代码完全相同:

public Configuration retrieveUserMailConfiguration(Long id) throws MailException {
    return translate(mailManagementService.retrieveUserMailConfiguration(id));
}

不要让它来自经验丰富的开发人员的事实欺骗您。这是代码腐烂,并且一直在发生。修复它。

编辑:我误读rethrow(e)为只是重新引发异常e。如果该rethrow方法实际上除了重新引发异常之外还执行其他操作,则将其删除确实会更改此方法的语义。


人们将继续相信,不处理任何内容的通配符捕获实际上是异常处理程序。您的版本不假装处理无法做到的事情,从而忽略了这个错误的假设。RetrieveUserMailConfiguration()的调用者希望得到一些回报。如果该函数不能返回合理的值,则它应该快速响亮地失败。好像其他答案都没有问题,catch(Exception)这是可恶的代码气味。
msw

1
@msw-货物崇拜编码。
Pete Becker 2015年

@msw-另一方面,它确实为您提供了设置断点的位置。
皮特·贝克尔

1
就好像您看不到放弃整个逻辑的问题一样。您的版本显然与原始版本没有相同的功能,顺便说一句,该版本已经快速大声地失败了。我宁愿在回答中编写像这样的免捕获方法,但是在使用现有代码时,我们不应与其他工作部分破坏合同。可能是所做的事情rethrow没有用(这里不是重点),但是您不能只是摆脱不喜欢的代码,而在显然不想要的时候说它与原始代码等效。自定义rethrow函数有副作用。
coredump

1
@ jpmc26问题的核心是关于“无法到达此处”异常,除了前面的try / catch块以外,它还可能由于其他原因而出现。但是我同意该程序块闻起来很腥,如果最初用这个措词措辞更仔细,它会获得更多的赞成票(顺便说一句,我没有赞成票)。最后的编辑是好的。
coredump
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.