文章目录
异常的使用
1.异常的概念
什么是异常呢?我相信大家在写代码的时候,会经常遇见这个东西,这个东西长什么样呢?看下面这几张图:
想必大家看到这几张图,就明白了什么是异常,我相信大家或多或少都有见到过这些东西,在Java中,程序执行的时候发生不正常的行为就称为异常,那你看看这个是不是异常:
这个就不是异常了,这个就是个错误了,遇见了这种错误,就说明你代码写的是有问题的,才会导致这种错误,一般遇到这种错误,程序就直接挂掉了,就不可能往后执行了,那异常呢?如果我们没有对他进行任何的处理,程序最后也会挂掉。所以我们得理解一下异常的体系和处理异常的方法。
2.异常的体系
在Java中异常有很多很多种,我们看看下面这张图,来了解一下异常的体系划分。
从图中可以分析出这么几点:
- Throwable:是异常体系的顶层类,其派生出两个重要的子类, Error 和 Exception。
- Error:指的是Java虚拟机无法解决的严重问题,比如:JVM的内部错误、资源耗尽等,典型代表:StackOverflowError,一旦发生程序就挂掉了。
- Exception:异常产生后程序员可以通过代码进行处理,使程序继续执行。我们平时所说的异常就是Exception。
3.异常的处理机制
异常类型
首先异常也分为两种:
- 运行时异常也称为非受查异常。
- 编译时异常也称为受查异常。
我写几个异常的案例让大家看看,大家就能明白异常的种类了:
运行时异常有这些:
-
ArithmeticException 算术异常
public static void main(String[] args) { //这个是算数异常ArithmeticException System.out.println(10 / 0); }
-
NullPointerException 空指针异常
public static void main(String[] args) { int[] arr = null; //这个是空指针异常NullPointerException System.out.println(arr.length); }
-
ArrayIndexOutOfBoundsException 数组下标越界异常
public static void main(String[] args) { int[] arr = {1,2,3}; //ArrayIndexOutOfBoundsException数组下标越界异常 System.out.println(arr[100]); }
编译时异常有这些:
-
CloneNotSupportedException克隆异常
class Student implements Cloneable{ @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } } public class Test { public static void main(String[] args) { Student s1 = new Student(); /** * 大家看这行代码,他为什么报红了,因为他调用了clone方法,里面抛出了一个异常叫做 * CloneNotSupportedException,这种异常就需要大家手动处理掉,如果你不处理,你的 * 代码就编译不通过,这里我就要给大家介绍一个新的关键字叫做throws,把你的鼠标放到 * 报错的位置上,按住alt+enter键,选择第一个选项,你会发现你的代码不报错了,为什 * 么呢?你会看到你的main方法后面多了一串字符叫做 * throws CloneNotSupportedException,这是用来干什么的呢?下面重点介绍 */ Student s2 = (Student)s1.clone(); } }
throws关键字的使用
在上面案例中,我们能看到加了throws,代码就没有报错了,这是为什么呢?我们先聊聊throws的作用。
throws的作用:用来声明一个异常,具体有什么用呢?举个例子,比如说我在写方法的时候,碰见了一个异常,但是我自己又不想处理,那这个时候就可以在方法的参数列表之后加一个throws,来声明这个异常,那这个时候,你就可以不用自己处理异常了,他就会交给方法的调用者来处理,如果这个方法的调用者也不想处理呢?也可以加throws来声明这个异常,那这个时候异常就会交给JVM来处理,那你的程序就会直接结束。也就是说如果你们都不想处理,那JVM就会替你们兜底。
public class Test {
//这里我创建了一个方法,里面抛出了一个受查异常也就是编译时的异常
public static void test()throws CloneNotSupportedException{
/**
* 在我抛出这个异常的时候,你会发现这还代码报红了,因为这个异常需要手动处理,
* 如果我不想处理怎么办,我想交给调用这个函数的人来处理,那你就在方法后面
* 加上throws关键字,来声明这个异常,这个代码就不会报错,相当于踢皮球,把
* 问题交给别人处理。
*/
//这里使用了throw关键字来抛出一个异常
throw new CloneNotSupportedException();
}
public static void main(String[] args)throws CloneNotSupportedException {
/**
* 你看在main方法这里,我调用test()方法,会直接报错,因为这个方法抛出的异常
* 还没有解决,所以他就在这里报错,如果我也不想处理掉这个异常该怎么办,那我也可以
* 在方法后面加上throws,来声明这个异常,那这个时候,异常就会交给JVM去处理,
* 但是不是说你加了这个关键字,你写的就是对的,JVM在看到这个异常的时候,会直接把程序结
* 束掉。我主要是想通过这个案例来讲解throws关键字怎么使用。
*/
test();
}
}
throws需要注意的点:
-
throws必须跟在方法的参数列表之后。
-
声明的异常必须是 Exception 或者 Exception 的子类。
-
方法内部如果抛出了多个异常,throws之后必须跟多个异常类型,之间用逗号隔开,如果抛出多个异常类型具有父子关系,直接声明父类即可。
throw关键字的使用
大家区分清楚哈!总有人会把这两个关键字搞混,一个是加了s,一个没有加,那throw关键字的作用是什么呢?
throw的作用:用来抛出一个异常,通常是用来抛出我们自定义的异常。
public class Test {
public static void test2(){
//通过throw关键字来抛出一个异常
throw new ArithmeticException();
}
public static void main(String[] args) {
/**
* 这个时候调用test2()方法,代码会直接报错,那如果想处理掉该怎么办,
* 这时候就要用到try catch了
* try catch语法:
* try {
* 这里写有问题的语句
* } catch (Exception e) {
* 这里写对有问题的语句进行处理的逻辑
* }
*/
try {
test2();
} catch (ArithmeticException e) {
System.out.println("ArithmeticException");
e.printStackTrace();
}
//这里你会发现,通过try catch这种方法处理掉的异常,后面的代码竟然会正常执行
System.out.println("aaaa");
}
}
throw需要注意的点:
- throw必须写在方法体内部。
- 抛出的对象必须是Exception或者是Exception的子类对象。
- 如果抛出的是 RunTimeException 或者 RunTimeException 的子类,则可以不用处理,直接交给JVM来处理。
- 如果抛出的是编译时异常,用户必须处理,否则无法通过编译。
- 异常一旦抛出,其后的代码就不会执行。
try catch语句块的使用
首先在方法后面加上throws并没有真正的解决掉这个异常,而是把这个异常交给别人去处理了,要想真正的处理好异常,还得靠try catch。
try catch的作用:这个语句块就是专门用来处理异常的,try语句块里面填写有问题的语句,catch语句块是负责捕获这些异常,比如说我try语句块里面抛出了一个受查异常,catch语句块刚好捕捉到了,那就可以在catch语句块里面写处理这个异常语句的逻辑。
public static void test2(){
//通过throw关键字来抛出一个异常
throw new ArithmeticException();
}
public static void main(String[] args) {
/**
* try catch语法:
* try {
* 这里写有问题的语句
* } catch (Exception e) {
* 这里写对有问题的语句进行处理的逻辑
* }
*/
try {
//这里存放会抛出异常的语句
//如果test2()这个方法抛出了一个异常,那么他后面的代码将不会执行
test2();
//这条语句不会被执行
System.out.println("bbb");
}catch (ArithmeticException e){
//这里写对这个异常处理的语句
System.out.println("算术异常");
e.printStackTrace();
}
//通过try catch处理的异常,后面的语句会正常执行
System.out.println("aaa");
}
通过上面例子我们能很直观的看出,真正要处理异常的话,还得靠try catch,那他里面有哪些需要注意的点呢?
try catch需要注意的点:
-
try语句块里面不一定会抛出异常。
public static void main(String[] args) { try { //这里面不一定会抛出异常,写正常代码也可以 int a = 10; int b = 20; System.out.println(a + b); } catch (Exception e) { e.printStackTrace(); } }
-
如果要处理多个异常的话,catch可以写多次。
public static void main(String[] args) { try { //算术异常 System.out.println(1 / 0); int[] arr = null; //空指针异常 System.out.println(arr.length); //数组下标越界异常 arr = new int[]{1, 2, 3}; System.out.println(arr[100]); /** * 如果要处理多个异常,catch也可以写多个,处理异常的顺序是根据try里面的代码, * 由上往下执行,谁先抛出异常,就会先处理那个异常,和catch写的顺序无关,并且 * 同一时间只会抛出一个异常,不会同时抛出多个,catch如果捕获到 * 了这个异常,就会进入对应的catch语句块里面,如果你抛出的这个异常,catch里 * 面没有对应的捕获代码,那最后这个异常就会交给JVM去处理,JVM就会把你的程序结束掉。 */ } catch (ArithmeticException e) { e.printStackTrace(); } catch (NullPointerException e) { e.printStackTrace(); } catch (IndexOutOfBoundsException e) { e.printStackTrace(); } }
-
try语句块里面抛出异常之后的代码将不会被执行。
public static void main(String[] args) { try { //算术异常 System.out.println(1 / 0); /** * 这一点刚刚有讲过,就是说如果上面的代码已经抛出异常了,那你后面的代码 * 都不会被执行。 */ System.out.println("aaaa"); }catch (ArithmeticException e){ e.printStackTrace(); } //try catch处理完异常后,后面的代码一定会被执行到 System.out.println("bbbbbbbbb"); }
-
如果说异常之间具有父子类关系的话,一定要把子类的异常放在最前面,父类的异常放在最后面,永远不要想着一劳永逸的写法哈!不要想着嫌麻烦,就把Exception写在最前面了,那样谁知道这个程序报的是什么异常呢?
public static void main(String[] args) { //这种写法有问题哈!不要这么写代码 //error写法 //try { //System.out.println(10 / 0); /** * 这个写法很有问题,Exception是所有异常的父类,那也就是说你放到catch的最前面 * 那后面的异常就不可能被捕获到了,所以代码就出错了,永远不要嫌麻烦,想着一劳 * 永逸的写法,正确做法是把Exception异常放到最后,如果前面的异常都没有捕获到 * 那还有Exception替你兜底。 * */ //}catch (Exception e){ // e.printStackTrace(); //}catch (ArithmeticException e){ // e.printStackTrace(); //} //正确写法: try { System.out.println(10 / 0); } catch (ArithmeticException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } System.out.println("bbb"); }
-
如果你的try语句块里面没有受查异常的话,但是你却在catch语句块里面写了一个捕获受查异常的代码,那你的程序一定会报错。这种写法是错误的。所以不要这么写代码哈!
public static void main(String[] args) { try { System.out.println(10 / 0); /** * 这里为什么报错了呢?我们的受查异常是在编译的时候 * 就会被编译器检测到,如果你在try语句块中没有写受查异常 * 相关的语句,但是你却在catch语句块里面写了捕获受查异常相关的语句, * 那么这个时候,你的代码就会报错了,因为你都没有抛出受查异常, * 那你想让catch帮你捕捉什么呢?所以这种写法也是有问题的。 */ } catch (CloneNotSupportedException e) { e.printStackTrace(); } }
finally语句块的使用
我们经常会看到别人的代码,就是在try catch后面会常常加着一个finally,那这个finally有什么作用呢?
finally的作用:这个语句块通常是用来关闭我们的连接资源,比如说数据库,文件操作阿!这些连接资源你用完之后,要记着及时关闭,否则会导致内存泄露。finally语句块的特点就是,无论程序如何执行,最后一定会执行finally语句块里面的语句。
-
释放连接资源
public static void main(String[] args) { /** * 大家看这个代码,我拿到创建了一个Scanner对象,最后就是 * 通过finally代码,来释放Scanner这个连接资源,大家写代码 * 的时候,记着就是用完资源之后,要及时释放掉。 */ //这是第一种释放的方式,还可以采用简写 Scanner scanner = new Scanner(System.in); int a = 0; try { a = scanner.nextInt(); } catch (ArithmeticException e) { System.out.println("b"); } finally { scanner.close(); } //也可以通过这种方式,来释放我们的连接资源,两种方式都可以 try (Scanner scanner2 = new Scanner(System.in)) { a = 0; a = scanner2.nextInt(); } catch (ArithmeticException e) { System.out.println("b"); } }
-
代码即使抛出了异常,finally语句块也会被执行到
public static void main(String[] args) { try { System.out.println(10 / 0); /** * 大家看这里没有写catch语句块,也就说这里肯定是捕获不到这个异常, * 那就会把这个异常交给JVM来处理,那你的程序就会被结束掉。 * 但是这个代码还是会执行finally语句块里面的内容。 */ } finally { System.out.println("finally里面的语句"); } /** * 你看程序是不是直接结束掉了,并且外面的代码没有执行到 * 但是finally里面的代码还是被执行了,这就印证了我说的那点 * finally里面的内容最后一定会被执行 */ System.out.println("外面的语句"); }
-
还有更极端一点的情况,比如说这种代码。
/** * 大家看这里我写了个方法,这个方法有返回值,但是会感觉到很奇怪,为什么有三个return呢? * 那到底返回哪一个值呢?正常的思考逻辑,执行到try语句块里面的时候,碰见return了,那这个时候 * 方法就应该已经结束了,然后把1给return回去,但是呢?这个代码里面有finally,我说过finally里面 * 的代码一定会被执行到,那现在是个什么逻辑呢?同学是这样的,try里面有return并且finally里面也有 * return的时候,那这个时候,代码不会执行try里面的return,而是去执行finally里面的return,然后 * 这个时候就会把3给return出去了,所以看到的结果就是3。大家记住在公司的时候,你是不会遇到这种代码 * 的,这种代码多半是面试题,就是面试官特意来考你的,所以大家不要思考错了方向。 */ public static int test() { try { return 1; } catch (ArithmeticException e) { return 2; } finally { return 3; } } public static void main(String[] args) { System.out.println(test()); }
以上就是finally所需要特别注意的一些点。
总结:想学好异常,那就先要把这5个关键字给弄懂。再给大家总结一下这5个关键字的作用是什么。
throws:用来申明一个异常,一般放在方法参数列表后面,花括号的前面。只能抛出Exception和Exception子类的异常。
throw:用来抛出一个异常,一般放在方法体里面。只能抛出Exception和Exception子类的异常。
try:用来存放有异常的语句。当然没有异常的语句也可以往里放。
catch:用来捕获一个异常。如果有多个异常的时候,可以写多个catch,如果异常里面有父子类关系的话,把子类放在前面,父类放在后面。
finally:用来释放各种连接资源。通常放在catch语句块之后,也可以放在try语句块之后。这个语句块的特点就是,无论程序如何执行,最后一定会执行里面的代码。
4.自定义异常
大家想一想,Java里面虽然提供了很多异常给我们,但是这里面一定能包含我想要的异常吗?不一定吧!如果我们想自己定义一个异常该怎么办呢?大家看下面这个案例:
//现在我有如下代码,这个代码没有用到任何异常的相关内容
public class Test {
//用户名
public static final String userName = "ZhangSan";
//密码
public static final String passWord = "123456";
public static boolean login(String username, String password) {
if (!userName.equals(username)) {
System.out.println("用户名错误");
return false;
}
if (!passWord.equals(password)) {
System.out.println("密码错误");
return false;
}
return true;
}
public static void main(String[] args) {
/**
* 现在我要登录了,我随便输入,他提示我登录失败,我感觉就是文字
* 给我的消息可能不够,他能不能告诉我是哪一行代码有问题呢?
* 哪一个输错了呢?这个时候我们就要采用异常的方式来处理
*/
String username = "aaa";
String password = "bbb";
if (login(username, password)) {
System.out.println("登录成功");
} else {
System.out.println("登录失败");
}
}
}
采用自定义异常的方式来处理:
/**
* 现在我们来看看如果自定义一个异常:
* 1.首先创建一个类,因为在之前分析的时候,我们会发现所有的异常其实就是一个类
* 2.这个类要继承Exception,因为我们之前说过,如果想用throw去抛出一个异常
* ,那这个异常必须是Exception或者是Exception的子类,所以要继承Exception
* 3.用throw去抛出这个异常
*/
//创建一个类,需要继承Exception或者是Exception的子类
class UserException extends RuntimeException {
//这里面还可以写构造方法,帮助我父类进行初始化
public UserException() {
super();
}
//这里我们能了解到父类里面有个成员属性message,如果我给里面赋值会发生什么呢
public UserException(String message) {
super(message);
}
}
//创建一个类,需要继承Exception或者是Exception的子类
class PassWordException extends RuntimeException {
//这里面还可以写构造方法,帮助我父类进行初始化
public PassWordException() {
super();
}
//这里我们能了解到父类里面有个成员属性message,如果我给里面赋值会发生什么呢
public PassWordException(String message) {
super(message);
}
}
//现在我有如下代码,这个代码正在用异常的相关内容
public class Test2 {
//用户名
public static final String userName = "ZhangSan";
//密码
public static final String passWord = "123456";
public static void login(String username, String password) {
if (!userName.equals(username)) {
//通过throw去抛出我们自定义的异常
/**
* 现在爆红是因为我们自定义的异常是一个受查异常,但是这个异常不符合我们的逻辑,
* 那我们可以让他继承RuntimeException,变成一个运行时的异常,这个时候就不会报错了
*/
//没有赋值的构造方法,里面没有任何参数的构造方法,只会打印你错在哪一行了
//throw new UserException();
//赋值的构造方法,这种加了参数的构造方法,可以把你自己想说的信息写在里面
throw new UserException("大兄弟,你的用户名错啦!");
}
if (!passWord.equals(password)) {
//通过throw去抛出我们自定义的异常
//没有赋值的构造方法,里面没有任何参数的构造方法,只会打印你错在哪一行了
//throw new PassWordException();
//赋值的构造方法,这种加了参数的构造方法,可以把你自己想说的信息写在里面
throw new PassWordException("大兄弟,你的密码错啦!");
}
}
public static void main(String[] args) {
String username = "aaa";
String password = "111";
/**
* 现在异常已经定义好了,我们运行看看效果,这个时候就达到我想要的效果了,
* 编译器通过错误提示,告诉我错在哪里了。
*/
login(username, password);
System.out.println("登录成功");
}
}
通过上面的案例,我们能了解到自定义的异常的步骤,并且了解到异常的好处,通过异常提供的消息,我们能很快的发现,我们的错误错在哪里了,这里再给大家总结一下,自定义异常的步骤:
- 首先创建一个类,因为在之前分析的时候,我们会发现所有的异常其实就是一个类。
- 这个类必须继承Exception或者是Exception的子类,因为我们之前说过,如果想用throw去抛出一个异常,那这个异常必须是Exception或者是Exception的子类,所以我们要继承Exception,变成Exception的子类才能被抛出去。
- 我们继承Exception类之后,可以创建两个构造方法,一个带参数的,一个不带参数,主要来了解一下这个带参数的方法,这个参数是个String类型,我们可以把我们想要说的话,通过参数传递过去,这样你就能在控制台上看到你所写的相关信息,对程序员更友好一点,程序员一看到这个信息,立马就知道自己错在哪里了。
- 用throw去抛出这个异常。
- 如果继承的是Exception的话,默认是一个受查异常,如果继承的是RuntimeException的话,默认是一个运行时异常。
5.致谢
感谢你一路看到这里,如果你觉得我写的不错的话,请给我一键三连!!!😉😉