关于Java:为什么覆盖方法不能抛出比覆盖方法更广泛的异常?

Why can't overriding methods throw exceptions broader than the overridden method?

我正在浏览Kathe sierra编写的SCJP 6,并遇到了有关以重写方法引发异常的解释。 我真的不明白。 有人可以向我解释吗?

The overriding method must NOT throw checked exceptions that are new
or broader than those declared by the overridden method. For example, a
method that declares a FileNotFoundException cannot be overridden by a
method that declares a SQLException, Exception, or any other non-runtime
exception unless it's a subclass of FileNotFoundException.


这意味着,如果某个方法声明抛出一个给定的异常,则子类中的重写方法只能声明抛出该异常或其子类。例如:

1
2
3
4
5
6
7
8
9
10
11
class A {
   public void foo() throws IOException {..}
}

class B extends A {
   @Override
   public void foo() throws SocketException {..} // allowed

   @Override
   public void foo() throws SQLException {..} // NOT allowed
}

SocketException extends IOException,但SQLException没有。

这是由于多态性:

1
2
3
4
5
6
A a = new B();
try {
    a.foo();
} catch (IOException ex) {
    // forced to catch this by the compiler
}

如果B已决定抛出SQLException,则编译器无法强迫您捕获它,因为您是通过其超类-A引用B的实例的。另一方面,IOException的任何子类将由处理IOException的子句(捕获或抛出)处理

您需要能够按其超类引用对象的规则是Liskov替换原理。

由于未经检查的异常可以抛出到任何地方,因此它们不受此规则的约束。如果需要,可以将未经检查的异常作为文档形式添加到throws子句中,但是编译器不会对此执行任何强制措施。


覆盖方法可以引发任何未经检查的(运行时)异常,无论
重写的方法声明异常

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Super {
    public void test() {
        System.out.println("Super.test()");
    }
}

class Sub extends Super {
    @Override
    public void test() throws IndexOutOfBoundsException {
        // Method can throw any Unchecked Exception
        System.out.println("Sub.test()");
    }
}

class Sub2 extends Sub {
    @Override
    public void test() throws ArrayIndexOutOfBoundsException {
        // Any Unchecked Exception
        System.out.println("Sub2.test()");
    }
}

class Sub3 extends Sub2 {
    @Override
    public void test() {
        // Any Unchecked Exception or no exception
        System.out.println("Sub3.test()");
    }
}

class Sub4 extends Sub2 {
    @Override
    public void test() throws AssertionError {
        // Unchecked Exception IS-A RuntimeException or IS-A Error
        System.out.println("Sub4.test()");
    }
}


我认为这在Java语法设计中是失败的。多态性不应限制异常处理的使用。实际上,其他计算机语言则不这样做(C#)。

此外,方法在更专门的子类中被重写,因此它更复杂,因此,更可能引发新异常。


我在这里为旧问题提供此答案,因为没有答案表明重写方法不能抛出任何事实,这也是重写方法可以抛出的事实:

1)抛出相同的异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static class A
{
    public void m1()
       throws IOException
    {
        System.out.println("A m1");
    }

}

public static class B
    extends A
{
    @Override
    public void m1()
        throws IOException
    {
        System.out.println("B m1");
    }
}

2)覆盖方法的引发异常的子类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static class A
{
    public void m2()
       throws Exception
    {
        System.out.println("A m2");
    }

}

public static class B
    extends A
{
    @Override
    public void m2()
        throws IOException
    {
        System.out.println("B m2");
    }
}

3)什么都不要扔。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static class A
{  
    public void m3()
       throws IOException
    {
        System.out.println("A m3");
    }
}

public static class B
    extends A
{  
    @Override
    public void m3()
        //throws NOTHING
    {
        System.out.println("B m3");
    }
}

4)不需要抛出RuntimeExceptions。

是否抛出RuntimeExceptions,编译器不会抱怨。 RuntimeExceptions不是检查的异常。如果未捕获,则仅需要将已检查的异常显示在抛出中。


为了说明这一点,请考虑:

1
2
3
4
5
6
7
8
9
public interface FileOperation {
  void perform(File file) throws FileNotFoundException;
}

public class OpenOnly implements FileOperation {
  void perform(File file) throws FileNotFoundException {
    FileReader r = new FileReader(file);
  }
}

假设您然后编写:

1
2
3
4
5
6
public class OpenClose implements FileOperation {
  void perform(File file) throws FileNotFoundException {
    FileReader r = new FileReader(file);
    r.close();
  }
}

这将给您带来编译错误,因为r.close()抛出一个IOException,它比FileNotFoundException还要广泛。

要解决此问题,请输入以下内容:

1
2
3
4
5
6
public class OpenClose implements FileOperation {
  void perform(File file) throws IOException {
    FileReader r = new FileReader(file);
    r.close();
  }
}

您将得到一个不同的编译错误,因为您正在实现perform(...)操作,但是会引发该方法的接口定义中未包含的异常。

为什么这很重要?接口的使用者可能会:

1
2
3
4
5
6
7
FileOperation op = ...;
try {
  op.perform(file);
}
catch (FileNotFoundException x) {
  log(...);
}

如果允许抛出IOException,则客户端的代码不再正确。

请注意,如果您使用未经检查的异常,则可以避免此类问题。 (我不建议您做或不做,这是一个哲学问题)


让我们接受面试问题。
有一个方法会在超类中引发NullPointerException。我们可以用一个
抛出RuntimeException的方法?

要回答此问题,请让我们知道什么是"未检查并已检查"异常。

  • 必须按照以下说明明确捕获或传播检查的异常
    基本try-catch-finally异常处理。未检查的异常没有此要求。
    它们不必被捕获或声明为抛出。

  • Java中的检查异常扩展了java.lang.Exception类。未经检查的异常扩展了java.lang.RuntimeException。

  • 公共类NullPointerException扩展了RuntimeException

    未经检查的异常扩展了java.lang.RuntimeException。
    这就是为什么NullPointerException是Uncheked异常的原因。

    让我们举个例子:
    范例1:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
        public class Parent {
           public void name()  throws NullPointerException {
               System.out.println(" this is parent");
           }
    }

    public class Child  extends Parent{
         public  void name() throws RuntimeException{
                 System.out.println(" child");
         }

         public static void main(String[] args) {
            Parent parent  = new Child();
            parent.name();// output => child
        }
    }

    该程序将成功编译。
    范例2:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
        public class Parent {
           public void name()  throws RuntimeException {
               System.out.println(" this is parent");
           }
    }

    public class Child  extends Parent{
         public  void name() throws  NullPointerException {
                 System.out.println(" child");
         }

         public static void main(String[] args) {
            Parent parent  = new Child();
            parent.name();// output => child
        }
    }

    该程序还将成功编译。
    因此很明显,在未检查的异常的情况下什么也不会发生。
    现在,让我们看一下Checked异常的情况。
    范例3:
    当基类和子类都引发检查异常时

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
        public class Parent {
           public void name()  throws IOException {
               System.out.println(" this is parent");
           }
    }
    public class Child  extends Parent{
         public  void name() throws IOException{
                 System.out.println(" child");
         }

         public static void main(String[] args) {
            Parent parent  = new Child();

            try {
                parent.name();// output=> child
            }catch( Exception e) {
                System.out.println(e);
            }

        }
    }

    该程序将成功编译。
    范例4:
    与子类的相同方法相比,当子类的方法引发边界检查的异常时。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    import java.io.IOException;

    public class Parent {
           public void name()  throws IOException {
               System.out.println(" this is parent");
           }
    }
    public class Child  extends Parent{
         public  void name() throws Exception{ // broader exception
                 System.out.println(" child");
         }

         public static void main(String[] args) {
            Parent parent  = new Child();

            try {
                parent.name();//output=> Compilation failure
            }catch( Exception e) {
                System.out.println(e);
            }

        }
    }

    该程序将无法编译。因此,在使用Checked异常时我们必须要小心。


    假设您拥有方法M1抛出E1的超类A,而类A则继承了方法M2覆盖M1的类B。 M2不能抛出与E1不同或更少的东西。

    由于多态性,使用类A的客户端应该能够将B视为A。Inharitance ===> Is-a(B is-a A)。如果处理类A的代码正在处理异常E1,如M1声明它抛出了此检查的异常,但是又抛出了不同类型的异常,该怎么办?如果M1抛出IOException,则M2很可能抛出FileNotFoundException,因为它是IOException。 A的客户可以毫无问题地进行处理。如果引发的异常范围更广,则A的客户将没有机会了解此情况,因此也就没有机会抓住它。


    为了理解这一点,我们来看一个例子,其中有一个类Mammal,它定义了readAndGet方法,该方法正在读取某些文件,对该文件进行一些操作并返回一个类Mammal的实例。

    1
    2
    3
    class Mammal {
        public Mammal readAndGet() throws IOException {//read file and return Mammal`s object}
    }

    Human扩展了类Mammal,并覆盖了readAndGet方法以返回Human的实例而不是Mammal的实例。

    1
    2
    3
    4
    class Human extends Mammal {
        @Override
        public Human readAndGet() throws FileNotFoundException {//read file and return Human object}
    }

    要调用readAndGet,我们将需要处理IOException,因为它是已检查的异常,而哺乳动物的readAndMethod正在将其抛出。

    1
    2
    3
    4
    Mammal mammal = new Human();
    try {
        Mammal obj = mammal.readAndGet();
    } catch (IOException ex) {..}

    而且我们知道对于编译器mammal.readAndGet()是从类Mammal的对象中调用的,但是在运行时,JVM会将mammal.readAndGet()方法调用解析为来自类Human的调用,因为Mammal持有new Human()

    Mammal中的方法readAndMethod抛出IOException,并且由于它是已检查的异常,因此只要我们在Mammal上调用readAndGet,编译器就会强制我们捕获它。

    现在假设Human中的readAndGet引发了任何其他检查的异常,例如异常,我们知道readAndGet将被Human实例调用,因为Mammal持有new Human()

    因为对于编译器,该方法是从Mammal调用的,所以编译器将强制我们仅处理IOException,但在运行时,我们知道该方法将抛出未得到处理的Exception异常,并且如果方法引发异常。

    这就是为什么在编译器级别本身不会阻止它的原因,并且我们不允许抛出任何新的或更广泛的检查异常,因为最终JVM不会对其进行处理。

    在覆盖方法时还需要遵循其他规则,您可以阅读有关为什么我们应该遵循方法覆盖规则的更多信息以了解原因。


    覆盖方法不得抛出比被覆盖方法声明的异常新的或更广泛的检查异常。

    这仅意味着当您覆盖现有方法时,此重载方法引发的异常应该与原始方法引发的异常或其任何子类相同。

    请注意,检查是否处理了所有检查到的异常是在编译时而不是在运行时完成的。因此,在编译时,Java编译器会检查重写的方法引发的异常类型。由于只能在运行时确定将执行哪个重写的方法,因此我们不知道必须捕获哪种异常。

    假设我们有类A及其子类BA具有方法m1,并且类B已覆盖此方法(为避免混淆,请将其命名为m2)。现在,假设m1抛出E1,而m2抛出E2,这是E1的超类。现在,我们编写以下代码:

    1
    2
    A myAObj = new B();
    myAObj.m1();

    请注意,m1只是对m2的调用(同样,重载方法中的方法签名相同,因此请不要与m1m2混淆..它们只是在本示例中有所区别...他们都有相同的签名)。
    但是在编译时,所有Java编译器所做的工作就是转到引用类型(在本例中为Class A)检查该方法是否存在,并期望程序员对其进行处理。所以很明显,您会抛出或抓住E1。现在,在运行时,如果重载的方法抛出E2(这是E1的超类),那么……很好,这是非常错误的(出于相同的原因,我们不能说B myBObj = new A())。因此,Java不允许这样做。重载方法抛出的未经检查的异常必须相同,子类相同或不存在。


    覆盖方法一定不能抛出经过检查的异常,这些异常是新的或更广泛的
    由重写方法声明。

    例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    class Super {
        public void throwCheckedExceptionMethod() throws IOException {
            FileReader r = new FileReader(new File("aFile.txt"));
            r.close();
        }
    }

    class Sub extends Super {    
        @Override
        public void throwCheckedExceptionMethod() throws FileNotFoundException {
            // FileNotFoundException extends IOException
            FileReader r = new FileReader(new File("afile.txt"));
            try {
                // close() method throws IOException (that is unhandled)
                r.close();
            } catch (IOException e) {
            }
        }
    }

    class Sub2 extends Sub {
        @Override
        public void throwCheckedExceptionMethod() {
            // Overriding method can throw no exception
        }
    }

    好吧,java.lang.Exception扩展了java.lang.Throwable。 java.io.FileNotFoundException扩展了java.lang.Exception。因此,如果方法抛出java.io.FileNotFoundException,则在覆盖方法中,您不能向层次结构中抛出比FileNotFoundException更高的任何东西,例如您不能抛出java.lang.Exception。您可以抛出FileNotFoundException的子类。但是,您将不得不在重写方法中处理FileNotFoundException。敲一些代码,然后尝试一下!

    这里有规则,因此您不会通过扩大特异性而失去原始的throws声明,因为多态性意味着您可以在超类上调用重写的方法。


    处理覆盖方法的检查和未检查异常的规则

    -当父类方法没有声明异常时,则子类重写-
    方法可以声明,

    1
    2
    3
     1. No exception or
     2. Any number of unchecked exception
     3. but strictly no checked exception

    -当父类方法声明未检查的异常时,则子类覆盖方法可以声明,

    1
    2
    3
     1. No exception or
     2. Any number of unchecked exception
     3. but strictly no checked exception

    -当父类方法声明检查异常时,则子类覆盖方法可以声明,

    1
    2
    3
    4
     1. No exception or
     2. Same checked exception or
     3. Sub-type of checked exception or
     4. any number of unchecked exception

    即使在父类的方法中声明了已检查和未检查异常的组合,上述所有结论仍然成立

    参考


    Java允许您选择限制父类中的异常,因为它假设客户端将限制捕获的内容。恕我直言,您基本上不应该使用此"功能",因为您的客户可能需要长期的灵活性。

    Java是一种设计较差的旧语言。现代语言没有这种限制。
    解决此缺陷的最简单方法是始终使您的基类throw Exception。客户可以抛出更具体的异常,但会使您的基类真正变得广泛。


    子类的重写方法只能抛出作为超类的方法的已检查异常的子类的多个检查的异常,而不能引发与父类的方法的已检查异常无关的多个检查的异常


    我们对以下内容有何解释?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    class BaseClass {

        public  void print() {
            System.out.println("In Parent Class , Print Method");
        }

        public static void display() {
            System.out.println("In Parent Class, Display Method");
        }

    }


    class DerivedClass extends BaseClass {

        public  void print() throws Exception {
            System.out.println("In Derived Class, Print Method");
        }

        public static void display() {
            System.out.println("In Derived Class, Display Method");
        }
    }

    当print方法抛出Exception时,类DerivedClass.java抛出编译时异常,baseclass的print()方法不抛出任何异常

    我可以将其归因于Exception比RuntimeException窄,它可以是No Exception(Runtime error),RuntimeException及其子异常。