学习目标
- 什么是异常
- 异常的结构
- 异常怎么处理
- 异常怎么捕获
- 异常怎么抛出
- 怎么自定义异常
- 熟悉一种日志记录框架
基础知识
异常
异常是程序中的一些错误,但并不是所有的错误都是异常,并且错误有时候是可以避免的。
异常发生的原因有很多,通常包含以下几大类:
- 用户输入了非法数据。
- 要打开的文件不存在。
- 网络通信时连接中断,或者JVM内存溢出。
这些异常有的是因为用户错误引起,有的是程序错误引起的,还有其它一些是因为物理错误引起的。
要理解Java异常处理是如何工作的,你需要掌握以下三种类型的异常:
- 检查性异常:最具代表的检查性异常是用户错误或问题引起的异常,这是程序员无法预见的。例如要打开一个不存在文件时,一个异常就发生了,这些异常在编译时不能被简单地忽略。
- 运行时异常: 运行时异常是可能被程序员避免的异常。与检查性异常相反,运行时异常可以在编译时被忽略。
- 错误: 错误不是异常,而是脱离程序员控制的问题。错误在代码中通常被忽略。例如,当栈溢出时,一个错误就发生了,它们在编译也检查不到的。
Java 程序通常不捕获错误。错误一般发生在严重故障时,它们在Java程序处理的范畴之外。
Error 用来指示运行时环境发生的错误。
异常的结构
从继承关系可知:Throwable
是异常体系的根,它继承自Object
。Throwable
有两个体系:Error
和Exception
,Error
表示严重的错误,程序对此一般无能为力:
- OutOfMemoryError:内存耗尽
- NoClassDefFoundError:无法加载某个Class
- StackOverflowError:栈溢出
而Exception则是运行时的错误,它可以被捕获并处理。
某些异常是应用程序逻辑处理的一部分,应该捕获并处理。例如:
- NumberFormatException:数值类型的格式错误
- FileNotFoundException:未找到文件
- SocketException:读取网络失败
还有一些异常是程序逻辑编写不对造成的,应该修复程序本身。例如:
- NullPointerException:对某个null的对象调用方法或字段
- IndexOutOfBoundsException:数组索引越界
Exception
又分为两大类:
RuntimeException
以及它的子类;- 非
RuntimeException
(包括IOException、ReflectiveOperationException等等)
异常的捕获
Java规定:
- 必须捕获的异常,包括
Exception及其子类
,但不包括RuntimeException及其子类
,这种类型的异常称为Checked Exception。 - 不需要捕获的异常,包括
Error及其子类
,RuntimeException
及其子类。
捕获异常使用try...catch
语句,把可能发生异常的代码放到try {…}中,然后使用catch捕获对应的Exception
及其子类:
//常规的异常捕获
try
{
// 程序代码
}catch(ExceptionName e1)
{
//Catch 块
}
//多重异常捕获
try{
// 程序代码
}catch(异常类型1 异常的变量名1){
// 程序代码
}catch(异常类型2 异常的变量名2){
// 程序代码
}catch(异常类型3 异常的变量名3){
// 程序代码
}
//完成的异常捕捉,无论是否发生异常,finally 代码块中的代码总会被执行。
try{
// 程序代码
}catch(异常类型1 异常的变量名1){
// 程序代码
}catch(异常类型2 异常的变量名2){
// 程序代码
}finally{
// 程序代码
}
//处理IOException和NumberFormatException的代码是相同的,所以我们可以把它两用|合并到一
public static void main(String[] args) {
try {
process1();
process2();
process3();
} catch (IOException | NumberFormatException e) { // IOException或NumberFormatException
System.out.println("Bad input");
} catch (Exception e) {
System.out.println("Unknown error");
}
}
技巧:
-
不推荐捕获了异常但不进行任何处理
。//错误示例 static byte[] toGBK(String s) { try { return s.getBytes("GBK"); } catch (UnsupportedEncodingException e) { // 什么也不干 } return null; } //正确示例 static byte[] toGBK(String s) { try { return s.getBytes("GBK"); } catch (UnsupportedEncodingException e) { // 先记下来再说: e.printStackTrace(); } return null; }
-
所有异常都可以调用
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)
-
finally语句保证了有无异常都会执行,它是可选的。
异常的抛出
使用throws
或throw
抛出异常
①使用 throws
关键字来声明,throws 关键字放在方法签名的尾部。
import java.io.*;
public class className
{
public void deposit(double amount) throws RemoteException
{
// Method implementation
throw new RemoteException();
}
//Remainder of class definition
}
//一个方法可以声明抛出多个异常,多个异常之间用逗号隔开
import java.io.*;
public class className
{
public void withdraw(double amount) throws RemoteException,
InsufficientFundsException
{
// Method implementation
}
//Remainder of class definition
}
②使用throw
抛出异常
void process2(String s) {
if (s==null) {
//创建某个Exception的实例;
NullPointerException e = new NullPointerException();
//用throw语句抛出。
throw e;
}
}
//简写
void process2(String s) {
if (s==null) {
throw new NullPointerException();
}
}
//保持原有的异常信息,抛出一个新异常
public class Main {
public static void main(String[] args) {
try {
process1();
} catch (Exception e) {
e.printStackTrace();
}
}
static void process1() {
try {
process2();
} catch (NullPointerException e) {
throw new IllegalArgumentException(e);
}
}
static void process2() {
throw new NullPointerException();
}
}
③当catch
和finally
都抛出了异常,最后以finally
抛出的异常为最终结果。
在极少数的情况下,我们需要获知所有的异常。如何保存所有的异常信息?方法是先用origin
变量保存原始异常,然后调用Throwable.addSuppressed()
,把原始异常添加进来,最后在finally
抛出:
public class Main {
public static void main(String[] args) throws Exception {
Exception origin = null;
try {
System.out.println(Integer.parseInt("abc"));
} catch (Exception e) {
origin = e;
throw e;
} finally {
Exception e = new IllegalArgumentException();
if (origin != null) {
e.addSuppressed(origin);
}
throw e;
}
}
}
自定义异常
Java标准库定义的常用异常包括:
Exception
│
├─ RuntimeException
│ │
│ ├─ NullPointerException
│ │
│ ├─ IndexOutOfBoundsException
│ │
│ ├─ SecurityException
│ │
│ └─ IllegalArgumentException
│ │
│ └─ NumberFormatException
│
├─ IOException
│ │
│ ├─ UnsupportedCharsetException
│ │
│ ├─ FileNotFoundException
│ │
│ └─ SocketException
│
├─ ParseException
│
├─ GeneralSecurityException
│
├─ SQLException
│
└─ TimeoutException
-
当我们在代码中需要抛出异常时,尽量使用JDK已定义的异常类型。例如,参数检查不合法,应该抛出
IllegalArgumentException
:static void process1(int age) { if (age <= 0) { throw new IllegalArgumentException(); } }
-
自定义异常体系时,推荐从
RuntimeException
派生“根异常”,再派生出业务异常;public class BaseException extends RuntimeException { } public class UserNotFoundException extends BaseException { } public class LoginFailedException extends BaseException { }
-
自定义异常时,应该提供多种构造方法。
public class BaseException extends RuntimeException { public BaseException() { super(); } public BaseException(String message, Throwable cause) { super(message, cause); } public BaseException(String message) { super(message); } public BaseException(Throwable cause) { super(cause); } }
日志
为了程序的健壮性、可维护性我们加了异常捕捉,通过记录日志我们可以将日常程序中的操作信息、异常信息记录下来,帮助我们更好的完善软件。
输出日志,而不是用System.out.println()
,有以下几个好处:
- 可以设置输出样式,避免自己每次都写"ERROR: " + var;
- 可以设置输出级别,禁止某些级别输出。例如,只输出错误日志;
- 可以被重定向到文件,这样可以在程序运行结束后查看日志;
- 可以按包名控制日志级别,只输出某些包打的日志;
- 可以将日志存储到本地或者数据库
使用JDK Logging
JDK的Logging定义了7
个日志级别,从严重到普通:
- SEVERE
- WARNING
- INFO
- CONFIG
- FINE
- FINER
- FINEST
因为默认级别是INFO
,因此,INFO级别以下的日志,不会被打印出来。使用日志级别的好处在于,调整级别,就可以屏蔽掉很多调试相关的日志输出。
// logging
import java.util.logging.Level;
import java.util.logging.Logger;
public class Hello {
public static void main(String[] args) {
Logger logger = Logger.getGlobal();
logger.info("start process...");
logger.warning("memory is running out...");
logger.fine("ignored.");
logger.severe("process will be terminated...");
}
}
输出信息:
Mar 02, 2019 6:32:13 PM Hello main
INFO: start process...
Mar 02, 2019 6:32:13 PM Hello main
WARNING: memory is running out...
Mar 02, 2019 6:32:13 PM Hello main
SEVERE: process will be terminated...
缺点
:Logging系统在JVM启动时读取配置文件并完成初始化,一旦开始运行main()方法,就无法修改配置;配置不太方便,需要在JVM启动时传递参数-Djava.util.logging.config.file=<config-file-name>
。
使用Commons Logging
和Java标准库提供的日志不同,Commons Logging是一个第三方日志库,它是由Apache创建的日志模块。
Commons Logging
的特色是,①它可以挂接不同的日志系统,并通过配置文件指定挂接的日志系统。②默认情况下,Commons Loggin自动搜索并使用Log4j(Log4j是另一个流行的日志系统),如果没有找到Log4j,再使用JDK Logging。
- 使用Commons Logging只需要和两个类打交道,并且只有两步:
第一步,通过LogFactory
获取Log
类的实例; 第二步,使用Log
实例的方法打日志。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."); } }
Commons Logging
定义了6
个日志级别:- FATAL
- ERROR
- WARNING
- INFO
- DEBUG
- TRACE
默认级别是INFO
- 在实例方法中引用Log,通常定义一个实例变量:
// 在实例方法中引用Log: public class Person { protected final Log log = LogFactory.getLog(getClass()); void foo() { log.info("foo"); } }
- 除了标准的
info(String)
外,还提供了一个非常有用的重载方法:info(String, Throwable)
,这使得记录异常更加简单。try { ... } catch (Exception e) { log.error("got exception!", e); }
使用Log4j
待补充
使用SLF4J和Logback
待补充
思考题
-
以下代码执行后输出结果为(A)
public class Test { public static void main(String[] args) { System.out.println("return value of getValue(): " + getValue()); } public static int getValue() { try { return 0; } finally { return 1; } } }
A:return value of getValue(): 1
B:return value of getValue(): 0
C:return value of getValue(): 0return value of getValue(): 1
D:return value of getValue(): 1return value of getValue(): 0