Java异常处理基础知识笔记:异常处理机制、异常继承关系、捕获异常、抛出异常、异常的传播、异常调用栈、自定义异常、第三方日志库
一、Java中的异常
1、Java内置了一套异常处理机制,总是使用异常来表示错误。异常是一种class
,因此它本身带有类型信息。异常可以在任何地方抛出,但只需要在上层捕获,这样就和方法调用分离了
2、Java异常的继承关系
从继承关系可知:Throwable
是异常体系的根,它继承自Object
。Throwable
有两个体系:Error
和Exception
,Error
表示严重的错误,程序对此一般无能为力。如:
OutOfMemoryError
:内存耗尽NoClassDefFoundError
:无法加载某个ClassStackOverflowError
:栈溢出
而Exception
则是运行时的错误,它可以被捕获并处理。某些异常是应用程序逻辑处理的一部分,应该捕获并处理。例如:
NumberFormatException
:数值类型的格式错误FileNotFoundException
:未找到文件SocketException
:读取网络失败
还有一些异常是程序逻辑编写不对造成的,应该修复程序本身。例如:
NullPointerException
:对某个null
的对象调用方法或字段IndexOutOfBoundsException
:数组索引越界
Exception
又分为两大类:
(1)RuntimeException
以及它的子类;
(2)非RuntimeException
(包括IOException
、ReflectiveOperationException
等等)
3、Java规定:
-
必须捕获的异常,包括
Exception
及其子类,但不包括RuntimeException
及其子类,这种类型的异常称为Checked Exception。 -
不需要捕获的异常,包括
Error
及其子类,RuntimeException
及其子类。
4、捕获异常使用try...catch
语句,把可能发生异常的代码放到try {...}
中,然后使用catch
捕获对应的Exception
及其子类
5、在方法定义的时候,使用throws Xxx
表示该方法可能抛出的异常类型。调用方在调用的时候,必须强制捕获这些异常,否则编译器会报错。
6、只要是方法声明的Checked Exception,不在调用层捕获,也必须在更高的调用层捕获。所有未捕获的异常,最终也必须在main()
方法中捕获,不会出现漏写try
的情况。这是由编译器保证的。main()
方法也是最后捕获Exception
的机会。
7、不推荐捕获了异常但不进行任何处理。所有异常都可以调用printStackTrace()
方法打印异常栈,这是一个简单有用的快速打印异常的方法。
二、捕获异常
1、可以使用多个catch
语句,每个catch
分别捕获对应的Exception
及其子类。JVM在捕获到异常后,会从上到下匹配catch
语句,匹配到某个catch
后,执行catch
代码块,然后不再继续匹配。简单地说就是:多个catch
语句只有一个能被执行。
2、存在多个catch
的时候,catch
的顺序非常重要:子类必须写在前面。
public static void main(String[] args) {
try {
process1();
} catch (IOException e) {
System.out.println("IO error");
} catch (UnsupportedEncodingException e) { // 永远捕获不到
System.out.println("Bad encoding");
}
}
对于上面的代码,UnsupportedEncodingException
异常是永远捕获不到的,因为它是IOException
的子类。当抛出UnsupportedEncodingException
异常时,会被catch (IOException e) { ... }
捕获并执行。因此,正确的写法是把子类放到前面。
3、finally语句 —— finally
是用来保证一些代码必须执行的
无论是否有异常发生,如果我们都希望执行一些语句,例如清理工作,怎么写? —— Java的try ... catch
机制还提供了finally
语句,finally
语句块保证有无错误都会执行。注意finally
有几个特点:
(1)finally
语句不是必须的,可写可不写;
(2)finally
总是最后执行。
4、捕获多种异常:如果某些异常的处理逻辑相同,但是异常本身不存在继承关系,那么就得编写多条catch
子句,因为处理IOException
和NumberFormatException
的代码是相同的,所以我们可以把它两用|
合并到一起。
public static void main(String[] args) {
try {
} catch (IOException | NumberFormatException e) { // IOException或NumberFormatException
System.out.println("Bad input");
} catch (Exception e) {
System.out.println("Unknown error");
}
}
三、抛出异常
1、异常的传播:当某个方法抛出了异常时,如果当前方法没有捕获异常,异常就会被抛到上层调用方法,直到遇到某个try ... catch
被捕获为止。
2、如何抛出异常?参考Integer.parseInt()
方法,抛出异常分两步:实际上,绝大部分抛出异常的代码都会合并写成一行:throw new NullPointerException();
// 1、创建某个Exception的实例
NullPointerException e = new NullPointerException();
// 2、用throw语句抛出
throw e
3、异常屏蔽:在finally里
抛出异常后,原来在catch
中准备抛出的异常就“消失”了,因为只能抛出一个异常。没有被抛出的异常称为“被屏蔽”的异常(Suppressed Exception)。
四、自定义异常
1、Java标准库定义的常用异常包括
Exception
│
├─ RuntimeException
│ │
│ ├─ NullPointerException
│ │
│ ├─ IndexOutOfBoundsException
│ │
│ ├─ SecurityException
│ │
│ └─ IllegalArgumentException
│ │
│ └─ NumberFormatException
│
├─ IOException
│ │
│ ├─ UnsupportedCharsetException
│ │
│ ├─ FileNotFoundException
│ │
│ └─ SocketException
│
├─ ParseException
│
├─ GeneralSecurityException
│
├─ SQLException
│
└─ TimeoutException
2、在一个大型项目中,可以自定义新的异常类型,但是,保持一个合理的异常继承体系是非常重要的。一个常见的做法是自定义一个BaseException
作为“根异常”,然后,派生出各种业务类型的异常。
3、BaseException
需要从一个适合的Exception
派生,通常建议从RuntimeException
派生:
public class BaseException extends RuntimeException {
}
// 其他业务类型的异常就可以从BaseException派生:
public class UserNotFoundException extends BaseException {
}
public class LoginFailedException extends BaseException {
}
4、自定义的BaseException
应该提供多个构造方法:
5、小结:
抛出异常时,尽量复用JDK已定义的异常类型;
自定义异常体系时,推荐从RuntimeException
派生“根异常”,再派生出业务异常;
自定义异常时,应该提供多种构造方法。
五、NullPointerException
NullPointerException
即空指针异常,俗称NPE。如果一个对象为null
,调用其方法或访问其字段就会产生NullPointerException
,这个异常通常是由JVM抛出的。
指针这个概念实际上源自C语言,Java语言中并无指针。我们定义的变量实际上是引用,Null Pointer更确切地说是Null Reference,不过两者区别不大。
六、使用JDK Logging
日志是为了替代System.out.println()
,可以定义格式,重定向到文件等;
日志可以存档,便于追踪问题;
日志记录可以按级别分类,便于打开或关闭某些级别;
可以根据配置文件调整日志,无需修改代码;
Java标准库提供了java.util.logging
来实现日志功能。
七、第三方日志库
1、Commons Logging 与 Log4j
通过Commons Logging实现日志,不需要修改代码即可使用Log4j;
使用Log4j只需要把log4j2.xml和相关jar放入classpath;
如果要更换Log4j,只需要移除log4j2.xml和相关jar;
只有扩展Log4j时,才需要引用Log4j的接口(例如,将日志加密写入数据库的功能,需要自己开发)。
2、SLF4J和Logback
SLF4J和Logback可以取代Commons Logging和Log4j;始终使用SLF4J的接口写入日志,使用Logback只需要配置,不需要修改代码。