文章目录
1.异常的概念
什么是异常(Exception)呢?
异常就是在程序执行过程中发生的不正常的行为.异常中断了正在执行程序的正常指令流
异常其实就是类.异常是可以继承的
异常产生后程序员可以通过代码进行处理,使程序继续执行,比如:我们感冒发烧,进行一些处理是会好起来的.
2.异常产生的原因
主要有3种原因:
😊编写程序的代码产生的错误.比如数组越界,空指针异常等.这些异常叫做未受查异常.
😊Java内部错误产生的异常,Java虚拟机产生的异常
😊通过throw(抛出异常)语句手动生成的异常,这种异常叫做受查异常,可以用来给方法的调用者一些信息.
3.异常的分类
🤨Throwable是异常体系的顶层类,它派生出两个子类,分别是Error(错误)和Exception(异常),前者是不受查异常(Unchecked Exception),后者是受查异常(Checked Exception).
异常的继承结构:
基类为Throwable,Error和Exception继承自Throwable,RunTimeException和IOException等继承自Exception.
非RunTimeException一般是外部错误(除了Error),其必须被try-catch语句捕获
🤨Error定义了在通常情况下不希望被程序捕获的异常.Error类型的异常是在运行时出现的错误.比如堆和栈的溢出.(StackOverflowError,OutOfMemoryError)这类异常是Java虚拟机无法解决的严重问题.
Error类体系描述了Java运行系统的内部错误以及资源耗尽的情景,Error不需要捕获.
因为异常可以在运行时发生,也可以在编译时发生,所以我们将异常分为运行时异常和编译时异常.
3.1 运行时异常
**运行时异常(不受查异常)**包括RuntimeException及其子类异常,比如
NullPointerException,IndexOutOfBoundsException,ArithmeticException.
也就是说这一类型的异常程序可以选择处理也可以选择不处理.这一类型的异常一般由于程序逻辑错误引起.
运行时异常指的是运行时的程序已经编译通过得到class文件了,再由JVM执行的时候出现的错误.
我们来举个例子:
当我们执行这段代码:
public class Test2 {
public static void main(String[] args) {
System.out.println(10/0);
}
}
运行后的结果是:
ArthmeticException这个异常叫做算术异常
再来看另一个代码:
public class Test2 {
public static void main(String[] args) {
int[] array=null;
System.out.println(array.length);
}
}
运行后的结果是:
NullPointerException这个异常叫做空指针异常.
再来看另一段代码:
public class Test2 {
public static void main(String[] args) {
int[] array= {1,2,3};
System.out.println(array[10]);
}
}
运行后的结果:
IndexOutOfBoundsException叫做下标越界异常
3.2 编译时异常
编译时异常就是RuntimeException以外的其他异常,包括Exception类及其子类异常.编译时异常是必须要处理的异常,如果不进行处理,那么程序的编译就不能通过.例如IOException,ClassNotFoundException,以及其他一些用户自定义的异常.一般用户自定义的异常都是编译时异常.
🐱例子:
public class Person implements Cloneable{
public Object clone() throws CloneNotSupportedException{
return super.clone();
}
}
public class Test2 {
public static void main(String[] args) {
Person person1=new Person();
Person person2=(Person) person1.clone();
}
}
结果:
正确的处理是这样:
🐶注意:**编译时出现的语法性错误,不能称之为异常.比如:System.out.println这一句代码拼写错误**,这属于"编译期出错"
3.3 自定义异常
😆虽然Java中有很多异常类,但是在实际开发中所遇到的一些异常,不能完全表示,所以就需要自定义异常类.
自定义异常默认会继承Exception或者RunTimeException.继承自Exception的异常默认是受查异常,继承自RunTimeException的异常默认是非受查异常.
比如,我们来自定义一个运行时异常:
public class MyException extends RuntimeException{
public MyException(){
}
public MyException(String message){
super(message);
}
}
我们来捕获一下这个异常:
结果:
👀接下来我们来看一个用户登录的自定义异常类:
public class UserNameException extends RuntimeException{
public UserNameException(){
}
public UserNameException(String message){
super(message);
}
}
public class PassWordException extends RuntimeException{
public PassWordException(){
}
public PassWordException(String message){
super(message);
}
}
public class Login {
public static String uName="zuozuo";
public static String pWord="666666";
public static void loginInfo(String userName,String passWord){
if(!uName.equals(userName)){
throw new UserNameException("用户名错误");
}
if(!pWord.equals(passWord)){
throw new PassWordException("密码错误");
}
System.out.println("登录成功");
}
public static void main(String[] args) {
try{
loginInfo("zuozuo","666666");
}catch (UserNameException e){
e.printStackTrace();
}catch (PassWordException e){
e.printStackTrace();
}
}
}
4.异常的处理
在java中,异常处理主要的5个关键字就是:throw,try,catch,final,throws
前提:错误在代码中是客观存在的,因此我们要让程序出现问题的时候及时通知程序员.所采取的方式有以下几种.
4.1 防御式编程
😺(1)LBYL:也就是Look before you leap.在操作之前就做充分的检查,也就是事先预防性
例:
public int func() {
Scanner sc=new Scanner(System.in);
int a=sc.nextInt();
int b=sc.nextInt();
if(b==0){
System.out.println("除数为0");
return 0;
}else {
return a/b;
}
}
但是这样做的缺点是:
正常流程和错误处理流程混在了一起,代码整体显得比较混乱
😺(2)EAFP:It is easier to ask forgiveness than permission.“事后获取原来比事先获取许可更容易”,也就是先操作,遇到问题再处理.事后认错型.
例:
public int func() {
try(Scanner sc=new Scanner(System.in)){
int a=sc.nextInt();
int b=sc.nextInt();
return a/b;
}catch(ArithmeticException e){
System.out.println("除数为0");
return 0;
}
}
优点:正常流程和错误流程分离开,程序员更关注正常流程,代码更清晰,容易理解代码.
🙉那么,异常处理的核心思想就是EAFP
问题来了:
处理异常的前提是什么?
处理异常的前提是得有异常,那么怎么才能有异常,那你就得抛出(触发)异常.
那如何抛出异常呢?请看下面.
4.2 异常的抛出
在编写程序时,如果程序中出现了错误,那么就需要将错误的信息告诉调用者.
在java中,可以通过throw关键字.抛出一个指定的异常对象,将错误信息告诉调用者.
语法规则是这样的:
throw new XXXXException(“异常产生的原因”);
例:获取数组任意位置元素
public static int getElement(int[] array,int index){
if(array==null){
throw new NullPointerException("传递的数组为null");
//抛出的是一个指定的异常,经常用的方式是,抛出自定义异常
}
return array[index];
}
🧐一些注意事项:
(1).throw必须写在方法体内部.
(2).抛出的对象必须是Exception或者Exception的子类对象
(3).如果抛出的是不受查异常(RunTimeException或者RunTimeException的子类),那么可以不用处理,直接交给JVM来处理
(4).如果抛出的是编译时异常,那么用户必须处理,否则无法通过编译
(5).异常一旦抛出,后面的代码就不会执行.
第五个注意点的例子:
结果就是:在没有对异常进行处理的情况下,抛出了异常,并且异常后面的内容不会被打印出来.(也就是异常抛出之后的代码都不会执行)
😎补充:
当我们没有解决这个异常的时候,这个异常就会被交给JVM来处理,一旦交给JVM来处理,程序就崩溃了.
5.异常的捕获
异常的捕获:也就是异常的具体处理方式.主要有两种:异常声明throws和try-catch捕获处理.
5.1 异常声明throws
也就是,在方法声明时参数列表之后,当方法中抛出编译时异常,用户不想处理该异常,就可以借助throws将异常抛给方法的调用者来处理,换句话说就是:当前方法不处理异常,提醒方法的调用者处理异常.
语法格式:
方法名(参数列表) throws 异常类型1,异常类型2…{
}
例:
还需要注意的一点:
调用声明抛出异常的方法时,调用者必须对异常进行处理,或者继续使用throws抛出.
😏如果方法内部抛出了多个异常,那么throws之后必须跟多个异常类型,用逗号分隔.
例:
😏如果抛出多个异常,并且有父子关系,那么直接声明父类.
例:
😁注意事项:
(1)throws必须跟在声明的方法的参数列表之后
(2)声明的异常必须是Exception或者Exception的子类
(3)方法内部如果抛出了多个异常,throws之后必须跟多个异常类型,之间用逗号隔开,如果抛出的多个异常类型之间有父子关系,那么直接声明父类即可.
5.2 try-catch捕获异常并处理
当程序抛出异常时,可以通过try-catch捕获并处理异常.
语法格式:
try{
//可能出现异常的代码,try中的代码可能会抛出异常,也可能不会抛出异常
}catch(要捕获的异常类型 e){
//如果try中的代码抛出异常了,此处catch捕获到的异常类型与try中抛出的异常类型一样时,或者是try中抛出的异常的父类时,就会被捕获到
//对异常就可以正常处理,处理完成后,跳出try-catch结构,继续执行之后的代码
}catch(异常的类型 e){
//对异常进行处理
}finally{
//此处的代码一定会被执行到
}
//之后的代码
//当异常被捕获到时,异常就被处理了,之后的代码一定会执行
//如果被捕获了,由于捕获的类型与抛出的异常的类型不匹配,还是会交给JVM来处理,那就没有捕获到,之后的代码也不会执行,相当于没有对抛出的异常进行正确的处理
例:
public static void main(String[] args) {
try{
int[] array=null;
System.out.println(array.length);
}catch (NullPointerException e){
System.out.println("捕获到一个空指针异常");
}
System.out.println("其他程序");
}
结果:
例:
public static void main(String[] args) {
try{
int[] array=null;
System.out.println(array.length);
}catch(ArithmeticException e){
System.out.println("捕获到空指针异常");
}
System.out.println("其他代码");
}
try-catch捕获到的不是抛出来的对应的异常,那就相当于没有对异常进行处理,那就会交给JVM来处理,后面的代码不会执行到.因为异常是按照类型来捕获的
结果:
😜如果try抛出了多个异常,就必须用多个catch来进行捕获
但是需要注意:用多个catch进行捕获,不是同时进行捕获的,因为不可能同时抛出不同的异常.
不会同时抛出两个或两个以上的异常,在同一时间,只会抛出一个异常.
例:
public static void main(String[] args) {
try{
int[] array=null;
System.out.println(array.length);
}catch(ArithmeticException e){
System.out.println("捕获到算术异常");
}catch(NullPointerException e){
System.out.println("捕获到空指针异常");
}
System.out.println("其他代码");
}
也可以简写成:
public static void main(String[] args) {
try{
int[] array=null;
System.out.println(array.length);
}catch(ArithmeticException | NullPointerException e){
System.out.println("捕获到算术异常或者空指针异常");
}
System.out.println("其他代码");
}
😁当try中存在多个异常时,从上往下执行,谁先抛出异常就捕获哪个异常.
换句话说也就是:try块内抛出异常位置之后的代码将不会执行.
catch的顺序不是捕获的顺序
![在这里插入图片描述](https://img-blog.csdnimg.cn/146cf73ce5e64a8b82fdba34ac777e7a.png
😮还有需要注意的一点是:
如果抛出的异常之间具有父子关系,那在catch时,子类异常的catch必须写在前面,父类异常的catch必须写在后面.
例:
public static void main(String[] args) {
try{
int[] array=null;
System.out.println(array.length);
}catch(NullPointerException e){
System.out.println("捕获到空指针异常");
}catch(Exception e){//可以把父类异常放在最后
System.out.println("捕获到一个异常");
}
System.out.println("其他代码");
}
结果:
来看这张图片:
这样的写法就是把父类异常的catch写在了子类异常的catch的前面,这样写代码编译就会报错.
原因是:Exception是所有异常的父类,它能捕获所有的异常,如果把他放到catch的第一个,后面的异常永远都不会捕获到,那么后续的catch没有任何作用了.
注意:
catch进行类型匹配的时候,不光会匹配相同类型的异常对象,也会捕获目标异常类型的子类对象.比如NullPointerException和ArrayIndexOutOfBoundsException都是Exception的子类,因此都能被捕获到.
5.3 finally
在写程序时,有些代码,不论程序是否发生异常,都需要执行,比如程序中打开的资源:网络连接,数据库连接,IO流,在程序正常或者异常退出时,都必须要对资源进行回收.而且,因为异常会引发程序的跳转,可能导致有些语句执行不到,finally就是用来解决这个问题的.
语法格式:
try{
//可能发生异常的代码
}catch(异常类型 e){
//对捕获到的异常进行处理
}finally{
//此处的语句无论是否发生异常,都会被执行到
}
//如果没有抛出异常,或者异常被捕获处理了,这里的代码也会执行.
😜例:
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
try{
int[] array=null;
System.out.println(array.length);
}catch(NullPointerException e){
e.printStackTrace();
System.out.println("捕获到空指针异常");
}catch (ArithmeticException e){
e.printStackTrace();
System.out.println("捕获到算术异常");
}finally {
sc.close();
System.out.println("资源关闭");
}
System.out.println("其他代码");
}
结果:
以下的代码是没有抛出异常的情况:
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
try{
int[] array= {1,2,3,4};
System.out.println(array.length);
}catch(NullPointerException e){
e.printStackTrace();
System.out.println("捕获到空指针异常");
}catch (ArithmeticException e){
e.printStackTrace();
System.out.println("捕获到算术异常");
}finally {
sc.close();
System.out.println("资源关闭");
}
System.out.println("其他代码");
}
结果:
我们发现:不管是否抛出异常,finally中的代码都会执行.
😎补充一点:
资源写到try之后,finally中就不用手动close了
来看一段特殊的代码:
结果是20.如果finally中也存在return,那么就会执行finally中的return,从而不会执行到try中原有的return.
但是,一般不建议在finally中写return
🤓面试题:
1.throw和throws的区别?
throw用来抛出异常,throws用来声明异常
2.finally中的语句一定会执行吗?
finally中的语句一定会执行
6.异常的处理流程
😎关于调用栈:
方法之间是存在相互调用关系的,这种调用关系我们可以用"调用栈"来描述.在JVM中有一块内存空间称为"虚拟机栈",它是专门用来存储方法之间的调用关系.当代码中出现异常的时候,我们就可以用e.printStacktrace()的方式来查看出现异常的代码的调用栈.
如果本方法中没有合适的处理异常的方式,就会沿着调用栈(先进后出)向上传递.如果向上一直传递都没有处理异常,最终就会交给JVM来处理,程序就会崩溃.
例:
public static void func(){
int[] array={1,2,3,4,5};
System.out.println(array[99]);
}
public static void main(String[] args) {
try{
func();
}catch (ArrayIndexOutOfBoundsException e){
e.printStackTrace();
}
}
结果:
😶异常处理的流程:
1.程序先执行try中的代码
2.如果try中的代码抛出异常,就会结束try中的代码,然后和catch中的异常类型进行匹配
3.如果找到匹配的异常类型,就会执行catch中的代码
4.如果没有找到匹配的异常类型,就会将异常向上传递到上层调用者
5.无论是否找到匹配的异常类型,finally中的代码都会被执行
6.一直到main方法也没有代码去处理异常,就会把异常将给JVM来处理,这样程序就异常终止了.(崩溃了)