目录
1、什么是异常,java提供异常处理机制有什么用?
什么是异常:程序执行过程中的不正常情况。
异常的作用:增强程序的健壮性。(观察到异常后对程序进行修改增加健壮性)
以下程序执行过程中发生了不正常的情况,而这种不正常的情况叫做:异常
java语言是很完善的语言,提供了异常的处理方式,以下程序执行过程中出现了不正常情况,
java把该异常信息打印输出到控制台,供程序员参考。程序员看到异常信息之后,可以对程序进行修改,让程序更加的健壮。
该异常信息时jvm创建、打印的
观察后修改
2、java语言中异常存在形式
异常在java中以类的形式存在,每一个异常类都可以创建异常对象
3、异常的继承结构
3.1 异常结构图
Object
Object下有Throwable(可抛出的)
Throwable下有两个分支:Error(不可处理,直接退出JVM)和Exception(可处理的)
Exception下有两个分支:
Exception的直接子类:编译时异常(要求程序员在编写程序阶段必须预先对这些异常进行处理,如果不处理编译器报错,因此得名编译时异常。)。
如:ClassNotFoundException编译时异常
(方法抛出异常交给调用者处理,调用者没有处理,编译器报错)
RuntimeException:运行时异常。(在编写程序阶段程序员可以预先处理,也可以不管,都行。)
异常机构图:
3.2 异常发生时机
编译时异常和运行时异常,都是发生在运行阶段。编译阶段异常是不会发生的。
再次强调:所有异常都是发生在运行阶段的
编译时异常因为什么而得名?
因为编译时异常必须在编译(编写)阶段预先处理,如果不处理编译器报错,因此得名
所有异常都是在运行阶段发生的。因为只有程序运行阶段才可以new对象;因为异常的发生就是new异常对象
3.3 编译时异常与运行时异常区别
编译时异常一般发生的概率比较高
(对于一些发生概率较高的异常,需要在运行之前对其进行预处理)
运行时异常一般发生的概率比较低
(没必要提前对这种发生概率较低的异常进行预处理)
假设java中没有对异常进行划分,没有分为:编译时异常和运行时异常,所有的异常都需要在编写
程序阶段对其进行预处理,首先,如果这样的话,程序肯定是绝对的安全的。但是程序员编写程序
太累,代码到处都是处理异常的代码。
4、异常处理的两种方式
第一种方式:在方法声明的位置上,使用throws关键字,抛给上一级。可以抛出多个异常,用逗号隔开
谁调用我,我就抛给谁。抛给上一级。
第二种方式:使用try..catch语句进行异常的捕捉
这件事发生了,谁也不知道,因为我给抓住了
异常发生之后,如果我选择了上抛,抛给了我的调用者,调用者需要对这个异常继续处理,那么调用者处理这个异常同样有两种处理方式
例子:
继续抛
try...catch(自己处理,调用者不知道)
注意:一般不在main方法上throws;因为这个异常如果真正的发生了,一定会抛给JVM。JVM只有终止异常处理机制的作用就是增强程序的健壮性。怎么能做到,异常发生了也不影响程序的执行。所以一般main方法中的异常建议使用try...catch进行捕捉。main就不要继续上抛了。
注意:Java中异常发生之后如果一直上抛,最终抛给了main方法,main方法继续向上抛,抛给了调用者JVM,JVM知道这个异常发生,只有一个结果。终止java程序的执行
例如:
这里的HelloWorld没有输出,没有执行
程序执行到此处发生了ArithmeticException异常,底层new了一个ArithmeticException异常对象,
然后抛出了,由于是main方法调用了100 / 0,所以这个异常ArithmeticException抛给了main方法,main方法没有处理,将这个异常自动抛给了JVM。JVM最终终止程序的执行。
ArithmeticException 继承 RuntimeException,属于运行时异常。在编写程序阶段不需要对这种异常进行预先的处理
5、出现异常后代码哪里执行哪里不执行
(1)对于出异常的方法这里:
首先当对象调用方法(构造方法也是方法)出现异常后,该方法(底层)会new异常对象(一般是编译时异常),并且throw抛出,再用throws抛给调用者;注意只要throw了这个方法中断执行
(2)对出调用者这里:
出现异常后,只要异常没有捕捉,采用上报(抛出)的方式,此方法后面代码不会执行。
另外需要意,try语句块中的某一行出现异常,该行后面的代码不会执行。try..catch捕捉异常之后,后续代码可以执行
(new FileInputStream("");这里路径错误,底层new了异常对象)
6、try...catch语法
(1)catch后面的小括号中的类型可以是具体的异常类型,也可以是该异常类型的父类型
(2)catch可以写多个,也可以写一个大的概括所有。建议catch的时候,精确的一个一个处理。这样有利于程序的调试
(3)catch写多个的时候,从上到下,必须遵守从小到大
(4)JDK8的新特性,一个catch里面可以写多个异常类型,用"|"隔开
7、throws和try...catch的选择
如果希望调用者来处理,选择throws上报。其它情况使用捕捉的方式
8、异常对象的常用方法
(1)exception.getMessage();获取异常简单的描述信息
获取异常简单描述信息:这个信息实际上就是构造方法上面String参数
(2)exception.printStackTrace();打印异常追踪的堆栈信息
java后台打印异常堆栈追踪信息的时候,采用了异步线程的方式打印的
9、异常追踪信息查看技巧
异常信息追踪信息,从上往下一行一行看。
但是需要注意的是:SUN写的代码就不用看了(看包名就知道是自己的还是SUN的。)。主要的问题是出现在自己编写的代码上
10、关于try..catch中的finally子句
(1)在finally子句中的代码是最后执行的,并且是一定会执行的,即使try语句块中的代码出现了异常。没有catch捉住也执行、try里面有return也能执行;(除非System.exit(0);退出jvm不执行)。
finally子句必须和try一起出现,不能单独编写
退出jvm不在执行
(2)通常在finally语句块中完成资源的释放/关闭,因为finally中的代码比较有保障,即使try语句块中的代码出现异常,finally中代码也会正常执行;不放在finally中当代码出现异常就会无法关闭资源
如:
try里出现异常,导致无法关闭流
放在finally里面关闭
(3)try和finally,没有catch也可以吗
try不能单独使用。
try finally可以联合使用
(4)try中由return时执行顺序(return一定是最后执行的)
先执行try...
再执行finally...
最后执行 return (return语句只要执行方法必然结束。)
11、finally执行顺序的一个面试题
按照10.(4)的执行顺序result不是应该时101吗?为什么时100
答:因为
java语法规则(有一些规则是不能破坏的,一旦这么说了,就必须这么做!):
java中有一条这样的规则:
方法体中的代码必须遵循自上而下顺序依次逐行执行(亘古不变的语法!)
java中还有一条语法规则:
return语句一旦执行,整个方法必须结束(亘古不变的语法!)
10.(4)规则与上面的规则产生矛盾;sun团队执行了下面的编译方法,解决这些方法的矛盾
找了一个中间变量;使结果达到从代码上看使先return的效果;而又执行了finally里面的代码
反编译之后的效果
public static int m(){
int i = 100;
int j = i;
i++;
return j;
}
12、final finally finalize 的区别
final 关键字:
final修饰的类无法继承
final修饰的方法无法覆盖
final修饰的变量不能重新赋值
finally 关键字:
和try一起联合使用
finally语句块中的代码是必须执行的
finalize 标识符:
是一个Object类中的方法名
这个方法是由垃圾回收器GC负责调用的
13、自定义异常
13.1 自定义异常方法
SUN提供的JDK内置的异常肯定是不够的用的。在实际的开发中,有很多业务,这些业务出现异常之后,JDK中都是没有的。和业务挂钩的。那么异常类需要我们程序员自己定义
两步:
第一步:编写一个类继承Exception或者RuntimeException
第二步:提供两个构造方法,一个无参数的,一个带有String参数的
异常的定义:
异常的使用:
13.2 自定义异常在开发中的应用
作用:
使用手动抛异常终止程序并让调用者得到异常信息(异常throw后用throws处理)
用法:
throw new 自定义的异常,throws抛给调用者
常用在登录信息提示那块或者其他需要判断的地方
例1:
自定义异常:
业务类:
test:
例2:
原先的一个代码:数组模拟栈
package com.bjpowernode.javase.array.homework;
/*
编写程序,使用一维数组,模拟栈数据结构。
要求:
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){
if(index >= elements.length - 1){
System.out.println("压栈失败,栈已满!");
return;
}
// 程序能够走到这里,说明栈没满
// 向栈中加1个元素,栈帧向上移动一个位置。
index++;
elements[index] = obj;
// 在声明一次:所有的System.out.println()方法执行时,如果输出引用的话,自动调用引用的toString()方法。
System.out.println("压栈" + obj + "元素成功,栈帧指向" + index);
}
/**
* 弹栈的方法,从数组中往外取元素。每取出一个元素,栈帧向下移动一位。
* @return
*/
public void pop(){
if(index < 0){
System.out.println("弹栈失败,栈已空!");
return;
}
// 程序能够执行到此处说明栈没有空。
System.out.print("弹栈" + elements[index] + "元素成功,");
// 栈帧向下移动一位。
index--;
System.out.println("栈帧指向" + index);
}
// set和get也许用不上,但是你必须写上,这是规矩。你使用IDEA生成就行了。
// 封装:第一步:属性私有化,第二步:对外提供set和get方法。
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;
}
}
用手动抛异常代替
System.out.println("压栈失败,栈已满!"); return;
定义异常
改写MyStack
测试(调用)
结果
14、关于方法覆盖中子类不能抛出比父类更多的异常
重写之后的方法不能比重写之前的方法抛出更多(更宽泛)的异常,可以更少
原因:
在使用多态的时候,父类引用指向子类,进而父类引用使用子类的覆盖方法时由于父类没的异常而子类有,就会导致子类方法的异常跟着程跑也没有进行throws或try...catch处理