为什么编译器允许在方法永远不会抛出异常的情况下使用throws关键字?

19

我想知道为什么Java编译器允许在方法声明中使用throws,即使该方法永远不会抛出异常。因为"throws"是处理异常的一种方式(告诉调用者去处理它)。

由于处理异常有两种方式(throws和try/catch)。在try/catch中,它不允许捕获try块中未抛出的异常,但是它允许在可能不会抛出异常的方法中使用throws。

private static void methodA() {
    try {
        // Do something
        // No IO operation here
    } catch (IOException ex) {  //This line does not compile because
                              //exception is never thrown from try
        // Handle   
    }
}

private static void methodB() throws IOException { //Why does this //compile when excetion is never thrown in function body
    //Do Something 
    //No IO operation
}

3
简而言之:这是一个设计选择,并没有对错之分。 - IsaacLevon
1
由于我们不是该语言的设计者,“为什么”问题除了基于猜测和个人意见的答案外,很难回答。 - RealSkeptic
我也觉得术语“throws”有些误导。现在我在脑海中将其解析为“could_throw”,它似乎更符合其行为。 - Eric Duminil
4个回答

27
throws子句是方法的一部分合约。它要求方法的调用者表现得好像指定的异常可能会被该方法抛出(即捕获异常或声明自己的throws子句)。
有可能初始版本的方法不会抛出throws子句中指定的异常,但将来的版本可能会抛出它,而不会破坏API(即任何调用该方法的现有代码仍将通过编译)。
相反的情况也可能发生。如果该方法曾经抛出throws子句中指定的异常,但将来的版本不再抛出它,您应该保留throws子句,以不破坏使用您方法的现有代码。
第一个例子:
假设您有以下使用methodB的代码:
private static void methodA() {
    methodB(); // doesn't have throws IOException clause yet
}

如果以后您想要将methodB更改为抛出IOException,那么methodA将停止通过编译。
第二个例子:
假设您有这段代码使用了methodB:
private static void methodA() {
    try {
        methodB(); // throws IOException
    }
    catch (IOException ex) {

    }
}

如果您从methodB的未来版本中删除throws条款,则methodA将无法通过编译。当methodAprivate时,此示例并不是非常有趣,因为它只能在本地使用(在同一类中,可以轻松修改调用它的所有方法)。但是,如果它变成public,您就不知道谁使用(或将使用)您的方法,因此您无法控制可能因添加或删除throws条款而导致的所有代码中断。如果它是实例方法,则即使您不抛出异常,也有另一个允许throws条款的原因-该方法可以被覆盖,并且即使基类实现不会抛出异常,覆盖方法也可能会抛出异常。

2
在我看来,应该更加强调关于覆盖的最后一点。"函数改变会怎样"这个概念比"即使父函数没有理由抛出特定异常,甚至今天的子函数版本可能需要这样做"这个概念不够具体。 - supercat
@supercat同意,但问题中的代码有静态方法,这些方法无法被覆盖,这就是为什么我大部分答案给出适用于静态方法的原因。 - Eran

8

签名定义了方法的契约。即使现在方法不会抛出IOException,将来也有可能会抛出,你需要为这种可能性做好准备。

假设现在只为该方法提供一个哑实现,但是你知道以后的实际实现可能会抛出IOException。如果编译器阻止你添加此throws子句,那么一旦提供该方法的实际实现,你就被迫重新修改所有对该方法的调用(递归地)。


1
私有方法是否可以说有一个契约是有争议的。 - RealSkeptic
1
它为类中的所有其他方法定义了一个契约。这些方法可能反过来想要声明一个IOException,因为它们依赖于调用私有方法的契约。 - JB Nizet
你也可以决定更改私有方法的返回值。这也是“契约”的一部分,不是吗?然而,当发生这种情况时,您所做的就是修复调用它的代码。使用“throws”也可以做同样的事情。 - RealSkeptic
我不明白这与问题有何关联,问题是:为什么编译器允许在方法中使用throws子句来声明未被该方法抛出的异常。你的观点是什么?编译器也允许你将long作为返回类型,即使你只返回适合int的常量值。为什么?原因相同:你定义了契约,即该方法可能现在或将来返回一个long值。 - JB Nizet

0
关键字throws告诉程序员该方法可能会发生IOException异常。如果您没有指定try/catch,这意味着当异常被抛出时程序将停止工作,而在try/catch中,如果抛出异常,则通过执行其他操作来处理它。
使用throws可以提高可读性并指定异常的可能性,而使用try/catch则可以告诉程序在出现异常情况下应该怎么做。

-1
  1. methodB会抛出IOException异常,因此调用methodB的方法负责捕获由methodB抛出的异常。尝试从其他方法调用methodB,它将要求您捕获它或重新抛出IOException。在链中的某个地方,您将不得不在try/catch块中捕获IOException。因此,您不会得到编译时错误

    private void sampleMethod(){ try { methodB(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } }

  2. methodA中的try/catch显然会吞掉异常,这意味着methodA负责在try/catch块中捕获异常。 "任何Java程序中的每个语句都必须是可达的,即每个语句必须至少可执行一次" 因此,您将收到编译器错误,因为您的try块没有任何代码来引起IOException。


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