异常
1 异常概述
1.1描述
程序在运行过程中,由于意外情况导致程序发生异常事件,默认情况下发生的异常会中断程序的运行。
在Java中,把常见的异常情况,都抽象成了对应的异常类型,那么每种异常类型都代表了一种特定的异 常情况。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5lianot5-1638149967118)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210621154521092.png)]
例如:
java.lang.ArrayIndexOutOfBoundsException
,将来这个类的对象,就表示程序中出现了数组下 标超过边界的异常情况。
public class Test {
public static void main(String[] args) {
int[] arr = {1,3,5,7};
System.out.println(arr[2]);
//这行代码执行时,出现异常情况,因为下标超过了数组的最大边界
System.out.println(arr[4]);
}
}
//运行结果:
Exception in thread "main" java.lang.ArrayIndex
可以看出,当前程序出现异常情况时,会创建并抛出和该异常情况对应的异常类的对象,这个异常 对象中保存了一些信息,用来表示当前程序到底发生了什么异常情况。
通过异常信息,我们可以定位异常发生的位置,以及异常发生的原因
1.2 异常体系
异常体系中的根类是: java.lang.Throwable
,该类下面有俩个子类型, java.lang.Error
和 java.lang.Exception
注意, Throwable
表示可以被抛出的
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mOrhAbf3-1638149967146)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210621155331175.png)]
Throwable
是所有异常类的父类,是Object的直接子类;Error
,是Throwable
的直接子类,表示错误情况,一般实程序出现了严重的问题,并且程序自身无法进行处理;Exception
,是Throwable
的直接子类,表示异常情况,程序中出来了这种异常,大多是可以通过特定的方式进行处理和矫正;
我们一般说的异常都是Exception
Exception
是Throwable
的直接子类,它的方法都是继承过来的,
例如:
printStackTrace
() ,打印输出当前发送异常的详细信息(重要)getMessage
() ,返回异常对象被抛出的时候,所携带的信息,一般是异常的发生原因(重要)printStackTrace(PrintWriter s)
,方法重载,可以指定字符输出流,对异常信息进行输出printStackTrace(PrintStream s)
,方法重载,可以指定字节输出流,对异常信息进行输出
1.3 异常种类
我们平时使用的异常类型,都是 Exception
类的子类型,它们把异常划分成了俩种:
- 编译时异常(检查性异常);
- 运行时异常;
- error错误: 错误不是异常,而是脱离程序员控制的问题。错误在代码中通常被忽略。例如,当栈溢出时,一个错误就发生了,它们在编译也检查不到的。
编译时异常,继承自 Exception
类,也称为checked exception
,编译器在编译期间,会主动检查这种异 常,发现后会报错,并提示我们要对这种异常进行处理。
运行时异常,继承自 RuntimeException
类,也称为unchecked exception,编译器在编译期间,不会检 查这种异常,也不要求我们去处理,但是在运行期间,代码中可能会抛出这种类型的异常。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jtcKTQ1d-1638149967148)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210621165920827.png)]
1.4 异常传播
如果一个方法抛出了异常,并且一直没有进行处理,那么该异常会抛给当前的调用者,并且一直往上抛,直到抛给JVM,最后JVM将这个遗异常信息打印出来,同时程序停止运行。
例如:
public class Test {
public static void main(String[] args) {
System.out.println("hello");
test1();//5
System.out.println("world");
}
public static void test1(){
test2();//10
}
public static void test2(){
test3();//13
}
public static void test3(){
int a = 1/0;//16
}
}
//运行结果:
hello
Exception in thread "main" java.lang.ArithmeticException: / by zero
at com.briup.demo.Test.test3(Test.java:16)
at com.briup.demo.Test.test2(Test.java:13)
at com.briup.demo.Test.test1(Test.java:10)
at com.briup.demo.Test.main(Test.java:5)
从运行结果可以看出,,因为是运行时异常,编译器不检查,这个异常在程序中没有 处理,那么这个异常就抛给了test2,test2方法又将异常抛给了test1方法,test1中又将异常抛给 main方法,main方法将异常抛给JVM,最后JVM将当前发生异常时,栈内存中方法的调用情况,打 印输出,方便我们定位错误信息!之后JVM停止运行。
对应的栈区中,方法调用的情况:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RNgHfP1R-1638149967151)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210621170610301.png)]
可以看出,异常信息打印输出的内容,就是发生异常的时候,栈区中方法调用的情况
- test3方法中的第16行代码抛出异常,导致test2方法中的第13行也报错,因为test2方法中调用了test3方法
- test2方法中的第13行报错,导致test1方法的第10行也报错,因为test1方法中调用了test2方法
- test1方法中的第10行报错,导致main方法中的第5行也报错,因为main方法中调用了test1方法
- 这时候,test3方法中抛出的异常对象,就传播到了main方法中,main中也没处理,那么就把这个异常抛 个JVM,那么JVM就打印输出异常信息,然后JVM停止运行!
- 所以程序最后停止在main方法中的第5行,test1方法的调用这句代码,下面的world也没有输出,因为 JVM停止了,代码就不再往下执行了!
如果,在异常传播的过程中,任何一个地方对异常进行了处理,那么JVM不会停止,程序还会正常 往下运行!
2 异常抛出
2.1 自动抛出异常
当前java代码中,出现了提前指定好的异常情况的时候,代码会自动创建异常对象,并且将该异常对象 抛出。
例如,当代码中执行 int a = 1/0; 的时候,代码会自动创建并抛出 ArithmeticException 类型的 异常对象,来表示当前的这种异常情况。(算术异常)
例如,当前代码中执行 String str = null; str.toString(); 的时候,代码会自动创建并抛出 NullPointerException 类型的异常对象,来表示当前这种异常情况。(空指针异常)
2.2 手动抛出异常
以上描述的异常情况,都是JVM中提前规定好的,我们不需要干预,JVM内部自己就会创建并抛出异常对 象。
但是在其他的一些情况下,我们也可以手动的创建并抛出异常对象,其效果也是一样的。
例如
public class ExcTest {
public void test(String name){
if(!"tom".equals(name)){
throw new RuntimeException("用户名和预期不符!");
}
}
public static void main(String[] args) {
ExcTest et=new ExcTest();
et.test("lisa");
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uCPP18qv-1638149967153)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210621171452109.png)]
注意,因为方法中抛出的是一个运行时异常,编译器不会做出检查,所以代码可以正常的编译运 行,但是运行的时候,name的值不是tom的时候,代码会报错,这个错误信息是我们自己抛出的
抛出编译时异常
public void test(String name) throws Exception {
if(!"tom".equals(name)){
throw new Exception("用户名和预期不符!");
}
}
public static void main(String[] args) throws Exception {
ExcTest et=new ExcTest();
et.test("lisa");
}
使用
throws
关键字,声明方法所抛出的异常类型即可这个声明的目的,就是告诉test方法的调用者,你调用我的这个test方法的时候要小心啦,方法在 运行的时候可能会抛出Exception类型的异常
这里描述为可能会抛出异常的原因是,只有name的值不是tom的时候才会抛出异常,其他情况没 有异常!
我们在调用API中方法的时候,就需要注意这个方法是否声明了可能抛出的异常类型,例如 forName
方 法
public final class Class{
public static Class<?> forName(String className)throws ClassNotFoundException{
//...
}
}
可以看出,
forName
就声明了,方法在执行过程中可能会抛出ClassNotFoundException
类型 的异常同时,
ClassNotFoundException
属于编译异常,所以我们调用forName
方法时候就要处理这 个异常,或者将异常继续抛出!
2.throws/throw 关键字:
如果一个方法没有捕获到一个编译时异常,那么该方法必须用throws关键字来声明。throws关键字放在方法签名的尾部。
也可以使用 throw 关键字抛出一个异常,无论它是新实例化的还是刚捕获到的。
一个方法可以声明抛出多个异常,多个异常之间用逗号隔开。
3 异常捕获
3.1 概述
当一个方法内,抛出了编译异常的时候,编译器在编译期间检查到,就会报错,提示我们有俩种修改方 案:
- 把这个异常在方法中抛出;
- 把这个异常在方法中进行处理;
1.声明抛出:
public void test(String className)throws ClassNotFoundException{
Class.forName(className);
}
可以看出,这里我们并没有处理forName方法抛出的异常,而是将这个异常继续声明抛出,那么将 来谁调用我们的test方法,谁就要处理这个异常情况
2.捕获处理
public void test(String className){
try {
Class.forName(className);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
这里使用了try-catch语句块,对可能抛出异常的代码进行异常捕获处理
3.2 try-carch 语句
try-catch语句块,就是用来对指定代码,进行异常捕获的,并且处理完成后,JVM不会停止运行,代码还可以正常的往下运行;
try{
编写可能会出现异常的代码
}catch(异常类型 e){
//处理异常的代码,可以是简单的输出异常信息,也可以使用日志进行了记录,也可以对数据进行修改纠正等操作
}
try:该代码块中编写可能产生异常的代码。
catch:用来进行某种异常的捕获,并对捕获到的异常进行处理。
eg:
public class Test {
public static void main(String[] args) {
System.out.println("hello");
Test t = new Test();
try {
t.test("zs");
} catch (Exception e) {
//打印输出发生异常的时候,栈里面的方法调用情况,方便我们定位和修改代码
e.printStackTrace();
}
System.out.println("world");
}
public void test(String name)throws Exception{
if(!"tom".equals(name)){
throw new Exception("用户名不正确");
}
}
}
//运行结果:
hello
world
java.lang.Exception: 用户名不正确
at com.briup.demo.Test.test(Test.java:20)
at com.briup.demo.Test.main(Test.java:9)
可以看出,try-catch语句处理过异常后,代码继续往下执行了,最后输出了world
3.3 捕获多种异常
如果try语句块中的多句代码,都会抛出异常,并且是不同类型的异常,那么catch语句块就有不同的写 法,来处理这几个不同类型的异常。
public static void main(String[] args) throws Exception {
String className="com.chapter09.ExcTest1";
String methodName="sayHello";
try {
//forName声明抛出ClassNotFoundException
Class c=Class.forName(className);
//getMethod方法声明抛出NoSuchMethodException
Method m=c.getMethod(methodName);
//invoke方法声明抛出IllegalAccessException和InvocationTargetException
m.invoke(null);
}catch (ClassNotFoundException |NoSuchMethodException |IllegalAccessException| InvocationTargetException e){
e.printStackTrace();
}
}
这里,使用一个catch语句,里面使用
|
来表示捕获多种不同的异常类型
public static void main(String[] args) throws Exception {
String className="com.chapter09.ExcTest1";
String methodName="sayHello";
try {
//forName声明抛出ClassNotFoundException
Class c=Class.forName(className);
//getMethod方法声明抛出NoSuchMethodException
Method m=c.getMethod(methodName);
//invoke方法声明抛出IllegalAccessException和InvocationTargetException
m.invoke(null);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
这里,使用了四个catch语句,分别对四种不同的异常类型进行捕获处理
注意,这种异常处理方式(多个catch块)
- 要求:多个catch中的异常不能相同,并且如果catch中的多个异常之间有 子父类异 常的关系的话,那么子类异常要求在上面的catch处理,父类异常在下面的catch处理。 因为如果父类型异常再最上面的话,下面catch语句代码,永远不会被执行;
- 先捕获子类,再捕获父类(若先捕获了父类Exception,那么后面子类的异常就会报错,因为父类已经包含了子类);
public static void main(String[] args) {
String className = "com.briup.demo.Student";
String methodName = "sayHello";
try {
//forName声明抛出ClassNotFoundException
Class c = Class.forName(className);
//getMethod方法声明抛出NoSuchMethodException
Method m = c.getMethod(methodName);
//invoke方法声明抛出IllegalAccessException和InvocationTargetException
m.invoke(null);
} catch (Exception e) {
e.printStackTrace();
}
}
这里,使用了一个catch语句,但是捕获的异常类型是Exception,它最大的异常类型,由于多态的 原因,Exception类型的引用e,可以捕获接收到任意类型的异常对象
3.4 finally语句
try-catch语句块,虽然可以捕获并处理异常情况,但是它也会改变代码的执行流程,例如:
public class Test {
public static void main(String[] args) {
System.out.println("hello");
Test t = new Test();
try {
t.test("zs");
System.out.println("briup");//注意观察,这句代码是否执行
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("world");
}
public void test(String name)throws Exception{
if(!"tom".equals(name)){
throw new Exception("用户名不正确");
}
}
}
//运行结果:
hello
world
java.lang.Exception: 用户名不正确
at com.briup.demo.Test.test(Test.java:20)
at com.briup.demo.Test.main(Test.java:9)
可以看出,以上代码中的第10行,并没有执行,因为第9行代码调用方法出现异常时,代码就从第9 行跳到了11行的catch语句块,刚好把11行的输出语句代码给跳过去了。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4lvGDuVb-1638149967157)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210621190259937.png)]
如果第11行是比较重要的代码,那么如何保证无论发生什么情况,它一定会被执行呢?
其实只要使用 finally
关键,就可以保证指定代码一定会执行,无论是否发生异常!
例如, try-catch-finally
,固定搭配,类似于 if-elseif-else
,可拆开使用,也可以一起使用
public class Test {
public static void main(String[] args) {
System.out.println("hello");
Test t = new Test();
try {
t.test("zs");
} catch (Exception e) {
e.printStackTrace();
}finally {
System.out.println("briup");//这句代码一定会被执行
}
System.out.println("world");
}
public void test(String name)throws Exception{
if(!"tom".equals(name)){
throw new Exception("用户名不正确");
}
}
}
//执行结果:
hello
briup
world
java.lang.Exception: 用户名不正确
at com.briup.demo.Test.test(Test.java:21)
at com.briup.demo.Test.main(Test.java:6)
eg:
public static void main(String[] args) {
System.out.println("hello");
try {
int a = 1;
if(a>0){
return;
}
} finally {
System.out.println("briup");//会被执行
}
System.out.println("world");//不会执行
}
4 自定义异常
4.1 自定义异常的原因
JavaAPI中已经存在的异常类,都是当年sun公司,提前定义好的,它们分别表示着某一种已知的异常情 况。
但是,在我们开发的系统中,大多数业务功能里面总会出现一些新的异常情况,而这些异常情况,当年 sun公司定义异常类型的时候,肯定是想不到的.
例如,在学生信息管理系统中,学生的年龄设置为负数、学生的成绩大于了100分,用户登录时候的密码 不正确、用户访问某些接口时候的权限不足等情况,在系统中都是异常情况,而这些异常类型在JavaAPI 中都是没有的。
所以,在实际开发中,我们会自定义一些异常的类型,用来表示上面描述的那些异常情况,这样做的好 处就是,我们通过观察系统的运行日志,就可以很快的知道当前系统是发生了什么事情,才导致出了这 些异常情况。
4.2 如何自定义异常
- 如果要自定义一个编译时异常类型,就自定义一个类,并继承
Exception
- 如果要自定义一个运行时异常类型,就自定义一个类,并继承
RuntimeException
例如,自定义编译时异常类型,通过名字可知,这是在用户登录期间发生异常时,应该创建并抛出的异 常类型
public class LoginExceptin extends Exception{
public LoginExceptin(){
}
public LoginExceptin(String message) {
super(message);
}
}
例如,自定义运行时异常类型,通过名字可知,这是在修改用户信息期间发生异常时,应该创建并抛出 的异常类型
public class ModifyUserInfoExceptin extends RuntimeException{
public ModifyUserInfoExceptin(){
}
public ModifyUserInfoExceptin(String message) {
super(message);
}
}
当前我们在系统的日志信息中看到 LoginExceptin
和 ModifyUserInfoExceptin
这俩种异常类型的 信息时,就知道是用户在登录和修改信息的时候,出现了问题。
5 断言 assert
断言( assert ),是JDK1.4的时候,增加的一个关键字。用它可以在程序中,确认一些关键性条件必 须是成立的,否则会抛出 AssertionError 类型的错误。(了解即可)
注意,断言( assert )并不是用来代替 if 判断的,而是确认系统中的一些关键性条件是必须成立 的,所以 assert 和 if 并不冲突,并且还可以通过给JVM传参数,来控制断言( assert )是否生效。
断言(assert)的使用方式:
assert 布尔表达式;
//或者
assert 布尔表达式 : "错误信息";
当布尔表达式为true时,断言通过,否则抛出 AssertionError 类型错误 所以,assert后面的布尔表达式必须true才行。(也就是说条件必须成立)
public class Test {
public static void main(String[] args) {
Test t = new Test();
t.test(0);
}
public void test(int a) {
assert a!=0 : "参数a的值不能为0";
int b = 10;
int c = b/a;
System.out.println(c);
}
}
final finally finalize区别
1.final 修饰符
修饰类:当用final修饰类的时,表明该类不能被其他类所继承。当我们需要让一个类永远不被继承,此时就可以用final修饰,但要注意:
final类中所有的成员方法都会隐式的定义为final方法。
、修饰方法:
不能被重写
、修饰变量:只能赋一次值;
2.finally语句块
finally作为异常处理的一部分,它只能用在try/catch语句中,并且附带一个语句块,表示这段语句最终一定会被执行(不管有没有抛出异常),经常被用在需要释放资源的情况下。
注意:在 try 语句块中执行了 System.exit (0) 语句,终止了 Java 虚拟机的运行,那finally语句块中的东西不会执行
3.finalize()
finalize()是在java.lang.Object里定义的,也就是说每一个对象都有这么个方法。这个方法在gc启动,该对象被回收的时候被调用
这个方法在gc启动,该对象被回收的时候被调用。
该方法用于**释放资源。**因为无法确定该方法什么时候被调用,很少使用。
Java允许在类中定义一个名为finalize()的方法。它的工作原理是:一旦垃圾回收器准备好释放对象占用的存储 空间,将首先调用其finalize()方法。并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。