JavaSE 进阶 - 第21章 异常
0、异常
-
什么是异常:程序执行过程中的不正常情况。
-
java中异常的作用:增强程序健壮性。
-
异常在java中以类的形式存在,每一个异常类都可以创建异常对象
public class Test {
public static void main(String[] args) {
// 通过“异常类”实例化“异常对象”
NumberFormatException nfe = new NumberFormatException("数字格式化异常!");
System.out.println(nfe);
//java.lang.NumberFormatException: 数字格式化异常!
NullPointerException npe = new NullPointerException("空指针异常发生了!");
System.out.println(npe);
//java.lang.NullPointerException: 空指针异常发生了!
}
}
1、UML
1.1 UML统一建模语言
一种图标式语言(画图的)
UML不是只有java中使用。只要是面向对象的编程语言,都有UML。
一般画UML图的都是软件架构师或者说是系统分析师。这些级别的人员使用的。
软件设计人员使用UML。
在UML图中可以描述类和类之间的关系,程序执行的流程,对象的状态等.
盖大楼和软件开发一样,一个道理。
盖楼之前,会先由建筑师画图纸。图纸上一个一个符号都是标准符号。
这个图纸画完,只要是搞建筑的都能看懂,因为这个图纸上标注的这些
符号都是一种“标准的语言”。
在java软件开发当中,软件分析师/设计师负责设计类,java软件开发人员
必须要能看懂。
1.2 异常的继承结构图
我们可以使用UML图来描述一下继承结构。
画UML图有很多工具,例如:Rational Rose(收费的)、starUML等....
Object
Object下有Throwable(可抛出的)
Throwable下有两个分支:Error(不可处理,直接退出JVM)和Exception(可处理的)
Exception下有两个分支:
① Exception的直接子类:编译时异常
(要求程序员在编写程序阶段必须预先对这些异常进行处理,
如果不处理编译器报错,因此得名编译时异常。)。
② RuntimeException:运行时异常。
(在编写程序阶段程序员可以预先处理,也可以不管,都行。)
2、java的异常处理机制
2.1 编译时异常和运行时异常,都是发生在运行阶段。编译阶段异常是不会发生的。编译时异常因为什么而得名?
因为编译时异常必须在编译(编写)阶段预先处理,如果不处理编译器报错,因此得名。
所有异常都是在运行阶段发生的。因为只有程序运行阶段才可以new对象。
因为异常的发生就是new异常对象。
2.2 编译时异常和运行时异常的区别?
编译时异常一般发生的概率比较高。
举个例子:
你看到外面下雨了,倾盆大雨的。
你出门之前会预料到:如果不打伞,我可能会生病(生病是一种异常)。
而且这个异常发生的概率很高,所以我们出门之前要拿一把伞。
“拿一把伞”就是对“生病异常”发生之前的一种处理方式。
对于一些发生概率较高的异常,需要在运行之前对其进行预处理。
运行时异常一般发生的概率比较低。
举个例子:
小明走在大街上,可能会被天上的飞机轮子砸到。
被飞机轮子砸到也算一种异常。
但是这种异常发生概率较低。
在出门之前你没必要提前对这种发生概率较低的异常进行预处理。
如果你预处理这种异常,你将活的很累。
假设你在出门之前,你把能够发生的异常都预先处理,你这个人会更加
的安全,但是你这个人活的很累。
假设java中没有对异常进行划分,没有分为:编译时异常和运行时异常,
所有的异常都需要在编写程序阶段对其进行预处理,将是怎样的效果呢?
首先,如果这样的话,程序肯定是绝对的安全的。
但是程序员编写程序太累,代码到处都是处理异常的代码。
2.3 编译时异常还有其他名字:
受检异常:CheckedException
受控异常
2.4 运行时异常还有其它名字:
未受检异常:UnCheckedException
非受控异常
2.5 再次强调:所有异常都是发生在运行阶段的!
3、异常处理的2种方式
-
第一种方式:在方法声明的位置上,使用throws关键字,抛给上一级。
谁调用我,我就抛给谁。抛给上一级。 -
第二种方式:使用try…catch语句进行异常的捕捉。
这件事发生了,谁也不知道,因为我给抓住了。
public class ExceptionTest05 {
// 第一种处理方式:在方法声明的位置上继续使用:throws,来完成异常的继续上抛。抛给调用者。
// 上抛类似于推卸责任。(继续把异常传递给调用者。)
/*
public static void main(String[] args) throws ClassNotFoundException {
doSome();
}
*/
// 第二种处理方式:try..catch进行捕捉。
// 捕捉等于把异常拦下了,异常真正的解决了。(调用者是不知道的。)
public static void main(String[] args) {
try {
doSome();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
public static void doSome() throws ClassNotFoundException{
System.out.println("doSome!!!!");
}
}
/*
doSome!!!!
*/
-
【对异常处理的理解】
举个例子:
我是某集团的一个销售员,因为我的失误,导致公司损失了1000元,
“损失1000元”这可以看做是一个异常发生了。我有两种处理方式,
第一种方式:我把这件事告诉我的领导 【异常上抛】
第二种方式:我自己掏腰包把这个钱补上【异常的捕捉】
张三 --> 李四 —> 王五 --> CEO -
【思考】:
异常发生之后,如果我选择了上抛,抛给了我的调用者,调用者需要
对这个异常继续处理,那么调用者处理这个异常 同样 有2种处理方式。
- 注意1:
Java中异常发生之后如果一直上抛,最终抛给了main方法,main方法继续向上抛,
抛给了调用者JVM,JVM知道这个异常发生,只有一个结果——终止java程序的执行。
所以说,一般不建议在main方法上使用throws,因为这个异常如果真正的发生了,一定会抛给JVM。JVM只有终止。
一般main方法中的异常建议使用try…catch进行捕捉。main就不要继续上抛了。
-
注意2:
只要异常没有捕捉,采用上报的方式,此方法的后续代码不会执行。
另外需要注意,try语句块中的某一行出现异常,该行后面的代码不会执行。
try…catch捕捉异常之后,后续代码可以执行。 -
在以后的开发中,处理编译时异常,应该上报还是捕捉呢,怎么选?
如果希望调用者来处理,选择throws上报。
其它情况使用捕捉的方式。 -
深入try…catch
1、catch后面的小括号中的类型可以是具体的异常类型,也可以是该异常类型的父类型。
2、catch可以写多个。建议catch的时候,精确的一个一个处理。这样有利于程序的调试。
3、catch写多个的时候,从上到下,必须遵守从小到大。
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class ExceptionTest07 {
public static void main(String[] args) {
// 1、编译报错(这里应该是FileNotFoundException,而不是NullPointerException)
/*try {
FileInputStream fis = new FileInputStream("F:/00测试文件夹/测试/测试素材/测试.txt");
} catch(NullPointerException e) {
}*/
// 2、可以
/*try {
FileInputStream fis = new FileInputStream("F:/00测试文件夹/测试/测试素材/测试.txt");
System.out.println("以上出现异常,这里无法执行!");
} catch(FileNotFoundException e) {
System.out.println("文件不存在!");
}
System.out.println("这里可以执行");*/
// 3、可以
/*try {
FileInputStream fis = new FileInputStream("F:/00测试文件夹/测试/测试素材/测试.txt");
} catch(IOException e) { // 多态:IOException e = new FileNotFoundException();
System.out.println("文件不存在!");
}*/
// 4、可以
/*try {
FileInputStream fis = new FileInputStream("F:/00测试文件夹/测试/测试素材/测试.txt");
} catch(Exception e) { // 多态:Exception e = new FileNotFoundException();
System.out.println("文件不存在!");
}*/
// 5、可以
/*try {
//创建输入流
FileInputStream fis = new FileInputStream("F:/00测试文件夹/测试/测试素材/测试.txt");
//读文件
fis.read();
} catch(Exception e) { //所有的异常都走这个分支。
System.out.println("文件不存在!");
}*/
// 6、可以
/*try {
//创建输入流
FileInputStream fis = new FileInputStream("F:/00测试文件夹/测试/测试素材/测试.txt");
//读文件
fis.read();
} catch(FileNotFoundException e) {
System.out.println("文件不存在!");
} catch(IOException e){
System.out.println("读文件报错了!");
}*/
// 7、可以
// JDK8的新特性!
try {
//创建输入流
FileInputStream fis = new FileInputStream("F:/00测试文件夹/测试/测试素材/测试.txt");
// 进行数学运算
System.out.println(100 / 0); // 这个异常是运行时异常,编写程序时可以处理,也可以不处理。
} catch(FileNotFoundException | ArithmeticException | NullPointerException e) {
System.out.println("文件不存在?数学异常?空指针异常?都有可能!");
}
}
}
4、异常对象的2个重要的方法
-
获取异常简单的描述信息:String msg = exception.getMessage();
-
打印异常追踪的堆栈信息(常用这个方法):exception.printStackTrace();
public class ExceptionTest08 {
public static void main(String[] args) {
// 这里只是为了测试getMessage()方法和printStackTrace()方法。
// 这里只是new了异常对象,但是没有将异常对象抛出。JVM会认为这是一个普通的java对象。
NullPointerException e = new NullPointerException("空指针异常fdsafdsafdsafds");
// 获取异常简单描述信息:这个信息实际上就是构造方法上面String参数。
String msg = e.getMessage(); //空指针异常fdsafdsafdsafds
System.out.println(msg);
// 打印异常堆栈信息
// java后台打印异常堆栈追踪信息的时候,采用了异步线程的方式打印的。
e.printStackTrace();
for(int i = 0; i < 1000; i++){
System.out.println("i = " + i);
}
System.out.println("Hello World!");
}
}
/*
空指针异常fdsafdsafdsafds
java.lang.NullPointerException: 空指针异常fdsafdsafdsafds
at ExceptionTest08.main(ExceptionTest08.java:5)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:567)
at com.intellij.rt.execution.application.AppMainV2.main(AppMainV2.java:131)
i = 0
i = 1
i = 2
i = 3
。。。
。。。
i = 997
i = 998
i = 999
Hello World!
*/
5、怎么查看异常的追踪信息,快速的调试程序呢?
- 异常信息追踪信息,从上往下一行一行看。
- 但是需要注意的是:SUN写的代码就不用看了(看包名就知道是自己的还是SUN的。)。
- 主要的问题是出现在自己编写的代码上。
6、关于try…catch中的finally子句
- 在finally子句中的代码是最后执行的,并且是一定会执行的,即使try语句块中的代码出现了异常。
【注意】只有System.exit(0),退出JVM之后,finally语句中的代码才不会执行
try {
System.out.println("try...");
// 退出JVM
System.exit(0);
} finally {
System.out.println("finally..."); //这里不会执行
}
- finally子句必须和try一起出现,不能单独编写。
public class ExceptionTest11 {
public static void main(String[] args) {
/*
try和finally,没有catch可以吗?可以。
try不能单独使用。
try finally可以联合使用。
以下代码的执行顺序:
先执行try...
再执行finally...
最后执行 return (return语句只要执行方法必然结束。)
*/
try {
System.out.println("try...");
return;
} finally {
// finally中的语句会执行。能执行到。
System.out.println("finally...");
}
// 编译器提醒错误:Unreachable statement(无法到达的语句)
// 这里不能写语句,因为这个代码是无法执行到的。————————在try里面有return;(return语句只要执行方法必然结束。)
//System.out.println("Hello World!");
}
}
- finally语句通常使用在哪些情况下呢?
通常在finally语句块中完成资源的释放/关闭。
因为finally中的代码比较有保障。
即使try语句块中的代码出现异常,finally中代码也会正常执行。
7、【finally面试题】
/*
finally面试题
*/
public class ExceptionTest13 {
public static void main(String[] args) {
int result = m();
System.out.println(result); //100
}
/*
java语法规则(有一些规则是不能破坏的,一旦这么说了,就必须这么做!):
java中有一条这样的规则:
方法体中的代码必须遵循自上而下顺序依次逐行执行(亘古不变的语法!)
java中还有一条语法规则:
return语句一旦执行,整个方法必须结束(亘古不变的语法!)
*/
public static int m(){
int i = 100;
try {
// 这行代码出现在int i = 100;的下面,所以最终结果必须是返回100
// return语句还必须保证是最后执行的。一旦执行,整个方法结束。
return i;
} finally {
i++;
}
}
}
/*
反编译之后的效果
public static int m(){
int i = 100;
int j = i;
i++;
return j;
}
*/
8、【面试题】:final 、finally 、finalize的区别
-
final 关键字
final修饰的类无法继承
final修饰的方法无法覆盖
final修饰的变量不能重新赋值。 -
finally 关键字
和try一起联合使用。
finally语句块中的代码是必须执行的。 -
finalize 标识符
是一个Object类中的方法名:finalize()
这个方法是由垃圾回收器GC负责调用的。
(在第18章讲过)
9、自定义异常
-
SUN提供的JDK内置的异常肯定是不够的用的。在实际的开发中,有很多业务,
这些业务出现异常之后,JDK中都是没有的。和业务挂钩的。
那么异常类我们程序员可以自己定义吗?
可以。 -
Java中怎么自定义异常呢?
第一步:编写一个类继承Exception或者RuntimeException.
第二步:提供两个构造方法,一个无参数的,一个带有String参数的。
public class MyException extends Exception{ // 编译时异常
public MyException(){
}
public MyException(String s){
super(s);
}
}
/*
public class MyException extends RuntimeException{ // 运行时异常
}
*/
public class ExceptionTest15 {
public static void main(String[] args) {
// 创建异常对象(只new了异常对象,并没有手动抛出)
MyException e = new MyException("用户名不能为空!");
// 打印异常堆栈信息
e.printStackTrace();
// 获取异常简单描述信息
String msg = e.getMessage();
System.out.println(msg);
}
}
/*
MyException: 用户名不能为空!
at ExceptionTest15.main(ExceptionTest15.java:5)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:567)
at com.intellij.rt.execution.application.AppMainV2.main(AppMainV2.java:131)
用户名不能为空!
*/
10、【终极案例】自定义异常在实际开发中的应用
/**
* 栈操作异常:自定义异常!
*/
public class MyStackOperationException extends Exception{ // 编译时异常!
public MyStackOperationException(){
}
public MyStackOperationException(String s){
super(s);
}
}
/*
编写程序,使用一维数组,模拟栈数据结构。
要求:
1、这个栈可以存储java中的任何引用类型的数据。
2、在栈中提供push方法模拟压栈。(栈满了,要有提示信息。)
3、在栈中提供pop方法模拟弹栈。(栈空了,也有有提示信息。)
4、编写测试程序,new栈对象,调用push pop方法来模拟压栈弹栈的动作。
5、假设栈的默认初始化容量是10.(请注意无参数构造方法的编写方式。)
*/
public class MyStack {
// 向栈当中存储元素,我们这里使用一维数组模拟。存到栈中,就表示存储到数组中。
// 因为数组是我们学习java的第一个容器。
// 为什么选择Object类型数组?因为这个栈可以存储java中的任何引用类型的数据
// new Animal()对象可以放进去,new Person()对象也可以放进去。因为Animal和Person的超级父类就是Object。
// 包括String也可以存储进去。因为String父类也是Object。
private Object[] elements;
// 栈帧,永远指向栈顶部元素
// 那么这个默认初始值应该是多少。注意:最初的栈是空的,一个元素都没有。
//private int index = 0; // 如果index采用0,表示栈帧指向了顶部元素的上方。
//private int index = -1; // 如果index采用-1,表示栈帧指向了顶部元素。
private int index;
/**
* 无参数构造方法。默认初始化栈容量10.
*/
public MyStack() {
// 一维数组动态初始化
// 默认初始化容量是10.
this.elements = new Object[10];
// 给index初始化
this.index = -1;
}
/**
* 压栈的方法
* @param obj 被压入的元素
*/
public void push(Object obj) throws MyStackOperationException {
if(index >= elements.length - 1){
// 创建异常对象
//MyStackOperationException e = new MyStackOperationException("压栈失败,栈已满!");
// 手动将异常抛出去!
//throw e; //这里捕捉没有意义,自己new一个异常,自己捉,没有意义。栈已满这个信息你需要传递出去。
// 合并(手动抛出异常!)
throw new MyStackOperationException("压栈失败,栈已满!");
}
// 程序能够走到这里,说明栈没满
// 向栈中加1个元素,栈帧向上移动一个位置。
index++;
elements[index] = obj;
// 在声明一次:所有的System.out.println()方法执行时,如果输出引用的话,自动调用引用的toString()方法。
System.out.println("压栈" + obj + "元素成功,栈帧指向" + index);
}
/**
* 弹栈的方法,从数组中往外取元素。每取出一个元素,栈帧向下移动一位。
* @return
*/
public void pop() throws MyStackOperationException {
if(index < 0){
throw new MyStackOperationException("弹栈失败,栈已空!");
}
// 程序能够执行到此处说明栈没有空。
System.out.print("弹栈" + elements[index] + "元素成功,");
// 栈帧向下移动一位。
index--;
System.out.println("栈帧指向" + index);
}
public Object[] getElements() {
return elements;
}
public void setElements(Object[] elements) {
this.elements = elements;
}
public int getIndex() {
return index;
}
public void setIndex(int index) {
this.index = index;
}
}
// 注意:最后这个例子,是异常最终要的案例。必须掌握。自定义异常在实际开发中的应用。
public class ExceptionTest16 {
public static void main(String[] args) {
// 创建栈对象
MyStack stack = new MyStack();
// 压栈
try {
stack.push(new Object());
stack.push(new Object());
stack.push(new Object());
stack.push(new Object());
stack.push(new Object());
stack.push(new Object());
stack.push(new Object());
stack.push(new Object());
stack.push(new Object());
stack.push(new Object());
// 这里栈满了
stack.push(new Object());
} catch (MyStackOperationException e) {
// 输出异常的简单信息。
System.out.println(e.getMessage());
}
// 弹栈
try {
stack.pop();
stack.pop();
stack.pop();
stack.pop();
stack.pop();
stack.pop();
stack.pop();
stack.pop();
stack.pop();
stack.pop();
// 弹栈失败
stack.pop();
} catch (MyStackOperationException e) {
System.out.println(e.getMessage());
}
}
}
/*
压栈java.lang.Object@119d7047元素成功,栈帧指向0
压栈java.lang.Object@776ec8df元素成功,栈帧指向1
压栈java.lang.Object@4eec7777元素成功,栈帧指向2
压栈java.lang.Object@3b07d329元素成功,栈帧指向3
压栈java.lang.Object@41629346元素成功,栈帧指向4
压栈java.lang.Object@404b9385元素成功,栈帧指向5
压栈java.lang.Object@6d311334元素成功,栈帧指向6
压栈java.lang.Object@682a0b20元素成功,栈帧指向7
压栈java.lang.Object@3d075dc0元素成功,栈帧指向8
压栈java.lang.Object@214c265e元素成功,栈帧指向9
压栈失败,栈已满!
弹栈java.lang.Object@214c265e元素成功,栈帧指向8
弹栈java.lang.Object@3d075dc0元素成功,栈帧指向7
弹栈java.lang.Object@682a0b20元素成功,栈帧指向6
弹栈java.lang.Object@6d311334元素成功,栈帧指向5
弹栈java.lang.Object@404b9385元素成功,栈帧指向4
弹栈java.lang.Object@41629346元素成功,栈帧指向3
弹栈java.lang.Object@3b07d329元素成功,栈帧指向2
弹栈java.lang.Object@4eec7777元素成功,栈帧指向1
弹栈java.lang.Object@776ec8df元素成功,栈帧指向0
弹栈java.lang.Object@119d7047元素成功,栈帧指向-1
弹栈失败,栈已空!
*/
11、之前在讲解方法覆盖的时候,当时遗留了一个问题:
- 重写之后的方法不能比重写之前的方法抛出更多(更宽泛)的异常,可以更少。
12、异常的 5 个关键字
- try、
- catch、
- finally、
- throws 在方法声明位置上使用,表示上报异常信息给调用者。
- throw 手动抛出异常!