认识异常
一、异常的概念与体系结构
1.异常的概念
在Java中,将程序执行过程中发生的不正常行为称为异常。比如之前写代码经常遇到的:
- 算术异常
public static void main(String[] args) {
System.out.println(10/0);
}
//运行结果
Exception in thread "main" java.lang.ArithmeticException: / by zero
at day1010.test2.Exception.main(Exception.java:5)
- 数组越界异常
int [] arr={1,2,3};
System.out.println(arr[100]);
//运行结果
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 100
at day1010.test2.Exception.main(Exception.java:8)
- 空指针异常
public int num=10;
public static void main(String[] args) {
//System.out.println(10/0);
/* int [] arr={1,2,3};
System.out.println(arr[100]);*/
Exception t=null;
System.out.println(t.num);
}
//Exception in thread "main" java.lang.NullPointerException
at day1010.test2.Exception.main(Exception.java:11)
从上面的内容可以看到,Java中不同类型的异常,都有与其对应的类来进行描述。
2.异常的体系结构
异常种类繁多,为了对不同异常或者错误进行很好的分类管理,Java内部维护了一个异常的体系结构:
从上图可以看到:
- Throwable:是异常体系的顶层类,其派生出两个重要的子类,Error和Expection。
- Error: 指的是Java虚拟机无法解决的严重问题。比如:JVM的内部错误、资源耗尽等,典型代表:StackOverflowError和OutOfMemoryError,一旦发生,无药可救。
- Expection:异常产生后程序员可以通过代码进行处理,使程序继续执行。比如:感冒、发烧。我们平时所说的异常就是Expection。
3.异常的分类
异常可能在编译时发生,也可能在程序运行时发生,根据发生的时机不同,可以将异常分为:
- 编译时异常
在程序编译期间发生的异常,称为编译时异常,也称为受检查异常。
public class Person {
private String name;
private String gender;
int age;
//想让该类支持深拷贝,重写Object类的clone方法即可
@Override
protected Person clone() {
return (Person)super.clone();
}
}
//编译时报错
java: 未报告的异常错误java.lang.CloneNotSupportedException; 必须对其进行捕获或声明以便抛出
- 运行时异常
在程序执行期间所发生的异常,称为运行时异常,也称为非受检查异常。
RunTimeExpection以及子类对应的异常,都称为运行时异常。比如:NullPointerExpection、ArithmeticExpection。
注意: 编译时出现的语法性错误,不能称之为异常。例如将System.out.println拼写错误,此时编译过程就会出错,这是"编译期"出错。而运行时指的是程序已经通过编译,生成了字节码文件,再由JVM执行过程中出现的错误。
二、异常的处理方式
1.防御式编程
错误在代码中是客观存在的,因此我们要让程序在出现问题的时候及时通知程序员。
LBYL
:Look Before You Leap。在操作之前就做充分的检查。即:事前防御型
boolean ret=false;
ret= 登陆游戏();
if(!ret){
处理登陆游戏错误;
return;
}
ret= 开始匹配();
if(!ret){
处理匹配错误;
return;
}
ret= 游戏确认();
if(!ret){
处理游戏确认错误;
return;
}
ret= 载入游戏画面();
if(!ret){
处理载入游戏画面错误;
return;
}
缺陷:正常流程和错误处理流程代码混在一起,代码整体显得比较混乱。
EAFP
:It’s Easier to Ask Forgiveness than Permission.意思是先操作,遇到问题再处理。即:事后认错型
try{
登陆游戏();
开始匹配();
游戏确认();
载入游戏画面();
}catch (登陆游戏异常){
处理登陆游戏异常;
}catch (开始匹配异常){
处理开始匹配异常;
}catch (游戏确认异常){
处理游戏确认异常;
}catch (载入游戏画面异常){
处理载入游戏画面异常;
}
优势:正常流程和错误流程是分开的,程序员更关注正常流程,代码更清晰,容易理解代码异常处理的核心思想就是EAFP。
在java中,异常处理的5个主要关键字:throw、try、catch、final、throws
2.异常的抛出
在编写程序时,如果程序中出现错误,此时就需要将错误的信息告知给调用者。比如
:参数检测。
在Java中,可以借助throw关键字,抛出一个指定的异常对象,将错误信息告知给调用者。具体语法如下:
throw new XXXException("异常产生的原因");
需求:实现一个获取数组中任意位置元素的方法。
public class test {
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={1,2,3};
System.out.println( getElement(array,0));
}
}
注意:
- throw必须写在方法体内部。
- 抛出的对象必须是Expection或者Expection的子类对象。
- 如果抛出的是RunTimeExpection或者RunTimeExpection的子类,则可以不用处理,直接交给JVM来处理。
- 如果抛出的是编译时异常,用户必须处理,否则无法通过编译。
- 异常一旦抛出,其后的代码就不会执行。
3.异常的捕获
异常的捕获,也就是异常的具体处理方式。主要有两种:异常声明throws以及try-catch捕获处理。
3.1异常声明throws
在方法声明时参数列表之后,当方法中抛出编译时异常,用户不想处理该异常,此时就可以借助throws将异常抛给方法的调用者来处理。即当前方法不处理异常,提醒方法的调用者处理异常。
语法格式:
修饰符 返回值类型 方法名(参数列表) throws 异常类型1,异常类型2..。{
}
需求:加载指定的配置文件config.ini
public class Config {
File file;
//FileNotFoundException:编译时异常,表明文件不存在,此处不处理,也没有能力处理,
// 应将错误信息报告给调用者,让调用者检查文件名字是否给错了
public void OpenConfig(String filename) throws FileNotFoundException{
if(filename.equals("config.ini")){
throw new FileNotFoundException("配置文件名字不对");
}
//打开文件
}
public void readConfig(){
}
}
注意事项:
- throws必须跟在方法的参数列表之后
- 声明的异常必须是Expection或者Expection的子类
- 方法内部如果抛出了多个异常,throws之后必须跟多个异常类型,之间用逗号隔开,如果抛出多个异常类型具有父子关系,直接声明父类即可。
public class Config {
File file;
//FileNotFoundException:编译时异常,表明文件不存在,此处不处理,也没有能力处理,
// 应将错误信息报告给调用者,让调用者检查文件名字是否给错了
public void OpenConfig(String filename) throws IOException,FileNotFoundException {
if(filename.endsWith(".ini")){
throw new IOException("文件不是.ini文件");
}
if(filename.equals("config.ini")){
throw new FileNotFoundException("配置文件名字不对");
}
//打开文件
}
public void readConfig(){
}
}
- 调用声明抛出异常的方法时,调用者必须对该异常进行处理,或者继续使用throws抛出
public static void main(String[] args) {
Config config=new Config();
config.OpenConfig("config.ini");
}
将光标放在抛出的异常方法上,Alt
+Enter
快速处理:
处理之后的代码:
public static void main(String[] args) throws IOException {
Config config=new Config();
config.OpenConfig("config.ini");
}
3.2 try-catch
捕获并处理
throws对异常并没有真正处理,而是将异常报告给抛出异常方法的调用者,由调用者处理。如果真正要对异常进行处理,就需要try-catch。
语法格式
try{
//将可能出现异常的代码放在这里
}catch(要捕获的异常类型 e){
//如果try中的代码抛出异常了,此处catch捕获时异常类型与try中抛出的异常类型一致时,或者是try中抛出异常的基类时,就会被捕获到
//对异常就可以正常处理,处理完成后,跳出try-catch结构,继续执行下面的代码
}[catch(异常类型 e){
//对异常进行处理
}finally{
//此处代码一定会执行
}]
//后续代码
//当异常被捕获到时,异常就被处理了,这里的后续代码一定会执行
//如果捕获了,由于捕获时类型不对,那就没有捕获到,这里的代码就不会被执行
注意:
- []中表示可选项,可以添加,也可以不用添加
- try中的代码可能会抛出异常,也可能不会
代码示例:
读取该配置文件,如果配置文件名字不是指定名字,抛出异常,调用者进行异常处理。
public class Config {
File file;
public void OpenConfig(String filename) throws FileNotFoundException {
if(filename.equals("config.ini")){
throw new FileNotFoundException("配置文件名字不对");
}
//打开文件
}
public void readConfig(){
}
public static void main(String[] args) {
Config config=new Config();
try{
config.OpenConfig("config.txt");
System.out.println("文件打开成功");
}catch(IOException e){
//异常的处理方式
System.out.println(e.getMessage());//只打印异常信息
System.out.println(e);//打印异常类型
e.printStackTrace();//打印信息最全面
}
//一旦异常被捕获处理了,此处的代码会执行
System.out.println("异常被处理了");
}
}
关于异常的处理方式,我们需要分情况而定:
- 对于比较严重的问题(例如和钱相关),应该让程序直接崩溃,防止造成更严重的后果。
- 对于不太严谨的问题(大多数场景),可以记录错误日志,并通过监控报警程序及时通知程序员。
- 对于可能会恢复的问题(和网络相关的场景),可以尝试进行重试。
注意事项:
- try块内抛出异常位置之后的代码将不会被执行
- 如果抛出异常类型与catch时异常类型不匹配,即异常不会被成功捕获,也就不会被处理,继续往外抛,知道JVM收到后中断程序----异常是按照类型来捕获的。
public static void main(String[] args) {
try{
int[] array={1,2,3};
System.out.println(array[3]);//此处会抛出数组越界异常
}catch(NullPointerException e){ //捕获时捕获的是空指针异常---真正的异常无法捕获到
e.printStackTrace();
}
System.out.println("后续代码");
}
//运行结果
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 3
at day1010.test3.Test1.main(Test1.java:7)
- try中可能会抛出多个不同的异常对象,则必须用多个catch来捕获-----即多种异常,多次捕获。
public static void main(String[] args) {
int[] arr={1,2,3};
try{
System.out.println("before");
//arr=null;
System.out.println(arr[100]);
System.out.println("after");
}catch(ArrayIndexOutOfBoundsException e){
System.out.println("这是个数组下标越界异常");
e.printStackTrace();
}catch(NullPointerException e){
System.out.println("这是个空指针异常");
e.printStackTrace();
}
System.out.println("after try catch");
}
如果多个异常的处理方式完全相同,也可以写成这样:
catch(ArrayIndexOutOfBoundsException|NullPointerException){
}
如果异常之间具有父子关系,一定是子类异常在前catch,父类异常在后catch,否则语法错误:
public static void main(String[] args) {
int[] arr={1,2,3};
try{
System.out.println("before");
arr=null;
System.out.println(arr[100]);
System.out.println("after");
}catch(Exception e){//Expection可以捕获到所有的异常
e.printStackTrace();
}catch(NullPointerException e){
System.out.println("这是个空指针异常");
e.printStackTrace();
}
System.out.println("after try catch");
}
//运行结果
java: 已捕获到异常错误java.lang.NullPointerException
正确代码:
public static void main(String[] args) {
int[] arr={1,2,3};
try{
System.out.println("before");
arr=null;
System.out.println(arr[100]);
System.out.println("after");
}catch(NullPointerException e){
System.out.println("这是个空指针异常");
e.printStackTrace();
}catch(Exception e){//Expection可以捕获到所有的异常
e.printStackTrace();
}
System.out.println("after try catch");
}
//运行结果
before
这是个空指针异常
after try catch
java.lang.NullPointerException
at day1010.test3.Test1.main(Test1.java:10)
- 可以通过一个catch捕获所有的异常,即多个异常,一次捕获(不推荐)
public static void main(String[] args) {
int[] arr={1,2,3};
try{
System.out.println("before");
arr=null;
System.out.println(arr[100]);
System.out.println("after");
}catch(Exception e){//Expection可以捕获到所有的异常
e.printStackTrace();
}
System.out.println("after try catch");
}
//运行结果
before
after try catch
java.lang.NullPointerException
at day1010.test3.Test1.main(Test1.java:9)
由于Expection类是所有异常类的父类。因此可以用这个类型表示捕捉所有的异常。
注:catch进行类型匹配的时候,不光会匹配相同类型的异常对象,也会捕捉目标异常类型的子类对象。比如前面的代码,NullPointerException和ArrayIndexOutOfBoundsExpection都是Expection的子类,因此都能被捕获到。
3.3 finally
在写代码时,有些特定的代码,不论程序是否发生异常,都需要执行。比如程序中打开的资源:网络连接、数据库连接、LO流等。在程序正常或者异常退出时,必须要对资源进行回收。另外,因为异常会引发程序的跳转,可能导致有些语句执行不到,finally就是用来解决这个问题的。
语法格式:
try{
//可能会发生异常的代码
}catch(异常类型 e){
//对捕获到的异常进行处理
}finally{
//此处的语句无论是否发生异常,都会被执行到
}
//如果没有抛出异常,或者异常被捕获处理了,这里的代码也会执行
public static void main(String[] args) {
try{
int[] arr={1,2,3};
arr[100]=10;
arr[0]=10;
}catch(ArrayIndexOutOfBoundsException e){
System.out.println(e);
}finally {
System.out.println("finally中的代码一定会执行");
}
System.out.println("如果没有抛出异常,或者异常被处理了,try-catch后的代码也会执行");
}
那么问题来了,既然finally和try-catch-finally后的代码都会执行,那为什么还要有finally呢?
需求:实现getData方法,内部输入一个整型数字,然后将该数字返回,并在main方法中打印。
public class TestFinally {
public static int getData(){
Scanner sc=null;
try{
sc=new Scanner(System.in);
int data=sc.nextInt();
return data;
}catch(InputMismatchException e){
e.printStackTrace();
}finally {
System.out.println("finally中的代码");
}
System.out.println("try-catch-finally之后的代码");
if(null!=sc){
sc.close();
}
return 0;
}
public static void main(String[] args) {
int d=getData();
System.out.println(d);
}
}
//运行结果
2
finally中的代码
2
上面的程序,如果正常输入,成功接收之后程序就返回了,try-catch-finally之后的代码根本没有执行。即输入流就没有被释放,造成资源泄露。
注意:finally中的代码一定会执行的,一般在finally中进行了一些资源清理的扫尾工作。
接着,我们来看一段代码;
这段代码的输出结果是什么呢?
public static int func(){
try{
return 10;
}finally {
return 20;
}
}
public static void main(String[] args) {
System.out.println(func());
}
正确答案是:20
注意:finally执行的时机是在方法返回之前(如果try或者catch中有return会在这个return之前执行finally)。但是如果finally中也存在return语句,那么就会执行finally中的return,从而不会执行到try中原有的return。
一般我们不建议在finally中写return,会被编译器当做一个警告。
4.异常的处理流程
关于调用栈:
方法之间是存在相互调用关系的,这种调用关系我们可以用"调用栈“来描述。在JVM中有一块内存空间称为"虚拟机栈"专门存储方法之间的调用关系。在代码中出现异常的时候,我们就可以使用e.printStackTrace();
的方式查看出现异常代码的调用栈。
如果本方法中没有合适的处理异常的方式,就会沿着调用栈向上传递。
public static void main(String[] args) {
try{
func();
}catch(ArrayIndexOutOfBoundsException e){
e.printStackTrace();
}
System.out.println("after try catch");
}
public static void func(){
int[] arr={1,2,3};
System.out.println(arr[100]);
}
//运行结果
java.lang.ArrayIndexOutOfBoundsException: 100
at day1010.test3.TestFinally.func(TestFinally.java:17)
at day1010.test3.TestFinally.main(TestFinally.java:9)
after try catch
如果向上一直传递都没有合适的方法处理异常,最终就会交给JVM处理,程序就会异常终止(和我们最开始未使用try-catch时是一样的)。
public static void main(String[] args) {
func();
System.out.println("after try catch");
}
public static void func(){
int[] arr={1,2,3};
System.out.println(arr[100]);
}
//运行结果
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 100
at day1010.test3.TestFinally.func(TestFinally.java:21)
at day1010.test3.TestFinally.main(TestFinally.java:8)
可以看到,程序以及异常终止了,没有执行 System.out.println("after try catch");
异常处理流程总结:
- 程序先执行try中的代码;
- 如果try中的代码出现异常,就会结束try中的代码,看和catch中的异常类型是否匹配;
- 如果找到匹配的异常类型,就会执行catch中的代码;
- 如果没有找到匹配的异常类型,就会将异常向上传递到上层调用者;
- 无论是否找到匹配的异常类型,finally中的代码都会被执行到(在该方法结束之前执行);
- 如果上层调用者也没有处理的了异常,就继续向上传递;
- 一直到main方法也没有合适的代码处理异常,就会交给JVM处理,此时程序就会异常终止。
三、自定义异常类
Java中虽然已经内置了丰富的异常类,但是并不能完全表示实际开发中所遇到的一些异常。此时就需要维护符合我们实际情况的异常结构。
举个栗子:我们想要实现一个用户登录功能。
public class Login {
String name = "admin";
String password = "123456";
public void loginInfo(String n, String p){
if(!name.equals(n)){
throw new UserException("用户名输入有误");
}
if(!password.equals(p)){
throw new PasswordException("密码输入有误");
}
//....
}
public static void main(String[] args) {
Login login = new Login();
login.loginInfo("admin","123456");
System.out.println("login ok");
}
}
此时我们在处理用户名密码错误的时候可能就需要抛出两种异常。我们可以基于已有的异常类进行扩展(继承),创建和我们业务相关的异常类。
具体方式:
- 自定义异常类,然后继承自Expection或者RunTimeExpection
- 实现一个带有String类型参数的构造方法。参数含义:出现异常的原因。
class UserError extends Exception {
public UserError(String message) {
super(message);
}
}
class PasswordError extends Exception {
public PasswordError(String message) {
super(message);
}
}
此时login代码可以改写成
public static void main(String[] args) {
try {
login("admin", "123456");
} catch (UserError userError) {
userError.printStackTrace();
} catch (PasswordError passwordError) {
passwordError.printStackTrace();
}
}
public static void login(String userName, String password) throws UserError,
PasswordError {
if (!Test.userName.equals(userName)) {
throw new UserError("用户名错误");
}
if (!Test.password.equals(password)) {
throw new PasswordError("密码错误");
}
System.out.println("登陆成功");
注意事项:
- 自定义异常通常会继承自Expection或者RunTimeExpection
- 继承自Exception的异常默认是受检查异常
- 继承自RunTimeException
一些总结
1.throw和throws的区别
- throw出现在方法体内部,throws在方法头,跟在参数列表之后。
- throws表示出现异常的一种可能性,并不一定会发生异常;throw则是抛出了异常,执行throw则一定会抛出某种异常。