在 Java 语言中,将程序执行中发生的不正常情况称为“异常”(开发过程中的语法错误和逻辑错误不是异常)。 Java 程序在执行过程中所发生的异常事件可分为两类(都是属于 Throwable 下的):
一个简单的异常演示:
public class TestExceptionOne {
public static void main(String[] args) {
String str = null;
str.indexOf("zx"); // NullPointerException
System.out.println("--------");
}
}
程序会发生 NullPointerException 具体过程如下:
当执行 str.indexOf("zx") 的时候, Java 发现 str 的值为 null ,无法继续执行,于是启用异常处理机制,首先创建一个异常对象,这里是类 NullPointerException 的对象,然后查找看谁能处理这个异常,在上面的代码中,没有代码能处理这个异常,于是 Java 就启用默认处理机制(异常发生点后的代码都不会执行),那就是打印异常栈信息到屏幕,并退出程序。异常栈信息包括了从异常发生点到最上层调用者的轨迹、行号等。
再看一个例子:
import java.util.Scanner;
public class TestExceptionOne {
public static void main(String[] args) {
System.out.println("请输入一串数字: ");
Scanner cin = new Scanner(System.in);
String str = cin.next();
int num = Integer.parseInt(str);
System.out.println(num);
}
}
从这个例子来看,为什么需要处理异常呢?
如果用户输入的不是数字,最后就会抛出 NumberFormatException 异常,对于屏幕输出中的异常栈信息,使用的用户无法理解,也不知道该怎么办,我们需要给用户一个较为友好的信息,告诉用户,他应该输入的是数字,要做到这一点,我们需要自己"捕获"异常。
查看 NumberFormatException 和 Integer 的源码可以发现:

分析:
上面的程序改进加上异常处理 try - catch :
import java.util.Scanner;
public class TestExceptionOne {
public static void main(String[] args) {
System.out.println("请输入一串数字: ");
Scanner cin = new Scanner(System.in);
try {
String str = cin.next();
int num = Integer.parseInt(str);
System.out.println(num);
}catch (NumberFormatException e){
System.out.println("输入的不是一串数字..");
}
}
}
捕获异常后,程序就不会异常退出了,但 try 语句内异常点之后的其他代码就不会执行了,执行完 catch 内的语句后,程序会继续执行 catch 大括号外的代码。

整体的架构图:

对于这些错误,一般有两种解决方法:
捕获错误最理想的是在编译期间,但有的错误只有在运行时才会发生。比如:除数为 0 ,数组下标越界等
关于 Throwable 类:
有四个构造方法:
public Throwable()
public Throwable(String message)
public Throwable(String message, Throwable cause)
public Throwable(Throwable cause)
有两个主要参数,一个是 message ,表示异常消息,另一个是 cause ,表示触发该异常的其他异常。异常可以形成一个异常链,上层的异常由底层异常触发, cause 表示底层异常。
Throwable 还有一个 public 方法用于设置 cause :
Throwable initCause(Throwable cause)
Throwable 的某些子类没有带 cause 参数的构造方法,就可以通过这个方法来设置,这个方法最多只能被调用一次。
所有构造方法中都有一句重要的函数调用:它会将异常栈信息保存下来,这是我们能看到异常栈的关键。
fillInStackTrace();
Throwable 有一些常用方法用于获取异常信息:
void printStackTrace() // 打印异常栈信息到标准错误输出流,
//它还有两个重载的方法:打印栈信息到指定的流。
void printStackTrace(PrintStream s)
void printStackTrace(PrintWriter s)
String getMessage() // 获取设置的异常 message 和 cause
Throwable getCause()
StackTraceElement[] getStackTrace() //获取异常栈每一层的信息,每个 StackTraceElement 包括文件名、类名、函数名、行号等信息。
分类:编译时异常和运行时异常

运行时异常:
编译时异常
区别
常见的运行时异常 RuntimeException :

Java 提供的异常处理机制是抓抛模型。
异常处理的 5 个关键字

Java 提供的 抓抛模型
抓 的两种处理方式
注意: 子类重写父类的方法,其抛出的异常类型只能是被重写的方法的异常类的子类或和异常类一样;
这里补充一下异常使用原则:
异常应该且仅用于异常情况,也就是说异常不能代替正常的条件判断。比如说,循环处理数组元素的时候,你应该先检查索引是否有效再进行处理,而不是等着抛出索引异常再结束循环。对于一个引用变量,如果正常情况下它的值也可能为 null ,那就应该先检查是不是 null ,不为 null 的情况下再进行调用。
另一方面,真正出现异常的时候,应该抛出异常,而不是返回特殊值,比如 String 的 substring 方法,它返回一个子字符串,代码如下:
public String substring(int beginIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
int subLen = value.length - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
代码会检查 beginIndex 的有效性,如果无效,会抛出 StringIndexOutOfBoundsException 。纯技术上一种可能的替代方法是不抛异常而返回特殊值 null ,但 beginIndex 无效是异常情况,异常不能假装当正常处理。
finally 内的代码不管有无异常发生,都会执行。具体来说:
由于 finally 的这个特点,它一般用于释放资源,如数据库连接、文件流等。
注意: try/catch/finally 语法中, catch 不是必需的,也就是可以只有 try/finally ,表示不捕获异常,异常自动向上传递,但 finally 中的代码在异常发生后也执行。
finally 有一个容易出错的地方,就是配合 return 的使用:
看下面例子:
public class TestExceptionOne {
public static void main(String[] args) {
System.out.println(method()); // 10
}
public static int method(){
int ret = 10;
try{
return ret;
}finally{
ret = 20;
}
}
}
程序输出 10 。实际执行过程是,在执行到 try 内的 return ret; 语句前,会先将返回值 ret 保存在一个临时变量中,然后才执行 finally 语句,最后 try 再返回那个临时变量, finally 中对 ret 的修改不会被返回。
如果在 finally 中也有 return 语句呢? try 和 catch 内的 return 会丢失,实际会返回 finally 中的返回值。 finally 中有 return 不仅会覆盖 try 和 catch 内的返回值,而且还会掩盖 try 和 catch 内的异常,就像异常没有发生一样,例如:
public class TestExceptionOne {
public static void main(String[] args) {
System.out.println(method()); // 20
}
public static int method(){
int ret = 10;
try{
int a = 5/0; // Exception will not happened
return ret;
}finally{
return 20;
}
}
}
5/0 会触发 ArithmeticException ,但是 finally 中有 return 语句,这个方法就会返回 20 ,而不再向上传递异常了。
finally 中不仅 return 语句会掩盖异常,如果 finally 中抛出了异常,则原异常就会被掩盖:
public class TestExceptionOne {
public static void main(String[] args) {
method();
}
public static void method(){
try{
int a = 5/0; // replaced
}finally{
throw new RuntimeException("hello");
}
}
}
finally 中抛出了 RuntimeException ,则原异常 ArithmeticException 就丢失了。

所以为避免混淆,应该避免在 finally 中使用 return 语句或者抛出异常,如果调用的其他代码可能抛出异常,则应该捕获异常并进行处理。
再看一个 finally 中使用 return 的例子:
public class ReturnExceptionDemo {
public static void methodA(){
try {
System.out.println("进入方法 A!");
throw new RuntimeException("在方法 A 中制造异常!");
}finally {
System.out.println("执行方法 A 中的 finally!");
}
}
public static int methodB(){
try {
System.out.println("进入方法 B!");
return 1;
} finally {
System.out.println("执行方法 B 中的 finally!");
return 2;
}
}
public static void main(String[] args) {
try {
methodA();
} catch (Exception e) {
System.out.println(e.getMessage());
}
System.out.println(methodB());
}
}
输出:

throws 关键字
throws 跟在方法的括号后面,可以声明多个异常,以逗号分隔。
这个声明的含义是说:
对于 RuntimeException 和 checked exception 区别:
如果一个方法内调用了另一个声明抛出 checked exception 的方法,则必须处理这些 checked exception ,不过,处理的方式既可以是 catch ,也可以是继续使用 throws ,如下代码所示:
public void tester() throws AppException {
try {
test();
} catch (SQLException e) {
e.printStackTrace();
}
}
对于 test 抛出的 SQLException ,这里使用了 catch ,而对于 AppException ,则将其添加到了自己方法的 throws 语句中,表示当前方法也处理不了,还是由上层处理吧。
除了 Java API 中定义的异常类,也可以自己定义异常类,一般通过继承 Exception 或者它的某个子类,如果父类是 RuntimeException 或它的某个子类,则自定义异常也是 unchecked exception ,如果是 Exception 或 Exception 的其他子类,则自定义异常是 checked exception 。
如何自定义一个异常类:
/**
* 自己创建的异常类对象
*/
public class MyException extends Exception{
//序列化的机制 可以唯一的确定一个异常类的对象
static final long serialVersionUID = -338751699319948L;
public MyException() {
}
public MyException(String message) {
super(message);
}
}
再看自定义异常的一个案例:
public class EcmDefDemo {
public static void main(String[] args) {
Scanner cin = new Scanner(System.in);
try {
// int a = cin.nextInt();
// int b = cin.nextInt();
int a = Integer.parseInt(args[0]);
int b = Integer.parseInt(args[1]);
ecm(a,b); //执行除法
}catch (NumberFormatException e) {
System.out.println("输入的数据类型不一致....");
}
catch (ArrayIndexOutOfBoundsException e) {
System.out.println("缺少了输入数据.....");
}
catch (ArithmeticException e) {
System.out.println("分母为 0 了.....");
}
catch (DefException e) {
System.out.println(e.getMessage());
}
}
//判断是否输入负数,如果输入了,就抛出一个异常
public static void ecm (int a,int b) throws DefException {
if(a < 0 || b < 0){
throw new DefException("你输入的数值存在负数.....");
}
System.out.println(a / b);
}
}
class DefException extends Exception{
static final long serialVersionUID = -33873124229948L;
public DefException() {
}
public DefException(String message) {
super(message);
}
}
