1.异常的概念与体系结构
1.1 异常的概念
在Java中,将程序执行过程中发生的不正常行为称为异常。
就比如下面这些:
- 算数异常
- 数组越界异常
-
空指针异常
- 其它异常
……
这些从报错信息中可以看到,这些异常都是java.lang下的类。没错,异常其实也是类,Java内部维护了一个异常的体系结构。
1.2 异常的体系结构
-
Throwable是异常体系的顶层类,其派生出两个重要的子类, Error 和 Exception。 -
Error指的是Java虚拟机无法解决的严重问题,像JVM的内部错误、资源耗尽等。比如:StackOverflowError和OutOfMemoryError。 -
Exception异常产生后程序员可以通过代码进行处理,使程序继续执行。
1.3 异常的分类
异常可能在编译时发生,也可能在程序运行时发生,根据发生的时机不同,可以将异常分为编译时异常和运行时异常。
- 运行时异常
在程序执行期间发生的异常,称为运行时异常,也称为非受检查异常。RunTimeException以及其子类对应的异常,都称为运行时异常。(这时编译器没有报错,可以通过编译。)
- 编译时异常
在程序编译期间必须要你处理的异常,称为编译时异常,也称为受检查异常。就是编译器要求你必须处理的异常,代码还没有运行,编码器就会检查你的代码,对可能出现的异常必须做出相对的处理(如果你不处理就会报错,比如下面的CloneNotSupportedException异常)。
2.异常的处理
2.1 异常的抛出(throw关键字)
Java中的异常可以手动抛出,其格式如下:
throw new XXXException("异常产生的原因");
在我们编写方法时,如果传进来的参数不符合要求,就可以手动抛出异常来告诉调用者错在哪。
实现一个获取数组中任意位置元素的方法。
public class Main {
public static int getElement(int[] array, int index){
if(null == array){
throw new NullPointerException("传递的数组为null");
}
if(index < 0 || index >= array.length){
throw new ArrayIndexOutOfBoundsException("传递的数组下标越界");
}
return array[index];
}
public static void main(String[] args) {
int[] array = {4,5,6};
getElement(array, 3);
System.out.println("这一行代码不会继续执行了");
}
}
结果:
注意:
- 抛出的对象必须是
Exception或者Exception的子类对象。 - 如果抛出的是
RunTimeException或者RunTimeException的子类,则可以不用处理(编译器没有报错),直接交给JVM来处理。(JVM处理的方式就是中断程序,所以发生异常之后的代码就无法执行) - 如果抛出的是
编译时异常,用户必须处理,否则无法通过编译(下面详细介绍)。
2.2 异常的捕获
2.2.1 异常声明 throws 关键字
编译时异常也是可以抛出的,但是如果不处理就无法通过编译。
我们忽略具体业务,只抛出(new)一个编译时异常 。
public class Main {
public static void main(String[] args) {
TestException();
}
public static void TestException(){
throw new FileNotFoundException("文件名字不对");
}
}
可以看到,这里跟前面的例子不同,这里的编译时异常 FileNotFoundException直接报错。那么这里的“处理”具体是怎么处理呢?
这就要用到throws了。异常的具体处理方式,主要有两种:异常声明throws 以及 try-catch捕获处理,这里先介绍 throws。
如果我们想让上面的代码不报错,就得要加上throws声明,如下:
可以看到 TestException() 方法中没有报错了,反而是main方法中报错了,这是为什么呢?这就是 throws 的作用,异常声明throws 的处理方式就是“甩锅”,就是把本方法中的异常甩到上一级方法中(就是调用者),让上一级方法处理。 这就间接的把异常给处理了。
但是这还没完呀,TestException() 把锅甩到了main身上,但是main也没处理,main说:“我不干了,凭啥我来处理?” 于是编译器继续报错,不让你编译成功。main迫于压力不得不继续甩锅,也加上了throws声明:
public class Main {
public static void main (String[] args) throws FileNotFoundException {
TestException();
}
public static void TestException() throws FileNotFoundException{
throw new FileNotFoundException("文件名字不对");
}
}
main 也加上了throws声明,这时编译器就不会报错,会让你编译成功。但是这么做就会把异常甩给JVM处理,上面提到过,JVM处理的方式就是结束程序。
结果:
最后总结并补充一下:
1.编译时异常和运行时异常都可以用throws来声明,其中的运行时异常没有要求,就算不用throws也不会报错。
2.throws声明是放在方法参数列表之后,当方法中抛出异常时,用户不想处理该异常,此时就可以借助throws将异常抛给方法的调用者来处理。即当前方法不处理异常,提醒方法的调用者处理异常。
3.方法内部如果抛出了多个异常,throws之后必须跟多个异常类型,之间用逗号隔开,如果抛出多个异常类型具有父子关系,直接声明父类即可。
public class Main{
public static int getElement(int[] array, int index) throws NullPointerException,ArrayIndexOutOfBoundsException{
if(null == array){
throw new NullPointerException("传递的数组为null");
}
if(index < 0 || index >= array.length){
throw new ArrayIndexOutOfBoundsException("传递的数组下标越界");
}
return array[index];
}
public static void main(String[] args) throws NullPointerException,ArrayIndexOutOfBoundsException {
int[] array = {4,5,6};
getElement(array, 3);
System.out.println("这一行代码不会继续执行了");
}
}
或者
public class Main {
public static int getElement(int[] array, int index) throws RuntimeException{
if(null == array){
throw new NullPointerException("传递的数组为null");
}
if(index < 0 || index >= array.length){
throw new ArrayIndexOutOfBoundsException("传递的数组下标越界");
}
return array[index];
}
public static void main(String[] args) throws RuntimeException {
int[] array = {4,5,6};
getElement(array, 3);
System.out.println("这一行代码不会继续执行了");
}
}
2.2.2 try-catch 捕获并处理
其实使用 throws 并没有对异常进行一个真正的处理,如果只使用throws到最后还让 JVM来处理了,我们知道JVM的处理方式就是结束程序,这就有一个缺点:发生异常的那行代码之后的程序就不会再执行了。
那么有什么办法来真正的处理异常,并且让程序继续执行下去?那就是 try-catch
try-catch的语法结构 与 switch-case 相似:
public static void main(String[] args) {
try{
// 将可能出现异常的代码放在这里
} catch (要捕获的异常类型 e){
// 如果try中的代码抛出异常了,此处catch捕获时异常类型与try中抛出的异常类型一致时,或者是try中抛出异常的基类时,就会被捕获到
// 对异常就可以正常处理,处理完成后,跳出try-catch结构,继续执行后序代码
} catch (要捕获的异常类型 e){
} catch (要捕获的异常类型 e){
} .....{
}
// 后序代码
// 当异常被捕获到时,异常就被处理了,这里的后序代码一定会执行
// 如果捕获了,由于捕获时类型不对,那就没有捕获到,这里的代码就不会被执行
}
- 将可能出现异常的代码放在
try里面。 catch中用来处理已捕获的异常e。
上面的案例:
public class Main {
public static void main (String[] args){
TestException();
}
public static void TestException() throws FileNotFoundException {
throw new FileNotFoundException("文件名字不对");
}
}
TestException()将异常甩给main方法,main:“行吧,脏活累活就交给我吧。”这时main就可以使用 try-catch来处理异常:
public class Main {
public static void main (String[] args){
try{
//可能出现异常的代码放在这里
TestException();
//后续代码不会执行
System.out.println("try块内抛出异常位置之后的代码将不会被执行");
} catch (FileNotFoundException e){
// 异常的处理方式,这一部分你想怎么处理就怎么处理。
//System.out.println(e.getMessage()); // 只打印异常信息
//System.out.println(e); // 打印异常类型:异常信息
e.printStackTrace(); // 打印信息最全面
}
System.out.println("如果异常处理成功,这一行代码将继续执行。");
}
public static void TestException() throws FileNotFoundException {
throw new FileNotFoundException("文件名字不对");
}
}
结果:
很显然,这样的处理方式才更加合理,达到了让程序继续执行下去的目的。但是要注意的是try块内抛出异常位置之后的代码将不会被执行。
补充:
- 如果抛出异常类型与
catch异常类型不匹配,即异常不会被成功捕获,也就不会被处理,继续往外抛,直到JVM收到后中断程序。
public class Main {
public static void main (String[] args){
int[] arr = {1,2,3,4};
try{
int n = arr[10];
} catch (NullPointerException e){
e.printStackTrace();
}
System.out.println("如果异常处理成功,以下代码将继续执行。");
}
}
结果:
try中可能会抛出多个不同的异常对象,则必须用多个catch来捕获。
public class Main {
public static void main (String[] args){
int[] arr = {1,2,3,4};
try{
//越界
int n = arr[10];
//空指针
arr = null;
n = arr[10];
} catch (NullPointerException e){
e.printStackTrace();
} catch (IndexOutOfBoundsException e){
e.printStackTrace();
}
System.out.println("如果异常处理成功,以下代码将继续执行。");
}
}
结果:
3.catch进行类型匹配的时候, 不光会匹配相同类型的异常对象, 也会捕捉目标异常类型的子类对象。
public class Main {
public static void main (String[] args){
int[] arr = {1,2,3,4};
try{
//越界
int n = arr[10];
//空指针
arr = null;
n = arr[10];
} catch (Exception e){
e.printStackTrace();
}
System.out.println("如果异常处理成功,以下代码将继续执行。");
}
}
结果:
4.finally关键字。有些特定的代码,不论程序是否发生异常,都需要执行,比如程序中打开的资源:网络连接、数据库连接、IO流等,在程序正常或者异常退出时,必须要对资源进进行回收。因为异常会引发程序的跳转,可能导致有些语句执行不到,finally就是用来解决这个问题的。
其语法格式:
public class Main {
public static void main (String[] args){
try{
// 可能会发生异常的代码
}catch(异常类型 e){
// 对捕获到的异常进行处理
}finally{
// 此处的语句无论是否发生异常,都会被执行到
}
// 如果没有抛出异常,或者异常被捕获处理了,这里的代码也会执行
}
}
案例:
public class Main {
public static void main (String[] args){
int[] arr = {1,2,3,4};
try{
System.out.println("进入try中");
int n = arr[10];
} catch (NullPointerException e){
e.printStackTrace();
} finally {
System.out.println("这一行一定会执行");
}
System.out.println("如果异常处理成功,以下代码将继续执行。");
}
}
结果:注意代码的执行顺序。
5.finally 和 try-catch-finally 后的代码都会执行,那为什么还要有finally呢?
public class Main {
public static void main (String[] args){
Scanner in = null;
try{
in = new Scanner(System.in);
//输入一个数并打印
int n = in.nextInt();
System.out.println(n);
return;
} catch (InputMismatchException e){
e.printStackTrace();
} finally {
System.out.println("finally中代码");
}
System.out.println("释放资源");
if(null != in){
in.close();
}
}
}
结果:
如果在try中加了return,正常情况下,try-catch-finally后面的代码是执行不到的,然而finally却是能执行得到,所以有finally存在的必要性,一般在finally中进行一些资源释放的工作。
2.3 异常的处理流程
- 程序先执行
try中的代码。 - 如果
try中的代码出现异常, 就会结束try中的代码, 看和catch中的异常类型是否匹配。 - 如果找到匹配的异常类型, 就会执行
catch中的代码。 - 如果没有找到匹配的异常类型, 就会将异常向上传递到上层调用者。
- 无论是否找到匹配的异常类型,
finally中的代码都会被执行到(在该方法结束之前执行)。 - 如果上层调用者也没有处理的了异常, 就继续向上传递。
- 一直到
main方法也没有合适的代码处理异常, 就会交给JVM来进行处理, 此时程序就会异常终止。
3.自定义异常类
Java给我们提供了很多的异常类,但是并不能完全表示实际开发中所遇到的一些异常,这就需要我们自己定义异常。
用户登录案例:
public class Login {
private static String userName = "李四";
private static String password = "123456abc";
public static void loginInfo(String user, String pass) {
if(!user.equals(userName)){
System.out.println("用户名错误!");
return;
}
if(!pass.equals(password)){
System.out.println("密码错误!");
return;
}
System.out.println("登陆成功");
}
public static void main(String[] args) {
loginInfo("李四","123456");
}
}
结果:
如果我们想让错误更明显,错误信息更详细,我们可以自己定义用户名错误异常以及密码错误异常。
自己定义的异常必须要继承Exception 或者 RuntimeException 类,继承自 Exception 的异常默认是受查异常;继承自RuntimeException的异常默认是非受查异常。
public class PasswordException extends Exception {
//实现一个带有String类型参数的构造方法,参数含义:出现异常的原因。
public PasswordException(String message){
super(message);
}
}
public class UserNameException extends Exception{
//实现一个带有String类型参数的构造方法,参数含义:出现异常的原因。
public UserNameException(String message){
super(message);
}
}
public class Login {
private static String userName = "李四";
private static String password = "123456abc";
public static void loginInfo(String user, String pass)throws UserNameException,PasswordException {
if(!user.equals(userName)){
throw new UserNameException("用户名错误!");
}
if(!pass.equals(password)){
throw new PasswordException("密码错误!");
}
System.out.println("登陆成功");
}
public static void main(String[] args) {
try{
loginInfo("李四","123456");
} catch(UserNameException e){
e.printStackTrace();
} catch (PasswordException e){
e.printStackTrace();
}
}
}
结果: