Java: 异常作为控制流吗?

13

我听说使用异常来控制程序流程是不好的做法。你对此有什么看法?

public static findStringMatch(g0, g1) {

    int g0Left = -1;
    int g0Right = -1;
    int g1Left = -1;
    int g1Right = -1;

//if a match is found, set the above ints to the proper indices
//...
//if not, the ints remain -1

        try {
            String gL0 = g0.substring(0, g0Left);
            String gL1 = g1.substring(0, g1Left);

            String g0match = g0.substring(g0Left, g0Right);
            String g1match = g1.substring(g1Left, g1Right);

            String gR0 = g0.substring(g0Right);
            String gR1 = g1.substring(g1Right);

            return new StringMatch(gL0, gR0, g0match, g1match, gL1, gR1);
        }
        catch (StringIndexOutOfBoundsException e) {
            return new StringMatch(); //no match found
        }

如果没有找到匹配项,那么ints的值将为-1。当我尝试取子字符串g0.substring(0, -1)时,这会导致异常。然后该函数将返回一个指示未找到匹配项的对象。

这是不好的做法吗?我可以手动检查每个索引是否都是-1,但感觉这样更麻烦。

更新

我已经删除了try-catch块并使用以下代码进行替换:

    if (g0Left == -1 || g0Right == -1 || g1Left == -1 || g1Right == -1) {
        return new StringMatch();
    }

哪种方法更好:检查每个变量是否为-1,还是使用布尔型变量foundMatch来跟踪并在最后检查它?

4个回答

19

一般来说,异常操作是昂贵的操作,并且通常发生在异常情况下。 因此,在控制应用程序流程的上下文中使用它们确实被认为是不好的做法。

具体来说,在您提供的示例中,您需要对提供给StringMatch构造函数的输入进行一些基本验证。 如果它是一个返回错误代码以表示一些基本参数验证失败的方法,则可以避免事先检查,但这并非总是如此。


7
+1:同意。乔舒亚·布洛赫在《Effective Java》一书中也提出了相同的建议。最好先明确测试您的参数。 - Jim Ferrans
1
请参阅《Effective Java》第9章中Bloch的第57条建议。 - Jim Ferrans
同意,但是我当时试图运行服务器套接字时,超时是通过异常编码的,因此您会在示例代码中看到部分内容,但正确的代码是要在代码中寻找匹配项(或不匹配),以便将异常用于异常情况,例如“基本参数验证失败”或空指针等。 - Nicholas Jordan
@Nicholas,这显然是一个设计决策。连接超时在套接字通信中是异常情况,即使在您的服务器应用程序中可能很常见。 - Yannick Motton

10

我已经对此进行了一些测试。在现代JVM上,它实际上并不会对运行时性能产生太大影响(如果有的话)。但如果您开启了调试模式,则会显著减慢速度。

有关详细信息,请参见以下内容

(我也应该提到,即使它不会影响性能,我仍然认为这是一种不好的做法。最重要的是,它反映了可能存在的较差算法设计,这将很难进行测试)


6
程序流程应尽可能呈直线状态(即使应用程序变得非常复杂),并利用标准控制流结构。下一个开发人员可能不是你,他会(正确地)误解你使用异常而不是条件语句来确定控制流的非标准方法。
我正在进行一些旧代码重构,对此问题有稍微不同的看法。
我发现这种方法最大的问题是使用try/catch打破了正常的编程流程。
在我正在处理的应用程序中(这与您应用的示例不同),异常被用于在方法调用内部传递某个结果(例如查找帐户号码并未找到)。这会在客户端代码上创建意大利面条式代码,因为调用方法(在非异常事件或正常使用情况下)会从之前执行的任何代码中跳出,并进入catch块。这在一些非常长的方法中多次重复出现,使得代码很容易被误读。
对于我的情况,除了真正的异常事件外,方法应按其签名返回一个值。异常处理机制旨在在异常发生时采取另一条路径(尝试在方法内部恢复,以便仍然可以正常返回)。
在我看来,如果您非常紧密地限定try/catch块的范围,就可以做到这一点;但我认为这是一个不好的习惯,并且可能导致代码非常容易被误解,因为调用代码会将任何抛出的异常都解释为“GOTO”类型的消息,从而改变程序流程。我担心虽然这种情况没有陷入这种陷阱,但经常这样做可能会导致编码习惯,从而导致我正在经历的噩梦。
而那个噩梦并不愉快。

不确定是否非常相似,但我依赖于第三方库的代码,通过try catch来封装/针对我自己的代码以抑制我知道的专有异常,我没有意识到第三方FTP正在发出重要的FTP中断消息,而不是返回不同的结果,所以我的日志中没有任何记录/被抑制。也许我可以只压制每个项目继承的自定义消息。 - FantomX1

6

是的,这是一种不好的做法,特别是当你有一种可以避免异常的方法时(在尝试索引之前检查字符串长度)。尝试和捕获块的设计目的是将“正常”逻辑与“异常”和错误逻辑分开。在你的示例中,你已经将“正常”逻辑散布到了异常/错误块中(找不到匹配并不是异常情况)。你还误用了substring,以便利用它产生的错误作为控制流。


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