文章目录
什么是异常
- 异常就是程序的错误。
- 错误在我们编写程序的过程中会经常发生,包括编译期间和运行期间的错误。
- 在程序运行过程中,意外发生的情况,背离我们程序本身意图的表现,都可以理解为异常。
举个例子:这些都算是运行的错误——在编译时没有错误提示。
异常分类
在Java中,用Throwable
和它的子类对异常进行描述。
Throwable
分为Error
和Exception
:
Error
:是程序中无法处理的错误,表示运行应用程序中较严重的问题——大多数错误与代码编写者执行的操作无关,主要是代码运行是Java虚拟机出现的问题,如:虚拟机错误VirtualMachineError
,内存溢出OutOfMemoryError
,线程死锁ThreadDeath
Exception
:是程序本身可以处理的异常。异常处理通常指针对这种异常的处理。包括:非检查异常Unchecked Exception
,检查异常Checked Exception
关于Error
:它们在应用程序的控制和处理能力之外,而且绝大多数是程序运行时不允许出现的状况,对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况
关于Unchecked Exception
:编译器不要求强制处理的异常,包含RuntimeException
以及它的相关子类。如:
- 空指针异常
- 数组下标越界异常
- 算数异常
- 类型转换异常
关于Checked Exception
:编译器要求在代码中处理的异常。是RuntimeException
以及它的相关子类之外的异常。
思维导图
异常处理
在Java应用程序中,异常处理机制分为:
- 抛出异常
- 捕捉异常
关于捕获异常:
对于运行时异常、错误或可查异常,Java技术所要求的异常处理方式有所不同。
Java规定:
- 对于可查异常必须捕捉、或者声明抛出
- 允许忽略不可查的
RuntimeException
(含子类)和Error
(含子类)
关于关键字:try
、catch
、finally
、throw
、throws
try-catch-finally
try
:用于捕获异常
catch
:用于处理捕获到的异常
finally
:无论是否发生异常,代码总能执行
**try块后可接零个或者多个catch块,如果没有catch块,必须跟一个finally块。**简言之,try必须跟catch或finally组合使用,不允许单独存在。
举例:要求:定义两个整,接收键盘用户输入,输出两数之商。
代码:
public class TryDemoOne {
public static void main(String[] args) {
// TODO Auto-generated method stub
// 要求:定义两个整,接收键盘用户输入,输出两数之商。
System.out.println("请输入两个整数:");
Scanner input = new Scanner(System.in);
int one = input.nextInt(), two = input.nextInt();
System.out.println("商为" + one / two);
}
}
奇葩输入1:12 0
则输出:
Exception in thread "main" java.lang.ArithmeticException: / by zero
at ExceptionProj/com.test.TryDemoOne.main(TryDemoOne.java:13)
奇葩输入2:12 a
则输出:
Exception in thread "main" java.util.InputMismatchException
at java.base/java.util.Scanner.throwFor(Scanner.java:939)
at java.base/java.util.Scanner.next(Scanner.java:1594)
at java.base/java.util.Scanner.nextInt(Scanner.java:2258)
at java.base/java.util.Scanner.nextInt(Scanner.java:2212)
at ExceptionProj/com.test.TryDemoOne.main(TryDemoOne.java:12)
接下来我们使用try-catch
结构处理上述异常。
若有异常,catch会捕获它,然后输出其代码块内的内容;否则完成try内的操作。
public class TryDemoOne {
public static void main(String[] args) {
// TODO Auto-generated method stub
// 要求:定义两个整,接收键盘用户输入,输出两数之商。
try {
System.out.println("请输入两个整数:");
Scanner input = new Scanner(System.in);
int one = input.nextInt(), two = input.nextInt();
System.out.println("商为" + one / two);
} catch (Exception e) {
System.out.println("程序出错啦!");
}
System.out.println("运算结束");
}
}
输入:12 a
输出:
程序出错啦!
运算结束
输入:12 0
输出:
程序出错啦!
运算结束
如果我们想知道错误的类型和位置,可以在catch
中添加语句:e.printStackTrace();
如:
public class TryDemoOne {
public static void main(String[] args) {
// TODO Auto-generated method stub
// 要求:定义两个整,接收键盘用户输入,输出两数之商。
try {
System.out.println("请输入两个整数:");
Scanner input = new Scanner(System.in);
int one = input.nextInt(), two = input.nextInt();
System.out.println("商为" + one / two);
} catch (Exception e) {
e.printStackTrace();
System.out.println("程序出错啦!");
}
System.out.println("运算结束");
}
}
那么输入12 0
时,会输出:
java.lang.ArithmeticException: / by zero
程序出错啦!
at ExceptionProj/com.test.TryDemoOne.main(TryDemoOne.java:14)
运算结束
ps:e.printStackTrace();
的错误信息的输出位置相对随机,不用计较。
我们该怎么看异常信息呢? 以
at java.base/java.util.Scanner.throwFor(Scanner.java:939)
at java.base/java.util.Scanner.next(Scanner.java:1594)
at java.base/java.util.Scanner.nextInt(Scanner.java:2258)
at java.base/java.util.Scanner.nextInt(Scanner.java:2212)
at ExceptionProj/com.test.TryDemoOne.main(TryDemoOne.java:13)
为例,这是输入12 a
的异常信息。
我们要从下往上看。
最后一行at ExceptionProj/com.test.TryDemoOne.main(TryDemoOne.java:13)
表示:异常发生的最外层的表示是在TryDemoOne.java
文件的第13行:
在这个位置它调用了Scanner.nextInt
方法,在Scanner.java:2212
,我们点击它,则跳转到Scanner.class
:
而这个位置调用了nextInt(defaultRadix)
,其实就是在Scanner.nextInt(Scanner.java:2258)
,点击它则跳转到:
到这里我们可以看到integerPattern()
,大概能猜出来这是一个整数转换的方法,而a肯定不能转化为整数,所以会跳转到Scanner.next(Scanner.java:1594)
,即:throwFor()
,异常抛出。
异常抛出的具体代码在Scanner.throwFor(Scanner.java:939)
,即:
所以,我们这里的异常是:InputMismatchException
,会被catch
所捕获。
这就是try-catch
产生异常、捕获异常的整体流程。
关于finally
:我们如果想要某个语句不管是否产生异常都一定要执行,可以在这个语句外套一个finally
,如:
public class TryDemoOne {
public static void main(String[] args) {
// TODO Auto-generated method stub
// 要求:定义两个整,接收键盘用户输入,输出两数之商。
try {
System.out.println("请输入两个整数:");
Scanner input = new Scanner(System.in);
int one = input.nextInt(), two = input.nextInt();
System.out.println("商为" + one / two);
} catch (Exception e) {
e.printStackTrace();
System.out.println("程序出错啦!");
} finally {
System.out.println("运算结束");
}
}
}
使用多重catch结构处理异常
如果针对不同的异常,有不同的处理方式,该怎么解决呢?
答:用多重catch解决
ps:
- 多重
catch
时不能出现同类型异常 - 安全起见,多重
catch
块的最后一个块中异常为Exception
(不能放第一个,会报错)
代码如下:
public static void main(String[] args) {
// TODO Auto-generated method stub
// 要求:定义两个整,接收键盘用户输入,输出两数之商。
try {
System.out.println("请输入两个整数:");
Scanner input = new Scanner(System.in);
int one = input.nextInt(), two = input.nextInt();
System.out.println("商为" + one / two);
} catch (ArithmeticException e) {
System.out.println("除数不能为0");
e.printStackTrace();
} catch (InputMismatchException e) {
System.out.println("请输入整数");
e.printStackTrace();
} catch (Exception e) {
System.out.println("其他异常");
e.printStackTrace();
} finally {
System.out.println("运算结束");
}
}
测试输入:12 0
输出:
除数不能为0
java.lang.ArithmeticException: / by zero
运算结束
at ExceptionProj/com.test.TryDemoOne.main(TryDemoOne.java:14)
测试输入:12 a
请输入整数
java.util.InputMismatchException
运算结束
at java.base/java.util.Scanner.throwFor(Scanner.java:939)
at java.base/java.util.Scanner.next(Scanner.java:1594)
at java.base/java.util.Scanner.nextInt(Scanner.java:2258)
at java.base/java.util.Scanner.nextInt(Scanner.java:2212)
at ExceptionProj/com.test.TryDemoOne.main(TryDemoOne.java:13)
终止finally执行的方法
catch
和finally
不能脱离try
独自出现。
一般情况下,不管finally一定会执行,有没有终止finally执行的方法呢?
答:有。System.exit(1)
;
则若catch的代码改成:
catch (ArithmeticException e) {
System.exit(1);
System.out.println("除数不能为0");
e.printStackTrace();
}
那么他会在System.exit(1);
时终止程序的执行,就不会允许到finally了。
return关键字在异常处理中的作用
类如下:
public class Test {
public static void main(String[] args) {
int res=test();
System.out.println("商为"+res);
}
public static int test() {
try {
System.out.println("请输入两个整数:");
Scanner input = new Scanner(System.in);
int one = input.nextInt(), two = input.nextInt();
return one/two;
} catch (ArithmeticException e) {
System.out.println("除数不能为0");
return 0;
} catch (Exception e) {
System.out.println("其他异常");
e.printStackTrace();
} finally {
System.out.println("运算结束");
return -10000;
}
}
}
测试输入为:2 1
输出:
运算结束
商为-10000
输入为:2 0
输出:
除数不能为0
运算结束
商为-10000
由此可见,在try-catch-finally中,三个语句都有return的情况下,永远都是返回finally中的return
也就是说,从语法的角度上finally块中带return是没问题的,但从逻辑的角度上,finally块中的return是一定会被返回的——这也就是为什么finally中有return就会有Warning:
如果把finally中的return删去,即:
public static int test() {
try {
System.out.println("请输入两个整数:");
Scanner input = new Scanner(System.in);
int one = input.nextInt(), two = input.nextInt();
return one/two;
} catch (ArithmeticException e) {
System.out.println("除数不能为0");
return 0;
} catch (Exception e) {
System.out.println("其他异常");
e.printStackTrace();
return 0;
} finally {
System.out.println("运算结束");
}
}
再测试一下:
输入:2 1
输出:
运算结束
商为2
输入:2 0
输出:
除数不能为0
运算结束
商为0
这就正确了。
使用throws声明异常类型
可以通过throws
声明将要抛出何种类型的异常,通过throw
将产生的异常抛出。如果一个方法可能会出现异常,但没有能力处理这种异常,可以在方法声明处用throws
子句来声明抛出异常。
throws
语句用在方法定义时声明该方法要抛出的异常类型。
public void method() throws Exception 1,Exception 2{
//可能产生异常的代码
}
当方法抛出异常列表中的异常时,方法将不对这些类型及其子类作处理,而抛向调用该方法的方法,由他去处理。
举个例子:
测试类代码如下:
public class TryDemo3 {
public static void main(String[] args) {
try {
int res = test();
System.out.println("商为" + res);
} catch (ArithmeticException e) {
// TODO Auto-generated catch block
System.out.println("除数不允许为0");
e.printStackTrace();
}
}
//表示这个函数可能会抛出ArithmeticException这样的异常
public static int test() throws ArithmeticException {
System.out.println("请输入两个整数:");
Scanner input = new Scanner(System.in);
int one = input.nextInt(), two = input.nextInt();
System.out.println("运算结束");
return one / two;
}
}
测试输入1:2 1
输出:
运算结束
商为2
测试输入2:2 0
输出:
运算结束
java.lang.ArithmeticException: / by zero
除数不允许为0
at ExceptionProj/com.test.TryDemo3.test(TryDemo3.java:26)
at ExceptionProj/com.test.TryDemo3.main(TryDemo3.java:10)
这段代码能处理别的异常吗?答案是不可以。
测试输入3:2 a
输出:这个异常是虚拟机在处理。
Exception in thread "main" java.util.InputMismatchException
at java.base/java.util.Scanner.throwFor(Scanner.java:939)
at java.base/java.util.Scanner.next(Scanner.java:1594)
at java.base/java.util.Scanner.nextInt(Scanner.java:2258)
at java.base/java.util.Scanner.nextInt(Scanner.java:2212)
at ExceptionProj/com.test.TryDemo3.test(TryDemo3.java:24)
at ExceptionProj/com.test.TryDemo3.main(TryDemo3.java:10)
解决方法1:throws后面接多个异常类型,用逗号分隔
public class TryDemo3 {
public static void main(String[] args) {
try {
int res = test();
System.out.println("商为" + res);
} catch (ArithmeticException e) {
// TODO Auto-generated catch block
System.out.println("除数不允许为0");
e.printStackTrace();
}catch(InputMismatchException e) {
System.out.println("输入必须为整数");
e.printStackTrace();
}
}
//表示这个函数可能会抛出ArithmeticException这样的异常
public static int test() throws ArithmeticException,InputMismatchException {
System.out.println("请输入两个整数:");
Scanner input = new Scanner(System.in);
int one = input.nextInt(), two = input.nextInt();
System.out.println("运算结束");
return one / two;
}
}
解决方法2:throws后接Exception
public class TryDemo3 {
public static void main(String[] args) {
try {
int res = test();
System.out.println("商为" + res);
} catch (ArithmeticException e) {
// TODO Auto-generated catch block
System.out.println("除数不允许为0");
e.printStackTrace();
} catch (InputMismatchException e) {
System.out.println("输入必须为整数");
e.printStackTrace();
} catch (Exception e) {
System.out.println("其他错误");
e.printStackTrace();
}
}
public static int test() throws Exception {
System.out.println("请输入两个整数:");
Scanner input = new Scanner(System.in);
int one = input.nextInt(), two = input.nextInt();
System.out.println("运算结束");
return one / two;
}
}
那么问题来了,当test方法后接的异常为Exception
时,我们在main方法中若是直接写一个test()
,会有报错:
而test方法后接的异常为ArithmeticException
时,就不会报错。为什么呢?
原因如下:ArithmeticException
异常是非检查异常,所以编译器不会要求我们一定去改它,而Exception
异常是父类,它包含检查异常,因此编译器会报错,要求我们去改掉它。
如果我们程序中可能产生的异常都是非检查异常,而我们希望编译器给我们报错的提示,改如何解决呢?
答:文档注释——/**
/**
*
* @return
* @throws ArithmeticException
* @throws InputMismatchException
*/
由图可见,写上了文档注释后,我们把鼠标放到test上,它会提示说可能有ArithmeticException
和InputMismatchException
这两种异常——尽管它不报错。
使用throw抛出异常对象
throw
用来抛出一个异常,且抛出的只能够是可抛出类Throwable
或者其子类的对象。
抛出异常的处理方式有两种:
方案1:自己抛出的异常自己处理——在throw
外加try
—catch
块:
public void method(){
try{
//代码
throw new 异常类型();
}catch(异常类型 e){
//对异常的处理代码
}
}
方案2:我们在抛出异常的方法声明处通过throws
关键字标识对应的异常类型——谁调用当前方法,谁处理它:
public void method() throws 异常类型{
//代码段1
throw new 异常类型();
}
异常处理可以:
- 规避可能出现的风险
- 完成一些程序的逻辑
举个例子:如何通过异常处理完成一些程序的逻辑。
假设我们有这样一个场景:一个旅馆要求18岁以下或80岁以上的人如果想办理入住,需要其亲友陪同。
方案1:通过try-catch包含throw语句——在方法内部自己抛出异常自己处理(在这里,18岁以下,80岁以上的人就是”异常“)
public class TryDemo4 {
public static void main(String[] args) {
// TODO Auto-generated method stub
testAge();
}
// 描述酒店的入住规则:限定年龄,要求18岁以下或80岁以上的人需要其亲友陪同。
public static void testAge() {
try {
System.out.println("请输入年龄");
Scanner input = new Scanner(System.in);
int age = input.nextInt();
if (age < 18 || age > 80) {
throw new Exception("要求18岁以下或80岁以上的人需要其亲友陪同");
} else {
System.out.println("欢迎入住!");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
测试输入:20
输出:欢迎入住!
测试输入:17
输出:
java.lang.Exception: 要求18岁以下或80岁以上的人需要其亲友陪同
at ExceptionProj/com.test.TryDemo4.testAge(TryDemo4.java:20)
at ExceptionProj/com.test.TryDemo4.main(TryDemo4.java:9)
方案2:通过throws在方法声明处抛出异常类型——谁调用谁处理——调用者可以自己处理,或继续向上抛出(由上层处理)
public class TryDemo4 {
public static void main(String[] args) {
// TODO Auto-generated method stub
try {
testAge();
} catch (Exception e) {
e.printStackTrace();
}
}
// 描述酒店的入住规则:限定年龄,要求18岁以下或80岁以上的人需要其亲友陪同。
public static void testAge() throws Exception {
System.out.println("请输入年龄");
Scanner input = new Scanner(System.in);
int age = input.nextInt();
if (age < 18 || age > 80) {
throw new Exception("要求18岁以下或80岁以上的人需要其亲友陪同");
} else {
System.out.println("欢迎入住!");
}
}
}
测试输入:20
输出:欢迎入住!
测试输入:17
输出:
java.lang.Exception: 要求18岁以下或80岁以上的人需要其亲友陪同
at ExceptionProj/com.test.TryDemo4.testAge(TryDemo4.java:20)
at ExceptionProj/com.test.TryDemo4.main(TryDemo4.java:9)
总结:throw抛出异常对象处理方案:
- 通过
try-catch
包含throw
语句——在方法内部自己抛出异常自己处理 - 通过
throws
在方法声明处抛出异常类型——谁调用谁处理——调用者可以自己处理,或继续向上抛出(由上层处理)
其他补充:throw
语句抛出的异常,在用throws
方法声明时,可以声明它的父类。但不能是子类,如:
这样是可以的,因为Throwable
是Exception
的父类,是匹配的。
这样是不行的,IOException
是Exception
的子类,范围小,不能描述Exception的相关信息。
ps:不建议抛出非检查异常,因为不会报错。
自定义异常类
使用Java内置的异常类可以描述在编程时出现的大部分异常情况。也可以通过自定义异常描述特定业务产生的异常类型。
所谓自定义异常,就是定义一个类,去继承Throwable
类或者它的子类。
对上面的案例,我们为18岁以下和80岁以上自定义一个异常类HotelAgeException
:
public class HotelAgeException extends Exception{
public HotelAgeException() {
super("要求18岁以下或80岁以上的人需要其亲友陪同");
}
}
在测试类中调用它:
public class TryDemo4 {
public static void main(String[] args) {
// TODO Auto-generated method stub
try {
testAge();
} catch (HotelAgeException e) {
System.out.println(e.getMessage());
} catch (Exception e) {
e.printStackTrace();
}
}
// 描述酒店的入住规则:限定年龄,要求18岁以下或80岁以上的人需要其亲友陪同。
public static void testAge() throws HotelAgeException {
System.out.println("请输入年龄");
Scanner input = new Scanner(System.in);
int age = input.nextInt();
if (age < 18 || age > 80) {
throw new HotelAgeException();
} else {
System.out.println("欢迎入住!");
}
}
}
测试输入:1
输出:要求18岁以下或80岁以上的人需要其亲友陪同
异常链
有时候我们会捕获一个异常后在抛出另一个异常。
代码:
public class TryDemo5 {
public static void main(String[] args) {
// TODO Auto-generated method stub
try {
test3();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void test1() throws HotelAgeException {
throw new HotelAgeException();
}
public static void test2() throws Exception {
try {
test1();
} catch (HotelAgeException e) {
throw new Exception("我是新产生的异常1");
}
}
public static void test3() throws Exception {
try {
test2();
} catch (Exception e) {
throw new Exception("我是新产生的异常2");
}
}
}
由此可见,这里的异常信息是丢失了的——由于新抛出了异常而导致异常信息丢失。有没有方法可以把异常保留下来呢?
API文档链接
找到lang包——找到Throwable类:可以用这个方法把 异常信息保留下来。
修改后的代码:
public class TryDemo5 {
public static void main(String[] args) {
// TODO Auto-generated method stub
try {
test3();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void test1() throws HotelAgeException {
throw new HotelAgeException();
}
public static void test2() throws Exception {
try {
test1();
} catch (HotelAgeException e) {
throw new Exception("我是新产生的异常1",e);
}
}
public static void test3() throws Exception {
try {
test2();
} catch (Exception e) {
throw new Exception("我是新产生的异常2",e);
}
}
}
输出:
由此可见,异常2由异常1产生,异常1由18岁以下80岁以上的异常产生。
这就是异常链。
- 有时候我们会捕获一个异常后抛出另一个异常。
- 顾名思义:将异常发生的原因一个传一个地串起来,即把底层的异常信息传到上层,这样逐层抛出。
总结
异常
在程序运行过程中,意外发生的情况,背离我们程序本身意图的表现,都可以理解为异常。
利用Java中的异常机制,我们可以更好地提升程序的健壮性。
在Java中,通过Throwable及其子类描述各种不同的异常类型。
异常处理
在Java应用程序中,异常处理机制为:抛出异常,捕捉异常。
通过5个关键字来实现:try
、catch
、finally
、throw
、throws
:
try-catch:
try-catch-finally:
异常类型匹配时:
异常类型不匹配时:
多重catch时:
有return时:finally执行完后才执行return
一些总结:
- 处理运行时异常时,采用逻辑去合理规避同时辅助
try-catch
处理 - 在多重
catch
后,可以加一个catch(Exception e)
来处理可能会遗漏 的异常 - 对于不确定的代码,也可以加上
try-catch
,处理潜在的异常 - 尽量去处理异常,切记只是简单的调用
printStackTrace()
去打印输出异常信息 - 具体如何处理异常,要根据不同的业务需求和异常类型去决定
- 尽量添加
finally
语句块去释放占用的资源
throw和throws
可以通过throws
声明将要抛出何种类型的异常,通过throw
将产生的异常抛出。
如果一个方法可能会产生异常,但没有能力处理这种异常,可以在方法声明处用throws
子句来声明抛出异常。
throw
- 当子类重写父类抛出异常的方法时,声明的异常必须是父类方法所声明异常的同类或子类
自定义异常
可以通过自定义异常描述特定业务产生的异常类型。
所谓自定义异常,就是定义一个类,去继承Throwable类或者它的子类。
异常链
- 有时候我们会捕获一个异常后抛出另一个异常。
- 顾名思义:将异常发生的原因一个传一个地串起来,即把底层的异常信息传到上层,这样逐层抛出。