Java的异常是class,它的继承关系如下:
2.Java异常 - 图1
从继承关系可知:Throwable是异常体系的根,它继承自Object。Throwable有两个体系:Error和Exception,Error表示严重的错误,程序对此一般无能为力,例如:

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

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

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

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

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

Exception又分为两大类:

  1. RuntimeException以及它的子类;
  2. 非RuntimeException(包括IOException、ReflectiveOperationException等等)

Java规定:

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

    1.捕获异常

    捕获异常使用try…catch语句,把可能发生异常的代码放到try {…}中,然后使用catch捕获对应的Exception及其子类。
    Java使用异常来表示错误,并通过try … catch捕获异常;
    Java的异常是class,并且从Throwable继承;
    Error是无需捕获的严重错误,Exception是应该捕获的可处理的错误;
    RuntimeException无需强制捕获,非RuntimeException(Checked Exception)需强制捕获,或者用throws声明;
    不推荐捕获了异常但不进行任何处理。

    1.多catch语句

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

    2.finally语句

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

如果没有发生异常,就正常执行try { … }语句块,然后执行finally。如果发生了异常,就中断执行try { … }语句块,然后跳转执行匹配的catch语句块,最后执行finally。
可见,finally是用来保证一些代码必须执行的。

3.捕获多种异常

如果某些异常的处理逻辑相同,但是异常本身不存在继承关系,那么就得编写多条catch子句。
使用try … catch … finally时:

  • 多个catch语句的匹配顺序非常重要,子类必须放在前面;
  • finally语句保证了有无异常都会执行,它是可选的;
  • 一个catch语句也可以匹配多个非继承关系的异常。

    2.抛出异常

    1.异常的传播

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

    2.抛出异常

    当发生错误时,例如,用户输入了非法的字符,我们就可以抛出异常。
    如何抛出异常?参考Integer.parseInt()方法,抛出异常分两步:
  1. 创建某个Exception的实例;
  2. 用throw语句抛出。

调用printStackTrace()可以打印异常的传播栈,对于调试非常有用;
捕获异常并再次抛出新的异常时,应该持有原始异常信息;
通常不要在finally中抛出异常。如果在finally中抛出异常,应该原始异常加入到原有异常中。调用方可通过Throwable.getSuppressed()获取所有添加的Suppressed Exception。

3.自定义异常

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

  1. public class BaseException extends RuntimeException {
  2. }

其他业务类型的异常就可以从BaseException派生:

  1. public class UserNotFoundException extends BaseException {
  2. }
  3. public class LoginFailedException extends BaseException {
  4. }
  5. ...

自定义的BaseException应该提供多个构造方法:

  1. public class BaseException extends RuntimeException {
  2. public BaseException() {
  3. super();
  4. }
  5. public BaseException(String message, Throwable cause) {
  6. super(message, cause);
  7. }
  8. public BaseException(String message) {
  9. super(message);
  10. }
  11. public BaseException(Throwable cause) {
  12. super(cause);
  13. }
  14. }

抛出异常时,尽量复用JDK已定义的异常类型;
自定义异常体系时,推荐从RuntimeException派生“根异常”,再派生出业务异常;
自定义异常时,应该提供多种构造方法。

4.NullPointerException

Java语言中并无指针。我们定义的变量实际上是引用,Null Pointer更确切地说是Null Reference,不过两者区别不大。

1.处理NullPointerException

如果遇到NullPointerException,我们应该如何处理?首先,必须明确,NullPointerException是一种代码逻辑错误,遇到NullPointerException,遵循原则是早暴露,早修复,严禁使用catch来隐藏这种编码错误。
NullPointerException是Java代码常见的逻辑错误,应当早暴露,早修复;
可以启用Java 14的增强异常信息来查看NullPointerException的详细错误信息。

5.使用断言

断言(Assertion)是一种调试程序的方式。在Java中,使用assert关键字来实现断言。
语句assert x >= 0;即为断言,断言条件x >= 0预期为true。如果计算结果为false,则断言失败,抛出AssertionError。
使用assert语句时,还可以添加一个可选的断言消息:

  1. assert x >= 0 : "x must >= 0";

这样,断言失败的时候,AssertionError会带上消息x must >= 0,更加便于调试。
Java断言的特点是:断言失败时会抛出AssertionError,导致程序结束退出。因此,断言不能用于可恢复的程序错误,只应该用于开发和测试阶段。
对于可恢复的程序错误,不应该使用断言。
实际开发中,很少使用断言。更好的方法是编写单元测试。
断言是一种调试方式,断言失败会抛出AssertionError,只能在开发和测试阶段启用断言;
对可恢复的错误不能使用断言,而应该抛出异常;
断言很少被使用,更好的方法是编写单元测试。

6.JDK Logging

  1. public class Hello {
  2. public static void main(String[] args) {
  3. Logger logger = Logger.getGlobal();
  4. logger.info("info");
  5. logger.warning("warning");
  6. logger.fine("fine");
  7. logger.severe("severe");
  8. }
  9. }

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

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

因为默认级别是INFO,因此,INFO级别以下的日志,不会被打印出来。使用日志级别的好处在于,调整级别,就可以屏蔽掉很多调试相关的日志输出。

7.Commons Logging

Commons Logging是一个第三方日志库,它是由Apache创建的日志模块。
Commons Logging的特色是,它可以挂接不同的日志系统,并通过配置文件指定挂接的日志系统。默认情况下,Commons Loggin自动搜索并使用Log4j(Log4j是另一个流行的日志系统),如果没有找到Log4j,再使用JDK Logging。
使用Commons Logging只需要和两个类打交道,并且只有两步:
第一步,通过LogFactory获取Log类的实例; 第二步,使用Log实例的方法打日志。
需要commons-logging-1.2.jar文件
Commons Logging定义了6个日志级别:

  • FATAL
  • ERROR
  • WARNING
  • INFO
  • DEBUG
  • TRACE

默认级别是INFO。
使用Commons Logging时,如果在静态方法中引用Log,通常直接定义一个静态类型变量:

  1. static final Log log = LogFactory.getLog(Main.class);

在实例方法中引用Log,通常定义一个实例变量:

  1. protected final Log log = LogFactory.getLog(getClass());

8.使用Log4j

把一个log4j2.xml的文件放到classpath下就可以让Log4j读取配置文件并按照我们的配置来输出日志。下面是一个配置文件的例子:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <Configuration>
  3. <Properties>
  4. <!-- 定义日志格式 -->
  5. <Property name="log.pattern">%d{MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36}%n%msg%n%n</Property>
  6. <!-- 定义文件名变量 -->
  7. <Property name="file.err.filename">log/err.log</Property>
  8. <Property name="file.err.pattern">log/err.%i.log.gz</Property>
  9. </Properties>
  10. <!-- 定义Appender,即目的地 -->
  11. <Appenders>
  12. <!-- 定义输出到屏幕 -->
  13. <Console name="console" target="SYSTEM_OUT">
  14. <!-- 日志格式引用上面定义的log.pattern -->
  15. <PatternLayout pattern="${log.pattern}" />
  16. </Console>
  17. <!-- 定义输出到文件,文件名引用上面定义的file.err.filename -->
  18. <RollingFile name="err" bufferedIO="true" fileName="${file.err.filename}" filePattern="${file.err.pattern}">
  19. <PatternLayout pattern="${log.pattern}" />
  20. <Policies>
  21. <!-- 根据文件大小自动切割日志 -->
  22. <SizeBasedTriggeringPolicy size="1 MB" />
  23. </Policies>
  24. <!-- 保留最近10份 -->
  25. <DefaultRolloverStrategy max="10" />
  26. </RollingFile>
  27. </Appenders>
  28. <Loggers>
  29. <Root level="info">
  30. <!-- 对info级别的日志,输出到console -->
  31. <AppenderRef ref="console" level="info" />
  32. <!-- 对error级别的日志,输出到err,即上面定义的RollingFile -->
  33. <AppenderRef ref="err" level="error" />
  34. </Root>
  35. </Loggers>
  36. </Configuration>

把以下jar包放到classpath中:

  • log4j-api-2.x.jar
  • log4j-core-2.x.jar
  • log4j-jcl-2.x.jar
  • commons-logging-1.2.jar

    9.使用SLF4J和Logback

    对比一下Commons Logging和SLF4J的接口:
Commons Logging SLF4J
org.apache.commons.logging.Log org.slf4j.Logger
org.apache.commons.logging.LogFactory org.slf4j.LoggerFactory

不同之处就是Log变成了Logger,LogFactory变成了LoggerFactory。
把以下jar包放到classpath下:

  • slf4j-api-1.7.x.jar
  • logback-classic-1.2.x.jar
  • logback-core-1.2.x.jar

然后使用SLF4J的Logger和LoggerFactory即可。和Log4j类似,我们仍然需要一个Logback的配置文件,把logback.xml放到classpath下,配置如下:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <configuration>
  3. <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
  4. <encoder>
  5. <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
  6. </encoder>
  7. </appender>
  8. <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
  9. <encoder>
  10. <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
  11. <charset>utf-8</charset>
  12. </encoder>
  13. <file>log/output.log</file>
  14. <rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
  15. <fileNamePattern>log/output.log.%i</fileNamePattern>
  16. </rollingPolicy>
  17. <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
  18. <MaxFileSize>1MB</MaxFileSize>
  19. </triggeringPolicy>
  20. </appender>
  21. <root level="INFO">
  22. <appender-ref ref="CONSOLE" />
  23. <appender-ref ref="FILE" />
  24. </root>
  25. </configuration>

使用:

  1. final Logger logger = LoggerFactory.getLogger(getClass());