Skip to content

1、java异常类层次结构

image.png

在java中,所有的异常都是由**Throwable**继承而来,Throwable有两个体系:ErrorException,继承关系如下:

**Error**表示Java运行时系统的内部错误和资源耗尽错误,程序一般对此无能为力,例如:

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

**Exception**异常是设计java程序时需要关注的异常,包括两个分支:

  • **RuntimeException**,运行时异常,程序错误导致的异常
  • **RuntimeException**异常 (包括IOExceptionSQLExceptionReflectiveOperationException等等) ,这类异常是应用程序逻辑处理的一部分,应该捕获并处理

java语言规范规定:

  • 非受查**(**unchecked**)**(异常必须捕获的异常):包括Exception及其子类,但不包括RuntimeException及其子类
  • 受查**(**checked**)**异常(不需要捕获的异常):派生于Error类或RuntimeException类的所有异常。

2、异常使用

2.1声明异常

方法应该在其首部声明所有可能抛出的异常。这样可以从首部反映出这个方法可能抛出哪类受查异常。 异常声明使用**throws**关键字,放在方法签名的尾部。

java
public void hello() throws Exception{
}

调用方在调用声明异常的方法时,必须抛出或捕获这些异常,否则编译器会报错。

2.2 抛出异常

java
throw new Exception()

2.2.1 异常的传递:

当某个方法抛出了异常时,如果当前方法没有捕获异常,异常就会被抛到上层调用方法,直到遇到某个**try ... catch**被捕获为止: 通过**printStackTrace()**可以打印出方法的调用栈,类似:

java
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)

catch子句中也可以抛出一个新的异常,这样做的目的是改变异常的类型:

java
try {
    //业务代码
} catch (XXException e) {
    throw new ZZZException();
}

但是这种方式将会丢失原始的异常类型,可以通过在构造异常的时候,把原始的Exception实例传进去,新的Exception就可以持有原始Exception信息

java
try {
    //业务代码
} catch (XXException e) {
    throw new ZZZException(e);
}

2.3异常捕获

如果某个异常发生的时候没有在任何地方进行捕获,那程序就会终止执行,并在控制台上打印出异常信息,其中包括异常的类型和堆栈的内容。

**try ... catch**语句可以捕获异常:

java
try{
    // 业务代码
}catch(Exception e){
    异常处理代码
}

try语句块的任何代码抛出一个在catch子句中说明的异常类时,程序将立即跳出try语句块,执行catch字句中的代码。

2.3.1 捕获多个异常

在一个try语句块中可以捕获多个异常类型,并对不同类型的异常做出不同的处理。 存在多个catch的时候,catch的顺序非常重要:子类必须写在前面。 比如下面的代码,当try中的代码抛出UnsupportedEncodingException异常时候,会被前面的IOException** **捕获并执行。

java
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");
    }
}

2.3.2 finally语句

无论是否有异常发生,如果我们都希望执行一些语句,**finally**语句块保证有无错误都会执行。

finally语句的特点特点:

  • finally语句不是必须的,可写可不写;
  • finally总是最后执行。

2.4 自定义异常

java代码库定义的常用异常

在项目中,经常会遇到任何标准异常类都没有能够充分地描述清楚的问题,这时可以创建新的异常类型。

异常类通常是通过类名来区分不同程序异常的,比如我们看到在异常SQLException,就会知道是SQL执行的异常,所以我们自定义异常时也应该结合业务类型定义异常类名。

通常情况下,定义新的异常类只需派生于Exception,或者派生于Exception子类的类。同时应该包含两个构造器,一个是默认的构造器,另一个是带有详细描述信息的构造器:

java
public class BaseException extends RuntimeException {
    public BaseException() {
        super()
    }
    public BaseException(String message) {
        super(message);
    }
}

2.5 try with resource

传统的手动释放外部资源一般放在一般放在**try.catch.finally**机制的finally代码块中,JDK1.7之后有了**try-with-resource**处理机制,可以更加方便的自动释放资源。

**注意:**使用**try-with-resource**要求被自动关闭的资源需要实现**Closeable**或者**AutoCloseable**接口)

带资源的try语句(try-with-resources)的最简形式为: try (Resource res){},try块退出时,会自动调用res.clos(),简化了关闭连接时出现异常的处理。

java
try( InputStrem inputStrem = new FileInputStream("test.gz" )){
	
}

3、断言

断言(Assertion)是一种调试程序的方式,断言机制允许在测试期间向代码中插入一些检查语句。当代码发布时, 这些插入的检测语句将会被自动地移走。在Java中,使用assert关键字来实现断言。

assert有两种形式:

  • assert 条件 ;
  • assert 条件 : 表达式; 这两种形式都会对条件进行检测,如果结果为false,则抛出AssertionError异常。在第二种形式中,表达式将被传入AssertionError的构造器,并转换成一个消息字符串。

PS:实际开发中,很少使用断言。更好的方法是编写单元测试。

4、日志

在开发中,会经常使用System.out.println方法调用来帮助观察程序运行的操作过程,记录日志API就是为了简化这个操作而设计的。

使用日志有以下几点好处:

  1. 可以设置输出样式,避免自己每次都写"ERROR: " + var;
  2. 可以设置输出级别,禁止某些级别输出。例如,只输出错误日志;
  3. 可以被重定向到文件,这样可以在程序运行结束后查看日志;
  4. 可以按包名控制日志级别,只输出某些包打的日志;

4.1 Commons Logging 与 Log4的使用

Java标准库内置了日志包**java.util.logging**,但是它有一些局限,使用并不是很广泛。

**Commons Logging**是一个第三方日志库,它是由Apache创建的日志模块。可以作为**日志接口**来使用。而真正的**日志实现**可以使用Log4j

**Commons Logging**定义了6个日志级别:

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

使用**Commons Logging**只需要和两个类打交道,并且只有两步:

第一步,通过LogFactory获取Log类的实例; 第二步,使用Log实例的方法打日志。

java
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class Main {
    public static void main(String[] args) {
        Log log = LogFactory.getLog(Main.class);
        log.info("start...");
        log.warn("end.");
    }
}

Log4j作为真正的日志实现,在实际的使用中,只需要配置配置文件,不需要关心它的API。

4.2 SLF4J和Logback 的使用

SLF4JLogback是另一对好搭档。**SLF4J**类似于Commons Logging,也是一个日志接口,而**Logback**类似于Log4j,是一个日志的实现。

SLF4J使用与Commons Logging类似:

java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

class Main {
    final Logger logger = LoggerFactory.getLogger(getClass());
    logger.ingo("")
}

参考: java核心技术卷1 廖雪峰java教程 java编程思想