1. 异常
1.1 异常的概念
在Java中,我们将程序执行过程中发生的不正常行为称为异常。异常是在程序运行过程中发生的错误或意外情况,它打破了程序的正常执行流程。在Java中通过面向对象的编程思想,我们也将这些扰乱程序正常执行的行为用类组织起来,也就是说异常实际上是由一个一个的类所组成。
IndexOutOfBoundsException
下标越界异常ArithmeticException
算术异常NullPointerException
空指针异常ClassCastException
类型转换异常StackOverFlowError
栈溢出错误.....
从上述列举过程看来,Java中对于不同类型的异常,我们都有与之对应的类来进行描述,也就是说异常拥有一套体系结构。
1.2 异常的体系
异常的种类很多,为了对不同的异常/错误等进行分类管理,Java内部维护了一个异常的体系结构:
Throwable
:异常体系的顶层类,其派生出两个子类:Error
和Exception
.
Error
: 对于Java虚拟机无法解决的严重问题,如JVM的内部错误、资源耗尽,如StackOverFlowError
和OutOfMemoryError
等,我们会抛出一个错误
Exception
: 异常产生后可由人为通过代码处理,使得程序能够继续执行,就是我们常说的异常Exception
.
我们常说的异常Exception
可以分为编译时异常和运行时异常。
- 编译时异常:
在程序编译期间发生的异常,称为编译时异常,也可以称为受查异常(Checked Exception),除了RuntimeException
及其子类之外的其他在Exception
下的异常包括Exception我们都称之为受查异常。 - 运行时异常:
在程序执行期间发生的异常,称为运行时异常,也可以称为非受查异常(Unchecked Exception),RuntimeException
以及其子类对应的异常,我们都称之为运行时异常。
Exception的注意事项:
- 编译时异常是在编译时期抛出的,会在编译期间检查程序是否会出现问题,一但出现编译时异常,强制要求我们进行处理,否则无法进行编译。
- 运行时异常是在Java虚拟机正常运行期间抛出的异常,这些异常在编译时不强制要求处理。
- 编译时出现的语法性错误,不能称之为异常;例如:在拼写System.out.println();时,将大小写拼写错误写成system.out.println(),此时不需要异常出现的情况。
1.3 自定义异常类
Java 中虽然已经内置了丰富的异常类, 但是并不能完全表示实际开发中所遇到的一些异常,此时就需要定义我们实际情况下所需要的异常结构。
//如何根据需求自定义一个异常类呢?
//(1)自定义异常类,必须继承自Exception 或者 RuntimeException
//(2)实现一个带String类型参数的构造方法,参数意义:书写出现异常的原因
//例如:实现一个用户登录时用户名或密码错误时的登录异常
//LogInException.java --自定义的登录异常类
public class LogInException extends RuntimeException {
public LogInException(String message) {
super(message);
}
public LogInException() {
super();
}
}
//--使用
//Login.java
public class LogIn {
//账号、密码
private String userName = "admin";
private String password = "123456";
public static void loginInfo(String userName, String password) throws LogInException {
if((!this.userName.equals(userName) || (!this.password.equals(password)) {
throw new LogInException("用户名和密码不匹配");
}
System.out.println("登陆成功");
}
}
//Main.java
public class Main {
public static void main(String[] args) {
LogIn logIn = new LogIn();
try {
logIn.loginInfo("admin","123456");
}catch(LogInException e) {
e.printStackTrace();
}finally {
System.out.println("finally中的代码一定会执行,且在try-catch-finally中最有执行。");
}
}
}
根据上面代码的处理结果,我们知道:
- 自定义异常通常会继承自Exception 或RuntimeException。
- 继承自 Exception 的异常默认是受查异常
- 继承自 RuntimeException的异常默认是非受查异常
2. 异常的处理
在异常处理中,我们需要用到的5个主要关键字是:throw
、try
、catch
、finally
、throws
。
2.1 异常的抛出
在编写程序时,如果程序中出现错误,我们想将错误信息告知调用者,如:参数检查错误等,我们可以借由异常实现。在Java中,可以借助throw
关键字抛出一个指定的异常对象(常抛出自定义的异常),将出现的问题借由错误信息告知调用者
//语法规则如下:
throw new 'XXX'Exception("异常产生的原因");
实例:实现一个获取数组中任意位置元素的方法
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];
}
[注意事项]
- 抛出的对象必须时Exception或Exception的子类对象,也就是说抛出的异常必须继承Exception或继承自Exception的子类。
- 如果抛出的是运行时异常
RuntimeException
或其子类,则可直接交由JVM进行处理- 如果抛出的是编译时异常,用户必须处理,否则无法通过编译
- 异常抛出后,其后的代码就不会执行了
2.2 异常的处理
异常的处理主要有两种:异常声明throws
以及try-catch-finally
捕获处理
2.2.1 异常声明throws
当方法中抛出编译时异常,用户不想处理该异常,此时可以借由throws
声明异常,将异常抛给方法的调用者来处理。即当前方法不处理异常,提醒方法的调用者处理异常。
//语法格式:异常声明处在方法声明时参数列表之后
修饰符 返回值类型 方法名(参数列表)throws 异常类型1,异常类型2... {
...
}
示例:
public static void loginInfo(String userName, String password) throws LogInException {
if((!this.userName.equals(userName) || (!this.password.equals(password)) {
throw new LogInException("用户名和密码不匹配");
}
System.out.println("登陆成功");
}
我们在当前的loginInfo方法中并没有处理LogInException,而是通过异常的声明将可能出现的异常报告给调用者,让调用者来解决处理这个问题。当调用者也不想处理这个问题,可以在调用的方法中也声明这个异常交给下一个调用这个调用的方法的人解决。
//也就是说如果在main方法中没有用try-catch-finally捕获的话,可以声明异常
public class Main {
public static void main(String[] args) throws LogInException {
LogIn logIn = new LogIn();
/*try {
logIn.loginInfo("admin","123456");
}catch(LogInException e) {
e.printStackTrace();
}finally {
System.out.println("finally中的代码一定会执行,且在try-catch-finally中最有执行。");
}*/
logIn.loginInfo("admin","123456");
}
}
对于异常声明,我们需要注意一下几个问题:
- throws 必须跟在方法的参数列表之后
- 声明的异常必须是 Exception 或者 Exception 的子类
- 方法内部如果抛出了多个异常,throws之后必须跟多个异常类型,之间用逗号隔开,如果抛出多个异常类型具有父子关系,直接声明父类即可
- 调用声明抛出异常的方法时,调用者必须对该异常进行处理,或者继续使用throws抛出
2.2.2 try-catch-finally捕获并处理
throws对异常并没有真正处理,而是将异常报告给抛出异常方法的调用者,由调用者处理。如果真正要对异常进行处理,就需要try-catch-finally
Tip: [中括号中的内容可加可不加],中括号内的内容表示可选;即可加也可不加
//语法格式:
try {
// 将可能出现异常的代码放在这里
}catch(要捕获的异常类型 e) {
// 如果try中的代码抛出异常了,此处catch捕获时异常类型与try中抛出的异常类型一致时,或者是try中抛出异常的父类时,就会被捕获到
// 对异常就可以正常处理,处理完成后,跳出try-catch结构,继续执行后序代码
}[catch(异常类型 e){
// 对异常进行处理
}finally{
// 此处代码一定会被执行到
}]
// 后序代码
// 当异常被捕获到时,异常就被处理了,这里的后序代码一定会执行
// 如果捕获了,由于捕获时类型不对,那就没有捕获到,这里的代码就不会被执行
也就是说try-catch-finally捕获中可能存在多个异常,这也就说我们可以拥有多个catch来捕获异常。上面对用户登录的例子中展现了try-catch-finally对异常的捕获。
我们以一段代码作为切入点来理解我们说到的try-catch-finally捕获:
public static void main(String[] args) {
int[] arr = {1, 2, 3};
try {
System.out.println("try代码块中执行前");
//arr = null;//(1)
//arr = new int[101];//(2)
System.out.println(arr[100]);
System.out.println("try代码块中执行后");
}catch (ArrayIndexOutOfBoundsException e) {
System.out.println("这是个数组下标越界异常");
e.printStackTrace();
}catch (NullPointerException e) {
System.out.println("这是个空指针异常");
e.printStackTrace();
}finally {
System.out.println("finally中的代码一定会执行");
}
System.out.println("after try-catch-finally");
}
- 不放开注释内容(1)和(2)的运行结果:
我们发现在try快中抛出异常位置后的代码将不会被执行,异常抛出后如果能被catch语句捕捉到则执行catch语句中的内容,然后执行finally快中的内容,当抛出异常并捕获到异常后,后序代码能够继续执行。
2. 当我们放开注释内容(1)后,执行代码,运行结果如下:
通过运行结果我们能够发现这次出现的是空指针异常,也就是说这次我们的异常被第二个catch语句捕获到了。这说明我们try快中的内容可以存在多个可能出现的异常,但是我们只能够捕获一个异常,也就是说我们不会同时抛出多个异常。
3. 当我们放开注释内容(2)后,执行代码,运行结果如下:
运行结果说明无论有没有抛出异常,一但存在finally快,我们都会执行finally块中的内容。
前面我们说过try-catch-finally捕获中[中括号]的内容可以不存在,也就是说finally快可以不存在,当finally快存在时,finally中的代码一定会执行。
// 下面程序输出什么?
public static void main(String[] args) {
System.out.println(func());
}
public static int func() {
try {
return 10;
} finally {
return 20;
}
}
//A: 10 B: 20 C: 30 D: 编译失败
finally 执行的时机是在方法返回之前(try 或者 catch 中如果有 return 会在这个 return 之前执行 finally). 但是如果finally 中也存在 return 语句, 那么就会执行 finally 中的 return, 从而不会执行到 try 中原有的 return.
上面程序输出结果 20
- 当catch不能捕获到try快中抛出的异常,那么后序代码将不会继续执行,(注意:这里后序代码不是try块内抛出异常后的代码,而是try-catch之后的代码)。
//省略掉finally的try-catch
public static void main(String[] args) {
int[] arr = {1, 2, 3};
try {
System.out.println("try代码块中执行前");
arr = null;
System.out.println(arr[100]);
System.out.println("try代码块中执行后");
}catch (ArrayIndexOutOfBoundsException e) {
System.out.println("这是个数组下标越界异常");
e.printStackTrace();
}
System.out.println("after try-catch-finally");
}
执行结果如下:
对try-catch-finally的总结:
- try块内抛出异常位置之后的代码将不会被执行
- 如果抛出异常类型与catch时异常类型不匹配,即异常不会被成功捕获,也就不会被处理,继续往外抛,直到JVM收到后中断程序----异常是按照类型来捕获的
- try中可能会抛出多个不同的异常对象,则必须用多个catch来捕获----即多种异常,多次捕获
- 如果异常之间具有父子关系,一定是子类异常在前catch,父类异常在后catch,否则语法错误,异常的是从上到下捕获的
- 当finally存在时,无论是否找到匹配的异常类型, finally 中的代码都会被执行到(在该方法结束之前执行)
- 异常的处理:如果没有找到匹配的异常类型, 就会将异常向上传递到上层调用者;如果上层调用者也没有处理的了异常, 就继续向上传递;一直到 main 方法也没有合适的代码处理异常, 就会交给 JVM 来进行处理, 此时程序就会异常终止