七、异常处理
1. 异常概述与异常体系结构
1) 什么是异常
- 背景
在使用计算机语言进行项目开发的过程中,即使程序员把代码写得尽善尽美,在系统的运行过程中仍然会遇到一些异常问题,因为很多异常问题不是靠代码能够避免的,比如:客户输入数据的格式,读取文件是否存在,网络是否始终保持通畅等。 - 定义
在Java语言中,将程序执行中发生的不正常情况 称为“异常”。(开发过程中的语法错误和逻辑错误不是异常) - 异常处理的意义
异常处理相当于是针对某一未来可能发生的问题,提前给出一种异常处理预案,Plan B。
2) 异常分类
异常在Java中分为Error和Exception。
- Error(java.lang.XXXError)
Java虚拟机无法解决的严重问题。如:JVM系统内部错误、资源耗尽等严重情况。比如:java.lang.StackOverflowError(栈溢出)和java.lang.OutOfMemoryError(堆溢出)。一般不编写针对性的代码进行处理Error。 - Exception(java.lang.XXXException)
其它因编程错误或偶然的外在因素导致的一般性问题,可以使用针对性的代码进行处理。例如:空指针访问、试图读取不存在的文件、网络连接中断、数组角标越界
3) Exception的分类
根据java程序运行的流程(先编译后运行),可以将Exception分为编译时异常、运行时异常。
a) 编译时异常
- 是指编译器要求必须处置的异常。即程序在运行时由于外界因素造成的一般性异常。编译器要求Java程序必须捕获或声明所有编译时异常。
- 对于这类异常,如果程序不处理,则无法运行。
b) 运行时异常 - 是指编译器不要求强制处置的异常。一般是指运行时的逻辑错误,是程序员应该积极避免其出现的异常。java.lang.RuntimeException类及它的子类都是运行时异常。
- 对于这类异常,可以不作处理,因为这类异常很普遍,若全处理可能会对程序的可读性和运行效率产生影响。
2. 常见异常
1) 运行时异常
- NullPointerException(空指针异常)
@Test
public void test1(){
int[] arr = null;
System.out.println(arr.length); //空指针异常
String str = "abc";
str = null;
System.out.println(str.charAt(0)); //空指针异常
}
- ArrayIndexOutOfBoundsException(数组角标越界)
@Test
public void test2(){
int[] arr = new int[10];
System.out.println(arr[10]);
}
- StringIndexOutOfBoundsException
@Test
public void test1(){
String str = "abc";
System.out.println(str.charAt(3));
}
- ClassCastException(类型转换异常)
@Test
public void test3(){
Object obj = new Date();
String str = (String)obj;
//无法实现强制转换,报错:ClassCastException(类型转换异常)
}
- InputMissmatchException(输入不匹配异常)
@Test
public void test5(){
Scanner scanner = new Scanner(System.in);
int score = scanner.nextInt();
System.out.println(score);
//根据nextInt,输入int型时正常运行;
//当输入其他类型如字符串时,则报错:InputMissmatchException(输入不匹配异常)
}
- ArithmeticException(算数异常)
@Test
public void test6(){
int a = 10;
int b = 0;
System.out.println(a / b);
//被除数为0,不可以进行除法运算,报:ArithmeticException(算数异常)
//当代码不符合数学运算规则时,报ArithmeticException(算数异常)
}
- NumberFormatException(数字格式异常)
@Test
public void test4(){
String str = "123";
str = "abc";
int num = Integer.parseInt(str);
//当由其他类型转换成数字类型时,若不是数字,强行转换时,会数字格式异常
}
- InvalidCastException(序列化异常)
在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常。 - java.io.EOFException
在对象反序列化时,或者使用数据流(DataXxxStream)读取缓存数据时,如果反序列化或者读取的顺序与序列化或输出的顺序不同,会导致序列化或读入报错:java.io.EOFException
2) 编译时异常(开发软件写好代码后,直接报错,命令行执行javac直接报错)
- FileNotFoundException、IOException
@Test
public void test7(){
File file = new File("hello.txt");
FileInputStream fis = new FileInputStream(file); //报错FileNotFoundException
int data = fis.read(); //报错IOException
while(data != -1){ //从文件中读取字节,文件末尾为-1,当字节不是-1时,就没到文件末尾
System.out.print((char)data);
data = fis.read();
}
fis.close(); //报错 IOException
}
- ClassNotFoundException
@Test
public void test1() {
boolean b = true;
Class c = Class.forName("java.lang.String");//报错ClassNotFoundException
}
- UnsupportedEncodingException
public byte[] getBytes(String charsetName) throws UnsupportedEncodingException {
if (charsetName == null) throw new NullPointerException();
return StringCoding.encode(charsetName, value, 0, value.length);//报错UnsupportedEncodingException
}
3) 常见的Error
- StackOverflowError、OutOfMemoryError
public class ErrorTest {
public static void main(String[] args) {
//java.lang.StackOverflowError
//main(args);
//java.lang.OutOfMemoryError
byte[] arr = new byte[1024 * 1024 * 1024];
}
}
3. 异常处理机制之try-catch
- 异常处理背景
- 在编写程序时,经常要在可能出现错误的地方加上检测的代码,如进行x/y运算时,要检测分母为0,数据为空,输入的不是数据而是字符等。过多的if-else分支会导致程序的代码加长、臃肿,可读性差。因此采用异常处理机制。
- Java采用的异常处理机制,是将异常处理的程序代码集中在一起,与正常的程序代码分开,使得程序简洁、优雅,并易于维护。
- 异常处理方式——抓抛模型
- 过程一:“抛”
a. 程序在正常执行过程中,一旦出现异常,就会在异常代码处系统自动生成一个对应异常类的对象,并将此对象抛出。
b. 一旦抛出异常对象后,其后的代码就不再执行。
c. 在程序产生异常对象的根位置,程序自动抛出了异常。
d. 如果当前程序执行处无法处理该异常,可以通过throws+异常类型,再次抛出该异常对象,将该异常对象交给上层调用者处理。
e. 上面处理的异常对象是系统生成的异常对象,也可以通过throw手动生成异常对象。
f. 可以throw系统创建好的系统类型对象,也可以抛出自定义的异常对象。
throw new RuntimeException("message");//生成运行时异常对象
throw new Exception("message");//生成编译时异常对象
throw new MyException("message");//生成自定义异常对象
- 过程二:“抓”
可以理解为异常的处理方式:①try-catch-finally ②throws - try-catch-finally
- throws + 异常类型
3) 格式
try{
//可能出现异常的代码
}catch(异常类型1 变量名1){
//处理异常的方式1
}catch(异常类型2 变量名2){
//处理异常的方式2
}
……
finally{
//一定会执行的代码
}
4) try
- 发生异常的语句之后的语句不再执行。
- try结构中声明的变量只能在try中使用,在try结构之外不可以调用。
- 使用try将可能出现异常的代码包装起来,在执行过程中,一旦出现异常,就会生成一个对应异常类的对象,根据此对象的类型,去catch中进行匹配。
5) catch
- 当被某catch捕获到异常处理完成后,就跳出当前try-catch结构,不再执行其他的catch。(没有finally结构的情况下)。
- catch中的异常类型如果没有子父类关系,则谁声明在上,谁声明在下,无所谓。
- 如果存在子父类关系,则要求子类一定要声明在父类的上面,否在,报错(不可到达的语句)。
- 通过异常对象,输出异常信息:
①e.getMessage(),String返回值
②e.printStackTrace(),void返回值。
@Test
public void test3(){
String str1 = null;
try{
str1 = "atguigu.com";
str1 = null;
System.out.println(str1.charAt(0));
}catch(ClassCastException e){
e.printStackTrace();
}catch(NullPointerException e){
System.out.println(e.getMessage());
}catch(RuntimeException e){
e.printStackTrace();
}
System.out.println(str1);
}
6) finally
-
finally语句可选
-
finally中声明的是一定会被执行的代码,即使catch中又出现了异常,或try中有return语句,或catch中有return语句,最后也一定会指定finally中的语句。
-
类似数据库连接、输入输出流、网络编程Socket等JVM不能自动回收的资源,需要自己手动释放资源。此时就需要添加资源释放的代码,声明在finally中。
7) try-catch-finally -
执行完try-catch-finally结构后,如果异常被catch,继续执行后面的代码。如果异常没有被catch,则不会执行后面的代码。
-
使用try-catch-finally处理编译时异常,使得程序在编译时通过不报错,但不保证运行时不会报错。
-
try-catch-finally结构可以嵌套使用。
-
开发中,在编译时不知道运行时异常出错的位置,所以通常不针对运行时异常编写try-catch-finally,出错了就回来改代码;编译时异常,一定要处理,否则编译不通过。
8) 代码举例
@Test
public void test2(){
try{
String str1 = "atguigu.com";
str1 = null;
System.out.println(str1.charAt(0));
}catch(NullPointerException e){
//异常的处理方式1
System.out.println("不好意思,亲~出现了小问题,正在加紧解决...");
}catch(ClassCastException e){
//异常的处理方式2
System.out.println("出现了类型转换的异常");
}catch(RuntimeException e){
//异常的处理方式3
System.out.println("出现了运行时异常");
}
//此处的代码,在异常被处理了以后,是可以正常执行的
System.out.println("hello");
}
4. 异常处理机制之throws
- 格式
public void method1() throws FileNotFoundException, IOException{
File file = new File("hello.txt");
FileInputStream fis = new FileInputStream(file);
int data = fis.read();
while(data != -1){
System.out.print((char)data);
data = fis.read();
}
fis.close();
}
- 说明
1) "throws+异常类型"写在方法的声明处。指明此方法执行时,可能抛出异常类型。
2) 当方法体执行出现异常时,仍会在异常代码处生成一个异常类的对象。
3) 此时异常对象若满足throws后的指明异常类型时,就会被抛出,被该方法的调用者接受处理。
4) 异常代码后续的代码,就不再执行。
5) ①try-catch-finally真正的将异常处理掉。②throws只是将异常抛给了方法的调用者。 - 注意
1) 方法重写规则中要求:子类重写的方法抛出的异常类型不大于父类中被重写的方法抛出的异常类型。(因为编译看左边,所以编译时,JVM会按照父类中被重写方法抛出的异常类型来检查异常处理。如果子类重写方法抛出了比父类更大的异常类型,则方法调用者会处理不了该异常类型。)
class SuperClass{
public void method() throws IOException{
}
}
class SubClass{
public void method() throws IOException{
//此时子类重写的方法抛出的异常必须是父类方法抛出的异常的相同类型、子类、兄弟类型
}
}
2) 如果父类中被重写的方法没有抛异常,则子类重写的方法不允许抛出异常。
4. 选择try-catch-finally或throws
1) 如果父类中被重写的方法没有throws方式处理异常,则子类重写的方法也不能使用throws,则子类必须使用try-catch-finally处理异常。
2) 如果方法中有一定要执行的方法,例如资源的释放操作,则必须使用try-catch-finally
3) 某方法a()中,先后调用了其他几个方法b()、c()、d(),bcd是递进关系依次执行的。则建议这bcd这几个方法使用throws的方式处理异常。在方法a中统一通过try-catch-finally处理异常。
- 代码举例
public class ThrowsTest {
public static void main(String[] args) {
ThrowsTest test = new ThrowsTest();
test.method3();
}
public void method3(){
try {
method2();
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("可以执行的代码");
}
public void method2()throws IOException{
method1();
}
public void method1() throws FileNotFoundException,IOException{
File file = new File("hello.txt");
FileInputStream fis = new FileInputStream(file);//FileNotFoundException
int b = fis.read();//IOException
while(b != -1){
System.out.print((char)b);
b = fis.read();//IOException
}
fis.close();//IOException
}
}
5. 手动抛出异常:throw
- 异常对象的产生的方式:
1) 系统自动生成的异常对象
2) 手动生成一个异常对象(throw)
throw之后的代码不在执行,可以对比借鉴系统自动生成异常对象的过程理解
throw new RuntimeException("message");//生成运行时异常对象
throw new Exception("message");//生成编译时异常对象
throw new MyException("message");//生成自定义异常对象
- 代码举例
//写一个学生类,可以注册学号,
//要求学号大于0可以正常完成学号赋值注册
//小于0则不能正常完成学号赋值注册
public class ThrowTest{
public static void main(String[] args){
try{
Strudent s = new Student();
s.regist(-1001);
System.out.println(s);
}catch(Exception e){
System.out.println(e.getMessage());
}
}
}
class Student{
private int id;
public void regist(int id) throws Exception{
if(id > 0){
this.id = id;
}else{
//System.out.println("您输入的数据非法!");
//throw new RuntimeException("您输入的数据非法!"); //抛出运行常
throw new Exception("您输入的数据非法"); //抛出编译时异常
}
}
@Override
public void toString(){
System.out.println();
}
}
判断输出结果
public class ReturnExceptionDemo {
static void methodA() {
try {
System.out.println("进入方法A"); //①
throw new RuntimeException("制造异常"); //③
}finally {
System.out.println("用A方法的finally"); //②
}
}
static void methodB() {
try {
System.out.println("进入方法B"); //④
return;
} finally {
System.out.println("调用B方法的finally"); //⑤
}
}
public static void main(String[] args) {
try {
methodA();
} catch (Exception e) {
System.out.println(e.getMessage());
}
methodB();
}
}
/*
* 定义一个ComparableCircle类,继承Circle类并且实现CompareObject接口。
* 在ComparableCircle类中给出接口中方法compareTo的实现体,用来比较两个圆的半径大小。
*/
public class ComparableCircle extends Circle implements CompareObject {
public ComparableCircle() {
super();
}
public ComparableCircle(double radius) {
super(radius);
}
@Override
public int compareTo(Object o) {
if(o == this){
return 0;
}
if(o instanceof ComparableCircle){
ComparableCircle c = (ComparableCircle)o;
return Double.compare(this.getRadius(), c.getRadius());
}else{
//形参o不是一个ComparableCircle类型的对象
throw new RuntimeException("输入的类型不匹配");
}
}
}
6. 用户自定义异常
- 要求
1) 继承于现有的异常类
①RuntimeException
②Exception
2) 提供全局常量:serialVersionUID(唯一的标识这个异常类)
3) 提供构造器
①空参构造器
②String形参构造器,并调用父类String形参构造器 - 代码举例
public class MyException extends Exception{
static final long seralVersionUID = -2341341341341234L;
public MyException(){
}
public MyException(String msg){
super(msg);
}
}
九、 总结
- 自定义异常:
供自己用,建议使用运行时异常,不需要每一层调用者都捕获处理异常,代码方便;
供其他人用,建议使用编译时异常,编译时就会提示异常,每一层都必须捕获处理。
练习
- final、finally、finalize的区别?
- final是修饰类、方法、属性、局部变量的一个关键字。代表最终的。修饰类代表类不可被继承。修饰方法代表方法不可被重写。修饰属性和局部变量代表属性和局部变量不可被修改。
- finally是异常处理try-catch-finally的一个分支结构关键字。代表异常处理中一定会执行的部分。在异常处理中可根据实际需求写或不写。
- finalize是Object类中的一个方法,默认什么都不做。Object的子类可以重写这个方法,完成一定的逻辑。这个方法在JVM垃圾回收器回收某对象在堆中的空间时,由JVM调用。
- throw 和 throws的区别?
throw用于生成异常对象,而throws用于处理异常对象,只不过是将异常对象抛出去交给别人处理。
throw用于方法体内,而throws用于方法声明处
throw后面跟的是异常对象,而throws后面跟的是异常类型