Java异常处理

Java的异常

一个健壮的程序必须处理各种各样的错误。所谓错误就是程序调用某个函数的时候失败了。

调用方获取调用失败信息:约定返回错误码;在语言层面上提供一个异常处理机制。

Java内置了一套异常处理机制,总是使用异常来表示错误。异常是一种class,因此它本身带有类型信息。异常可以在任何地方抛出,但只需要在上层捕获。

 Throwable是异常体系的根,它继承自ObjectThrowable有两个体系:ErrorExceptionError表示严重的错误,程序对此一般无能为力。

  • OutOfMemoryError:内存耗尽
  • NoClassDefFoundError:无法加载某个Class
  • StackOverflowError:栈溢出

Exception是运行时的错误,可以被捕获并处理。某些异常是应用程序 逻辑处理的一部分,应该捕获并处理:

  • NumberFormatException:数值类型的格式错误
  • FileNotFoundException:未找到文件
  • SocketException:读取网络失败

还有一些异常是程序逻辑编写不对造成的,应该修复程序本身:

  • NullPointerException:对某个null的对象调用方法或字段
  • IndexOutOfBoundsException:数组索引越界

Exception又分为两大类:

        RuntimeException以及它的子类;

        非RuntimeException(包括IOException、ReflectiveOperation等等)

Java规定:

  • 必须捕获的异常,包括Exception及其子类,但不包括RuntimeException及其子类,这种类型的异常称为Checked Exception。

  • 不需要捕获的异常,包括Error及其子类,RuntimeException及其子类

捕获异常

捕获异常使用try...catch语句,把可能发生异常的代码放到try{...}中,然后使用catch捕获对应的Exception及其子类。

在方法定义的时候,使用throws Xxx表示该方法可能抛出的异常类型。调用方在调用的时候,必须强制捕获这些异常,否则编译器会报错。

所有异常都可以调用printStackTrace()方法打印异常栈,这是一个简单有用的快速打印异常的方法。

捕获异常

在Java中,凡是可能抛出异常的语句,都可以用try ... catch捕获。把可能发生异常的语句放在try { ... }中,然后使用catch捕获对应的Exception及其子类。

多catch语句

可以使用多个catch语句,每个catch分别捕获对应的Exception及其子类。JVM在捕获到异常后,会从上到下匹配catch语句,匹配到某个catch后,执行catch代码块,然后不再继续匹配。

存在多个catch的时候,catch的顺序非常重要:子类必须写在前面。

public static void main(String[] args) {
    try {
        process1();
        process2();
        process3();
    } catch (IOException e) {
        System.out.println("IO error");
    } catch (UnsupportedEncodingException e) { // 永远捕获不到
        System.out.println("Bad encoding");
    }
}

对于上面的代码,UnsupportedEncodingException异常是永远捕获不到的,因为它是IOException的子类。

finally语句

Java的try ... catch机制还提供了finally语句,保证有无错误都会执行。finally的特点:不是必须的,可写可不写;总是最后执行。

某些情况下,可以没有catch,只使用try...finally结构。

void process(String file) throws IOException {
    try {
        ...
    } finally {
        System.out.println("END");
    }
}

因为方法声明了可能抛出的异常,所以可以不写catch。

捕获多种异常

如果某些异常的处理逻辑相同,但是异常本身不存在继承关系,那么就得编写多条catch子句。如果两个异常的处理代码是相同的,可以把它用|合并到一起。

抛出异常

异常的传播

当某个方法抛出了异常时,如果当前方法没有捕获异常,异常就会被抛到上层调用方法,直到遇到某个try...catch被捕获为止。

通过printStackTrace()可以 打印出方法的调用栈,类似:

java.lang.NumberFormatException: null
    at java.base/java.lang.Integer.parseInt(Integer.java:614)
    at java.base/java.lang.Integer.parseInt(Integer.java:770)
    at Main.process2(Main.java:16)
    at Main.process1(Main.java:12)
    at Main.main(Main.java:5)

上述信息表示:NumberFormatException是在java.lang.Integer.parseInt方法中被抛出的,从下往上看,调用层次依次是:

  1. main()调用process1()
  2. process1()调用process2()
  3. process2()调用Integer.parseInt(String)
  4. Integer.parseInt(String)调用Integer.parseInt(String, int)

抛出异常

抛出异常分为两步:创建某个Exception的实例;用throw语句抛出。

void process2(String s) {
    if (s==null) {
        throw new NullPointerException();
    }
}

如果一个方法捕获了某个异常后,又在catch子句中抛出新的异常,就相当于把抛出的异常类型“转换”了。捕获到异常并再次抛出时,一定要留住原始异常,否则很难定位第一案发现场。

在catch中抛出异常,不会影响finally的执行。JVM会先执行finally,然后抛出异常。

异常屏蔽

finally抛出异常后,原来catch中准备抛出的异常就“消失”了,因为只能抛出一个异常。没有被抛出的异常称为“被屏蔽”的异常。

自定义异常

Java标准库定义的常用异常包括:

当我们在代码中需要抛出异常时,尽量使用JDK已定义的异常类型。

在一个大型项目中,可以自定义新的异常类型,但是,保持一个合理的异常继承体系是非常重要的。一个常见的做法是自定义一个BaseException作为“根异常”,然后,派生出各种业务类型的异常。BaseExcrption需要从一个适合的Exception派生,通常建议从RuntimeException派生。自定义的BaseException应该提供多个构造方法。

NullPointerException

NullPointerException即空指针异常,如果一个对象为null,调用其方法或访问其字段就会产生空指针异常,这个异常通常是由JVM抛出的。

处理NullPointerException

NullPointerException是一种代码逻辑错误。好的代码习惯可以极大地降低NullPointerException的产生:成员变量在定义式初始化;返回空字符串、空数组而不是null。如果调用方一定要根据null判断,那么考虑返回Optional<T>:

public Optional<String> readFromFile(String file) {
    if (!fileExist(file)) {
        return Optional.empty();
    }
    ...
}

这样调用方必须通过Optioanl.isPresent()判断是否有结果。

定位NullPointerException

调用a.b.c.x()时产生了NullPointerException,原因可能是:

  • anull
  • a.bnull
  • a.b.cnull

从Java 14开始,如果产生了空指针异常,JVM可以给出详细的信息告诉我们null对象到底是谁。

这种增强的NullPointerException详细信息是Java 14新增的功能,但默认是关闭的,我们可以给JVM添加一个-XX:+ShowCodeDetailsInExceptionMessages参数启用它。

使用断言

断言是一种调试程序的方式,在Java中,使用assert关键字来实现断言。 

public static void main(String[] args) {
    double x = Math.abs(-123.45);
    assert x >= 0;
    System.out.println(x);
}

语句assert x>=0;即为断言,断言条件x >=0预期为true。如果计算结果为false,则断言失败,抛出AssertionError。

Java断言的特点是:断言失败时会抛出AssertionError,导致程序结束退出。因此,断言不能用于可恢复的程序错误,只应用于开发和测试阶段。对于可恢复的程序错误,不应该使用断言。

JVM默认是关闭断言指令的,遇到assert语句就自动忽略了,不执行。要执行assert语句,必须给Java虚拟机传递-enableassertions参数启用断言。还可以有选择地对特定地类启用断言,命令行参数是:-ea:com.itranswarp.sample.Main,表示只对com.itranswarp.sample.Main这个类启用断言。或者对特定的包启用断言,命令行参数是:-ea:com.itranswarp.sample...(注意结尾有3个.),表示对com.itranswarp.sample这个包启动断言。

使用JDK Logging

输出日志的好处:

        可以设置输出样式,避免自己每次都写“ERROR:”+var;

        可以设置输出级别,进制某些级别输出;

        可以被重定向到文件,这样可以在程序运行结束后查看日志;

        可以按包名控制日志级别,只输出某些包打的日志;

        ...

Java标准库内置了日志包java.util.logging,我们可以直接用。

JDK的Logging定义了7个日志级别,从严重到普通:

  • SEVERE
  • WARNING
  • INFO
  • CONFIG
  • FINE
  • FINER
  • FINEST

因为默认级别是INFO,因此,INFO级别以下的日志,不会被打印出来。

使用Java标准库内置Logging有以下局限:

Loggering系统在JVM启动时读取配置文件并完成初始化,一旦开始运行main()方法,就无法修改配置;配置不太方便,需要在JVM启动时传递参数-Djava.util.logging.config.file=<config-file-name>。因此,Java标准库内置的Logging使用并不是非常广泛。

使用Commons Logging

Commons Logging是一个第三方日志库,是由Apache创建的日志模块。它的特色是可以挂接不同的日志系统,并通过配置文件指定挂接的日志系统。默认情况下,Commons Logging自动搜索并使用Log4j(Log4j是另一个流行的日志系统),如果没有找到Log4j,再使用JDK Logging。

使用Commons Logging只需要和两个类打交道,并且只有两步:第一步,通过LogFactory获取Log类的实例;第二步,使用Log实例的方法打日志。

Commons Logging是一个第三方提供的库,必须先把它下载下来,下载后解压,找到commons-logging-1.2.jar这个文件,再把Java源码Main.java放到一个目录下。编译的时候要指定classpath,不然编译器找不到我们引用的包。

使用Commons Logging时,如果在静态方法中引用Log,通常直接定义一个静态类型变量;在实例方法中引用Log,通常定义一个实例变量。

使用Log4j

Commons Logging可以作为“日志接口”来使用,真正的“日志实现”可以使用Log4j。

Log4j是一种非常流行的日志框架,最新版本是2.x。

Log4j是一个组件化设计的日志系统,架构大致如下:

当我们使用Log4j输出一条日志时,Log4j自动通过不同的Appender把同一条日志输出到不同的目的地。在输出日志的过程中,通过Filter来过滤哪些log需要被输出,哪些log不需要被输出。 

在开发阶段,始终使用Commons Logging接口来写入日志,并且开发阶段无需引入Log4j。如果需要把日志写入文件,只需要把正确的配置文件和Log4j相关的jar包放入classpath,就可以自动把日志切换成Log4j写入,无需修改任何代码。

使用SLF4J和Logback

SLF4J类似于Commons Logging,是一个日志接口,而Logback类似于Log4j,是一个日志的实现。

SLF4J的日志接口传入的是一个带占位符的字符串,用后面的变量自动替换占位符,看起来更加自然。

对比一下Commons Logging和SLF4J的接口:

不同之处就是Log变成了Logger,LogFactory变成了LoggerFactory。 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值