Java编程笔记10:异常

Java编程笔记10:异常

5c9c3b3b392ac581.jpg

图源:PHP中文网

Java中的异常类都继承自Throwable,这是所有异常的基类。异常类型中有这几个类比较重要:

image-20220123183114503

其中Throwable是基类,凡是Throwable的子类型都可以被当做异常抛出或捕获。Exception是开发者常打交道的异常类型,如果需要在程序中抛出异常,可以使用内置的继承自Exception的子类,也可以自行创建一个继承自Exception的异常类,所以可以将Exception及其子类异常称作"用户异常"(customer exception)。Error这类异常由JVM管理,一般不需要开发者操心,所以可以称作“系统异常”。RuntimeException是一个Exception的子类型,这类异常比较特殊,代表一种在编译时无法察觉,只有在系统运行时才会出现的异常,所以叫做“运行时异常”。

基础

看一个最简单的异常示例:

package ch10.create;

class MyExcetion extends Exception {
}

public class Main {
    private static void testException() throws MyExcetion {
        throw new MyExcetion();
    }

    public static void main(String[] args) {
        try {
            testException();
        } catch (Exception e) {
            e.printStackTrace();
            // ch10.create.MyExcetion
            // at ch10.create.Main.testException(Main.java:8)
            // at ch10.create.Main.main(Main.java:13)
        }
    }
}

这个例子中包含了Java异常最基础的几个组成部分:

  • 异常定义
  • 抛出异常
  • 异常声明
  • 异常捕获

下面对这几部分分别进行说明。

异常定义

要创建一个用户自定义异常很简单,创建一个类,并集成Exception即可,就像上面的示例中展示的MyException

一般来说,异常最重要的信息是异常类型本身,所以通过内建异常的名称就可以分辨其用途并使用:

package ch10.create2;

public class Main {
    private static void testException(Object o) {
        if (o == null) {
            throw new NullPointerException();
        }
        System.out.println(o.toString());
    }

    public static void main(String[] args) {
        testException("Hello World!");
        testException(null);
        // Hello World!
        // Exception in thread "main" java.lang.NullPointerException
        // at ch10.create2.Main.testException(Main.java:9)
        // at ch10.create2.Main.main(Main.java:16)
    }
}

这个例子中,testException在使用参数o前先检查其是否为null,如果是,就抛出一个内置异常NullPointerException,表示传入的参数是null而非一个有效对象,程序无法继续运行。

再举一个例子,计算机执行数学运算,可能会出现“除零问题”,这同样可以用异常来表示:

package ch10.create3;

public class Main {
    private static double divide(double a, double b) {
        if (b == 0) {
            throw new ArithmeticException();
        }
        return a / b;
    }

    public static void main(String[] args) {
        System.out.println(divide(1, 5));
        System.out.println(divide(10, 0));
        // 0.2
        // Exception in thread "main" java.lang.ArithmeticException
        //         at ch10.create3.Main.divide(Main.java:13)
        //         at ch10.create3.Main.main(Main.java:21)
    }
}

这里的内置异常ArithmeticException代表执行数学运算时出现的异常(包括除零问题)。

难道我们我们需要在使用对象或者进行除法运算时都要像上面这样进行检查并抛出异常?当然不需要,这是因为这类常见异常都继承自RuntimeException,属于前边提到的运行时异常。在运行时,如果JVM发现要使用对象成员,但对象是null,或者执行除法运算时除数为0,就会自动产生相应的异常并打印异常信息:

package ch10.create4;

public class Main {
    private static double divide(int a, int b) {
        return a / b;
    }

    public static void main(String[] args) {
        System.out.println(divide(1, 5));
        System.out.println(divide(10, 0));
        // 0.0
        // Exception in thread "main" java.lang.ArithmeticException: / by zero
        //         at ch10.create4.Main.divide(Main.java:5)
        //         at ch10.create4.Main.main(Main.java:10)
    }
}

如果仔细观察,你或许会发现我将divide参数类型都改成了int而不是原来的double。这是因为如果不修改的话,这里将不会产生任何异常,而是输出一个奇怪的Infinity

这是因为在计算机的世界里,整形是精确值,但是浮点数是近似值,是有精度限制的。所以0.0并不一定会与0相等,而且我们也不能将两个浮点数直接用==进行比较,所以之前的代码其实是有问题的,应当这么写:

	...
	private static double divide(double a, double b) {
        if ((b - 0) < 0.0001) {
            throw new ArithmeticException();
        }
        return a / b;
    }
	...

这也解释了为什么10.0/0.0=Infinity,其实此处的浮点数0.0是一个无限接近0但不是0的数,比如0.00000000000001,用10.0除,自然得到的结果是无穷大。

除了异常类型本身之外,也可以给异常添加一些额外信息:

package ch10.create6;

class MyExcetion extends Exception {
    public MyExcetion(String msg) {
        super(msg);
    }
}

public class Main {
    private static void testException() throws MyExcetion {
        throw new MyExcetion("this is a message.");
    }

    public static void main(String[] args) {
        try {
            testException();
        } catch (Exception e) {
            e.printStackTrace();
            // ch10.create6.MyExcetion: this is a message.
            // at ch10.create6.Main.testException(Main.java:11)
            // at ch10.create6.Main.main(Main.java:16)
        }
    }
}

这是利用了异常基类Throwable的一个构造方法:

    public Throwable(String message) {
        fillInStackTrace();
        detailMessage = message;
    }

该方法可以保存一个字符串作为信息到异常中。

抛出异常

抛出异常很简单,只要使用throw关键字即可(比较有趣的是Python使用的是raise),但需要注意的是,Java中,视抛出异常的类型的不同,方法同样需要做出改变:


package ch10.throw1;

class MyException extends Exception {

}

public class Main {
    private static void throwCheckedException() throws MyException {
        throw new MyException();
    }

    private static void throwUnCheckedException() {
        throw new RuntimeException();
    }

    public static void main(String[] args) {
        try {
            throwCheckedException();
        } catch (MyException e) {
            e.printStackTrace();
        }
        throwUnCheckedException();
        // ch10.throw1.MyException
        //         at ch10.throw1.Main.throwCheckedException(Main.java:10)
        //         at ch10.throw1.Main.main(Main.java:19)
        // Exception in thread "main" java.lang.RuntimeException
        //         at ch10.throw1.Main.throwUnCheckedException(Main.java:14)
        //         at ch10.throw1.Main.main(Main.java:23)
    }
}

就像上面展示的,MyException是一个继承自Exception的自定义异常,要在方法中直接抛出这个异常,需要在方法中添加异常声明(exception declare),这是指throws关键字和其之后的部分。但抛出内置异常RuntimeException则不需要。

《Thinking in Java》将Exception Declare被翻译为“异常说明”。

这是因为异常可以用“在抛出时是否需要添加异常声明”分为两类:

  • 被检查的异常(Checked Exception),指从Exception继承,但不包括RuntimeException一类的异常。
  • 不被检查的异常(Unchecked Exception),指继承自ErrorRuntimeException的异常。

如果调用方法时,该方法会抛出被检查的异常,或者在当前方法中直接创建并抛出此类异常,都必须使用try...catch捕获异常并处理,或者在方法声明中添加异常声明,直接将异常继续向上抛出。

Java异常的这个设计相当独特,在Python和PHP中,都没有类似的“被检查的异常”。大多数支持异常的编程语言都相当简单,如果有异常产生,只有两种结果:

  1. 不对异常捕获处理,直接让异常向上一直抛出,直到语言的编译器/解释器捕获,终止程序并显示调用栈相关信息。
  2. 对异常进行捕获处理。

之所以Java会如此设计,可能出于两个考虑:

  • 某些异常必须被处理,比如打开某种需要关闭的资源后产生的异常。
  • 积极地处理异常可以提高程序的健壮性。

但从现实看,使用异常提高程序的健壮性只是一种“理想化的想法”,事实上绝大多数情况下,我们都无法对异常做出有效处理,唯一能做的就是向上抛出,除了报告错误外,没法做到更多。

最后要补充的是,异常声明可以同时声明多个异常:


package ch10.throw2;

class MyException extends Exception {
}

class MyException2 extends Exception {
}

public class Main {
    private static void throwCheckedException(int i) throws MyException, MyException2 {
        if (i > 10) {
            throw new MyException();
        } else {
            throw new MyException2();
        }
    }

    public static void main(String[] args) {
        try {
            throwCheckedException(10);
        } catch (MyException e) {
            e.printStackTrace();
        } catch (MyException2 e) {
            e.printStackTrace();
        }
    }
}

甚至可以在实际上没有“真的抛出异常”的情况下添加异常声明:

...
public class Main {
    private static void throwCheckedException(int i) throws MyException, MyException2 {
        ;// do nothing.
    }
	...
}

利用这种特性,可以在抽象基类中预先“埋设”未来可能产生的异常。

捕获异常

使用try...catch语句可以捕获异常并进行必要的处理:

package ch10.catch1;

class MyException extends Exception {
}

public class Main {
    public static void main(String[] args) {
        try {
            throw new MyException();
        } catch (MyException e) {
            ;
        }
    }
}

try中的部分被称作“监控区域”(guarded region),如果这部分代码会产生异常,就会被之后的catch部分捕获,并执行相应的处理语句。

当然上面的示例不会产生任何输出,因为catch中没有任何语句,这种情况下程序就像“没有异常”一样,被称作“吞掉异常”,此种情况可能会掩盖程序已经存在的错误,是应当被避免的。

printStackTrace

当然也可以直接打印异常对象,但更常见的做法是使用printStackTrace方法打印异常的调用栈,通过观察调用栈相关信息可以快速定位问题:

package ch10.catch2;

class MyException extends Exception {
}

public class Main {
    private static void g() throws MyException {
        throw new MyException();
    }

    private static void f() throws MyException {
        g();
    }

    public static void main(String[] args) {
        try {
            f();
        } catch (MyException e) {
            e.printStackTrace();
            // ch10.catch2.MyException
            // at ch10.catch2.Main.g(Main.java:8)
            // at ch10.catch2.Main.f(Main.java:12)
            // at ch10.catch2.Main.main(Main.java:17)
        }
    }
}

默认的printStackTrace方法会将调用栈信息打印到标准错误流,当然,默认情况下的标准错误流和标准输出流都是指向控制台。

printStackTrace有一个可以接收printStream参数的重载方法:

    ...
	public void printStackTrace(PrintStream s) {
        printStackTrace(new WrappedPrintStream(s));
    }
	...

所以默认的printStackTrace可以看做是printStackTrace(System.err),类似的,我们可以用该方法将错误信息重定向到其它输出流,比如标准输出(System.out):

    ...
	public static void main(String[] args) {
        try {
            f();
        } catch (MyException e) {
            e.printStackTrace(System.out);
        }
    }
	...
Logger

除此以外,还可以结合内建的日志类java.util.logging.Logger,将异常信息输出到日志中:

	...
	public static void main(String[] args) {
        try {
            f();
        } catch (MyException e) {
            StringWriter sw = new StringWriter();
            e.printStackTrace(new PrintWriter(sw));
            Logger logger = Logger.getLogger("xyz.icexmoon.java-notes.ch10.ctch4.Main");
            logger.severe(sw.toString());
        }
    }
	...

可以通过Logger.getLogger方法获取一个日志实例,其参数是日志实例的名称,一般习惯会使用带包名的完整类名进行命名。

Logger用于处理和记录日志的类是logging.Handler,这是一个抽象类。默认会使用一个子类ConsoleHandler,也就是输出到控制台。如果要让日志输出到文件,可以使用FileHandler

package ch10.catch4;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.logging.FileHandler;
import java.util.logging.Logger;

class MyException extends Exception {
}

public class Main {
	...
    public static void main(String[] args) {
        try {
            f();
        } catch (MyException e) {
            StringWriter sw = new StringWriter();
            e.printStackTrace(new PrintWriter(sw));
            Logger logger = Logger.getLogger("xyz.icexmoon.java-notes.ch10.ctch4.Main");
            FileHandler fh;
            try {
                String fname = "D:\\workspace\\java\\java-notebook\\xyz\\icexmoon\\java_notes\\ch10\\catch4\\exp.log";
                fh = new FileHandler(fname);
                logger.addHandler(fh);
                logger.severe(sw.toString());
            } catch (SecurityException e1) {
                e1.printStackTrace();
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        }
    }
}

查看fname所表示的日志文件就可以发现,会写入xml结构的日志信息:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE log SYSTEM "logger.dtd">
<log>
<record>
  <date>2022-01-24T08:33:52.892243300Z</date>
  <millis>1643013232892</millis>
  <nanos>243300</nanos>
  <sequence>0</sequence>
  <logger>xyz.icexmoon.java-notes.ch10.ctch4.Main</logger>
  <level>SEVERE</level>
  <class>ch10.catch4.Main</class>
  <method>main</method>
  <thread>1</thread>
  <message>ch10.catch4.MyException
	at ch10.catch4.Main.g(Main.java:14)
	at ch10.catch4.Main.f(Main.java:18)
	at ch10.catch4.Main.main(Main.java:23)
</message>
</record>
</log>
捕获所有异常

可以设置多个catch语句来捕获异常,其匹配规则是按顺序进行匹配,一旦匹配成功,就会执行相应的异常处理语句,而会跳过其余的catch语句:

package ch10.all;

import java.util.Random;

class MyException1 extends Exception {
}

class MyException2 extends Exception {
}

class MyException3 extends Exception {
}

public class Main {
    private static void throwExp(int i) throws MyException1, MyException2, MyException3 {
        if (i < 3) {
            throw new MyException1();
        } else if (i >= 6) {
            throw new MyException2();
        } else {
            throw new MyException3();
        }
    }

    public static void main(String[] args) {
        Random random = new Random();
        try {
            throwExp(random.nextInt(10));
        } catch (MyException1 e1) {
            System.out.println("e1:" + e1);
        } catch (MyException2 e2) {
            System.out.println("e2:" + e2);
        } catch (MyException3 e3) {
            System.out.println("e3:" + e3);
        }
    }
}

异常匹配实际上也包括继承关系,比如可以通过Exception捕获我们需要捕获的所有异常,所以上边的示例也可以改写为:

    ...
    public static void main(String[] args) {
        Random random = new Random();
        try {
            throwExp(random.nextInt(10));
        } catch (Exception e) {
            System.out.println("e:" + e);
        }
    }
    ...

我们可以将Exception作为最后一个异常匹配检测,将更详尽的异常类型放在前边:

	...
	public static void main(String[] args) {
        Random random = new Random();
        try {
            throwExp(random.nextInt(10));
        } catch (MyException1 e1) {
            System.out.println("check exp1: " + e1);
        } catch (MyException2 e2) {
            System.out.println("check exp2: " + e2);
        } catch (MyException3 e3) {
            System.out.println("check exp3: " + e3);
        } catch (Exception e) {
            System.out.println("default exp check: " + e);
        }
    }
    ...
finally

有时候,无论是否产生异常,我们都希望执行一些特定语句,最常见的是在打开需要进行释放的资源(文件、网络等)的时候。

下面就是一个打开文件并逐行读取的示例:

package ch10.finally1;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;

public class Main {
    public static void main(String[] args) {
        String fn = "D:\\workspace\\java\\java-notebook\\xyz\\icexmoon\\java_notes\\ch10\\catch4\\exp.log";
        BufferedReader br = null;
        try {
            br = new BufferedReader(new FileReader(fn));
            String line = null;
            do {
                line = br.readLine();
                System.out.println(line);
            } while (line != null);
            try {
                br.close();
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (Exception e) {
            try {
                br.close();
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        }
    }
}

事实上,更好的做法是使用finally,应当在资源成功创建后使用try...catch...finally使用资源以及确保资源总是能够得到释放:

package ch10.finally2;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;

public class Main {
    public static void main(String[] args) {
        String fn = "D:\\workspace\\java\\java-notebook\\xyz\\icexmoon\\java_notes\\ch10\\catch4\\exp.log";
        BufferedReader br = null;
        try {
            br = new BufferedReader(new FileReader(fn));
            try {
                String line = null;
                do {
                    line = br.readLine();
                    System.out.println(line);
                } while (line != null);

            } catch (IOException e) {
                throw e;
            } finally {
                try {
                    br.close();
                } catch (IOException e1) {
                    throw e1;
                }
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

使用finally可以让代码更简洁一些。

finally内的语句在任何情况下都会被执行,这包括有breakcontinue的情况下:

package ch10.finally3;

public class Main {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            try {
                System.out.println("now i=" + i);
                if (i == 2) {
                    break;
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                System.out.println("finally block is executed.");
            }
        }
        // now i=0
        // finally block is executed.
        // now i=1
        // finally block is executed.
        // now i=2
        // finally block is executed.
    }
}

甚至即使try块中使用return语句也无法阻止finally的执行:

package ch10.finally4;

import java.util.Random;

public class Main {
    private static void testFinally(int i) {
        try {
            if (i < 5) {
                System.out.println("i<5");
                return;
            } else {
                System.out.println("i>=5");
                return;
            }
        } finally {
            System.out.println("finally block is executed.");
        }
    }

    public static void main(String[] args) {
        Random random = new Random();
        testFinally(random.nextInt(10));
        // i>=5
        // finally block is executed.
    }
}

即使异常没有被当前代码正常捕获,而是继续向上抛出,finally同样会被执行:

package ch10.finally5;

class MyExp extends Exception {
}

public class Main {
    public static void main(String[] args) throws MyExp {
        try {
            throw new MyExp();
        } finally {
            System.out.println("finally block is executed.");
        }
    }
}
// finally block is executed.
// Exception in thread "main" ch10.finally5.MyExp
//         at ch10.finally5.Main.main(Main.java:9)

栈轨迹

前面说了,通过printStackTrace可以打印异常的调用栈。实际上调用栈简单的说就是函数的调用轨迹,可以看做是将函数按照调用顺序一一压入栈中。

可以直接通过异常获取调用栈,并查看其中元素的信息:

package ch10.stack_trace;

import util.Fmt;

class MyExp extends Exception {
}

public class Main {
    private static void f() throws MyExp {
        throw new MyExp();
    }

    private static void g() throws MyExp {
        f();
    }

    public static void main(String[] args) {
        try {
            g();
        } catch (MyExp e) {
            for (StackTraceElement ste : e.getStackTrace()) {
                Fmt.printf("line:%d method:%s\n", ste.getLineNumber(), ste.getMethodName());
            }
            // line:10 method:f
            // line:14 method:g
            // line:19 method:main
        }
    }
}

函数入栈顺序是main()go()f(),所以出栈顺序相反,是f()g()main()

重新抛出异常

有时候我们会在捕获到异常后将异常重新抛出,此时异常的调用栈信息不会发生任何改变,如果使用printStackTrace打印就能发现这一点:

package ch10.stack_trace2;

import util.Fmt;

class MyExp extends Exception {
}

public class Main {
    private static void f() throws MyExp {
        throw new MyExp();
    }

    private static void g() throws MyExp {
        try {
            f();
        } catch (MyExp e) {
            throw e;
        }
    }

    public static void main(String[] args) {
        try {
            g();
        } catch (MyExp e) {
            e.printStackTrace();
            // ch10.stack_trace2.MyExp
            // at ch10.stack_trace2.Main.f(Main.java:10)
            // at ch10.stack_trace2.Main.g(Main.java:15)
            // at ch10.stack_trace2.Main.main(Main.java:23)
        }
    }
}

如果想要在重新抛出异常的时候改写调用栈的内容,可以使用fillInStackTrace方法:

...
public class Main {
	...
    private static void g() throws MyExp {
        try {
            f();
        } catch (MyExp e) {
            e.fillInStackTrace();
            throw e;
        }
    }

    public static void main(String[] args) {
        try {
            g();
        } catch (MyExp e) {
            e.printStackTrace();
            // ch10.stack_trace3.MyExp
            // at ch10.stack_trace3.Main.g(Main.java:15)
            // at ch10.stack_trace3.Main.main(Main.java:22)
        }
    }
}

调用栈会从调用fillInStackTrace方法的地方重新记录。

异常链

除了将原有异常重新抛出,也可以创建新异常并抛出,但问题是这样做会丢失原始异常的相关信息,这对排查问题是不利的。

这个问题可以通过建立“异常链”的方式解决。所谓的异常链就是让原始异常和新异常关联起来,Java是通过给几种基础异常类ExceptionErrorRuntimeException类添加构造函数来实现:

    ...
    public Exception(String message, Throwable cause) {
        super(message, cause);
    }
    public Exception(Throwable cause) {
        super(cause);
    }
    ...

这里的参数cause就是引发新异常的原始异常。

但是除了这几种基础异常以外,其它大部分异常都没有实现相关的构造函数,但是提供一个initCause方法来设置对原始异常的关联:

	...
	public synchronized Throwable initCause(Throwable cause) {
		...
        return this;
    }
    ...

下面是一个简单示例:

package ch10.stack_trace4;

class MyExp extends Exception {
}

public class Main {
    private static void f() throws MyExp {
        throw new MyExp();
    }

    private static void g() throws MyExp {
        try {
            f();
        } catch (MyExp e) {
            MyExp newExp = new MyExp();
            newExp.initCause(e);
            throw newExp;
        }
    }

    public static void main(String[] args) {
        try {
            g();
        } catch (MyExp e) {
            e.printStackTrace();
            // ch10.stack_trace4.MyExp
            //         at ch10.stack_trace4.Main.g(Main.java:15)
            //         at ch10.stack_trace4.Main.main(Main.java:23)
            // Caused by: ch10.stack_trace4.MyExp
            //         at ch10.stack_trace4.Main.f(Main.java:8)
            //         at ch10.stack_trace4.Main.g(Main.java:13)
            //         ... 1 more
        }
    }
}

当然也可以通过throw new RuntimeException(e)的方式抛出新异常,效果是类似的。

异常丢失

在某些情况会出现异常丢失的情况,且难以觉察,比如:

package ch10.dispose;

class MyExp extends Exception {
}

class MyExp2 extends Exception {
}

public class Main {
    private static void throwMyExp2() throws MyExp2 {
        throw new MyExp2();
    }

    public static void main(String[] args) throws Exception {
        try {
            throw new MyExp();
        } finally {
            throwMyExp2();
        }
    }
}
// Exception in thread "main" ch10.dispose.MyExp2
//         at ch10.dispose.Main.throwMyExp2(Main.java:11)
//         at ch10.dispose.Main.main(Main.java:18)

try中抛出了异常,但是没有相应的捕获和处理语句,且finally调用了一个同样会产生异常的函数,这样就导致最终向上抛出的异常是finally导致的MyExp2异常而非原始的MyExp异常。

一个更简单的丢失异常的例子:

package ch10.dispose2;

class MyExp extends Exception {
}

public class Main {
    private static void throwMyExp() {
        try {
            throw new MyExp();
        } finally {
            return;
        }
    }

    public static void main(String[] args) {
        throwMyExp();
    }
}

通过finally语句直接使用return结束函数调用,居然可以让原本抛出的异常消失,就像什么都没有发生过一样。幸运的是这种情况下IDE会提示finally块没有正常闭合。

异常的限制

在Java中,所有事情一旦涉及继承就会变得复杂,异常同样如此。总的来说,如果涉及继承和方法覆盖,依然需要遵守里氏替换原则,即子类实例必须能当作父类实例进行使用。

这样的限制体现在两方面:

  1. 子类覆盖的方法只能“削减”父类方法的异常声明,而不能增加。
  2. 子类覆盖的方法中的声明的异常类型只能是父类方法中异常的子类,而不能是超类。

当然,这里提到的异常都属于“被检查的异常”,毕竟“不被检查的异常”不涉及异常声明的问题。

第一种限制很容易理解,毕竟如果子类可以在覆盖方法时随意添加新的异常类型,那就意味着原有的处理父类的代码需要做出修改,这样显然是违反里氏替换原则,且对多态是不利的。

举一个例子:

package ch10.limit;

class MyExp extends Exception {
}

class MyExp2 extends Exception {
}

class Parent {
    public void test() throws MyExp {
    }
}

class Child extends Parent {
    @Override
    public void test() throws MyExp,MyExp2 {
        super.test();
    }
}

public class Main {
    private static void dealParent(Parent parent) {
        try {
            parent.test();
        } catch (MyExp e) {
            throw new RuntimeException(e);
        }
    }

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

dealParent函数可以多态调用Parent及其子类的test方法,但因为Child在重写test方法时添加了一个新的异常声明,就导致dealParent函数不能正常运行。当然,实际上因为Java对异常的限制,上面的代码不能通过编译。

关于第二种限制,可以类比Java编程笔记5:多态 - 魔芋红茶’s blog (icexmoon.xyz)中提到的协变返回类型,其实理念是一致的,如果子类覆盖方法时使用的是异常的子类型,那么子类型的异常因为可以被动作父类型异常来看待,所以自然也就不会违反里氏替换原则。

package ch10.limit2;

class MyExp extends Exception {
}

class MyExp2 extends MyExp {
}

class Parent {
    public void test() throws MyExp {
    }
}

class Child extends Parent {
    @Override
    public void test() throws MyExp2 {
    }
}
...

这样做是允许的,或许私下里可以称作“协变异常声明”?

和返回值一样,异常声明同样不包含在“参数签名”之内,也就是说不会被作为参数重载的标识。

此外,和普通方法不同,构造器并没有类似的限制:

package ch10.limit3;

class MyExp extends Exception {
}

class MyExp2 extends Exception {
}

class Parent {
    public Parent() throws MyExp {
    }
}

class Child extends Parent {

    public Child() throws MyExp, MyExp2 {
        super();
    }
}

public class Main {

    public static void main(String[] args) throws MyExp, MyExp2 {
        Parent parent = new Parent();
        Child child = new Child();
    }
}

看似上面的方式违反了里氏替换原则,但其实里氏替换原则是针对多态而言的,而创建对象和构造器并不涉及多态,自然也就不会有相关的限制。

构造器

如果构造器涉及异常同样会很麻烦,这里使用一个可以逐行读取文件的程序作为示例:

package ch10.constructor;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;

class FileInput {
    BufferedReader reader;

    public FileInput(String fname) throws FileNotFoundException {
        FileReader fr;
        try {
            fr = new FileReader(fname);
            reader = new BufferedReader(fr);
        } catch (FileNotFoundException e) {
            throw e;
        }
    }

    public String readLine() throws IOException {
        return reader.readLine();
    }

    public void close() throws IOException {
        reader.close();
    }

}

public class Main {
    public static void main(String[] args) {
        String fname = "D:\\workspace\\java\\java-notebook\\xyz\\icexmoon\\java_notes\\ch10\\catch4\\exp.log";
        try {
            FileInput fi = new FileInput(fname);
            try {
                String line = null;
                do {
                    line = fi.readLine();
                    System.out.println(line);
                } while (line != null);
            } finally {
                fi.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

因为FileInput对象创建的时候有可能抛出异常,所以必须将new FileInputtry包裹,同时因为FileInput包含一个文件资源,需要在创建后及时调用close方法关闭资源,因此需要在FileInput对象创建后,使用try...finally语句,以保证fi.close在任何时候都可以被调用。

这里没有使用try...catch...finally,而是在外侧catch中捕获更基础的IOException类型的异常,这样可以让代码简化一些。

即使我使用了一些方式尽可能让这段示例代码简化,但可以看出,如果构造函数涉及异常,客户端代码就无可避免地变得更复杂,可能包含多层try...catch结构。

简化异常

虽然Java的异常机制看起来很好,但实际上给开发者添加了很多麻烦,尤其是“被检查的异常”。

开发者往往需要花费更多的时间在添加try...catch和异常说明上,如果涉及继承,那么无疑会更麻烦。

事实上我们可以在“不丢失异常”的情况下在某种程度上简化异常处理,以提高编码效率。

将异常输出到控制台

main作为Java程序的入口函数,同行可以添加异常声明,此时就可以省去相应的异常捕获代码,直接让相应的异常透过main传递给JVM,JVM会直接调用异常的printStackTrace,将异常信息输出到控制台:

package ch10.simple;

class MyExp extends Exception {
}

public class Main {
    public static void main(String[] args) throws MyExp {
        throw new MyExp();
    }
}
// Exception in thread "main" ch10.simple.MyExp
//         at ch10.simple.Main.main(Main.java:8)

转化为不被检查异常

如果将被检查的异常转化为不被检查的异常,那么自然也就不涉及相关问题:

package ch10.simple2;

class MyExp extends Exception {
}

public class Main {
    public static RuntimeException exchangeExp(Exception e) {
        return new RuntimeException(e);
    }

    public static void main(String[] args) {
        try {
            throw new MyExp();
        } catch (MyExp e) {
            throw exchangeExp(e);
        }
    }
}
// Exception in thread "main" java.lang.RuntimeException: ch10.simple2.MyExp
//         at ch10.simple2.Main.exchangeExp(Main.java:8)
//         at ch10.simple2.Main.main(Main.java:15)
// Caused by: ch10.simple2.MyExp
//         at ch10.simple2.Main.main(Main.java:13)

参考资料:

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值