Java基础学习——第七章 异常处理
一、异常概述与体系结构
1. 异常概述
- 异常:在Java开发中,将程序执行中发生的不正常情况称为“异常” (语法错误和逻辑错误不是异常)
 - Java程序在执行过程中发生的异常事件可分为两类: 
  
- Error:Java虚拟机无法解决的严重问题。 如: JVM系统内部错误、 资源耗尽等严重情况。 比如: StackOverflowError(栈溢出)和OOM(堆溢出)。 一般不编写针对性的代码进行处理
 - Exception:其它因编程错误或偶然的外在因素导致的一般性问题,可以使用针对性的代码进行处理。 例如:① 空指针访问;② 试图读取不存在的文件;③ 网络连接中断;④ 数组角标越界
 
 
2. 异常的体系结构
- 捕获错误最理想的是在编译期间, 但有的错误只有在运行时才会发生。比如: 除数为0, 数组下标越界等。异常(Exception)可以分为:编译时异常(红色)和==运行时异常==(蓝色) 
  
- 编译时异常:是指编译器要求必须处置的异常,即程序在运行时由于外界因素造成的一般性异常。 编译器要求程序必须捕获或声明所有编译时异常;对于这类异常,如果不处理,可能会带来意想不到的结果
 - 运行时异常:是指编译器不要求强制处置的异常。一般是指编程时的逻辑错误,是程序员应该积极避免其出现的异常。 java.lang.RuntimeException 类及它的子类都是运行时异常;对于这类异常,可以不作处理,因为这类异常很普遍,若全处理可能会对程序的可读性和运行效率产生影响
 
 

- java.lang.Throwable 
  
- java.lang.Error:一般不编写针对性的代码进行处理
 - java.lang.Exception:可以使用针对性的代码进行处理 
    
- 编译时异常(checked) 
      
- IOException 
        
- EOFException
 - FileNotFoundException
 - MalformedURLException
 - UnknownHostException
 
 - ClassNotFoundException
 - CloneNotSupportedException
 
 - IOException 
        
 - 运行时异常(unchecked Exception,RuntimeException) 
      
- RuntimeException 
        
- ArithmeticException
 - ClassCastException
 - IllegalArgumentException
 - IllegalStateException
 - IndexOutOfBoundsException
 - NoSuchElementException
 - NullPointerException
 - InputMisMatchException
 - NumberFormatException
 
 
 - RuntimeException 
        
 
 - 编译时异常(checked) 
      
 
 
二、常见异常
import org.junit.Test;
import java.io.*;
import java.util.*;
public class Day16_ExceptionTest {
    //**************************** 编译时异常 ****************************
    @Test
    public void test7() {
        File file =new File("hello.txt");
        FileInputStream fis = new FileInputStream(file); //FileNotFoundException
        int data = fis.read(); //IOException
        while (data != -1) {
            System.out.print((char)data);
            data = fis.read(); //IOException
        }
        fis.close(); //IOException
    }
    //**************************** 运行时异常 ****************************
    //NullPointerException:空指针异常
    @Test
    public void test1() {
        //数组空指针异常
        int[] arr = null;
        System.out.println(arr[3]); //NullPointerException
        //对象空指针异常
        String str = new String("abc");
        str = null;
        System.out.println(str.charAt(0)); //NullPointerException
    }
    //IndexOutOfBoundsException:角标越界异常
    @Test
    public void test2() {
        //数组角标越界
        int[] arr = new int[3];
        System.out.println(arr[3]); //ArrayIndexOutOfBoundsException
        //字符串角标越界
        String str = new String("abc");
        System.out.println(str.charAt(3)); //StringIndexOutOfBoundsException
    }
    //ClassCastException:类型转换异常
    @Test
    public void test3() {
        Object obj = new Date();
        String str = (String) obj; //ClassCastExceptions
    }
    //NumberFormatException:数字格式化异常
    @Test
    public void test4() {
        String str =  "123a";
        int num = Integer.parseInt(str); //NumberFormatException
    }
    //InputMisMatchException:输入不匹配异常
    @Test
    public void test5() {
        Scanner scan = new Scanner(System.in);
        System.out.print("请输入一个整数:");
        int score = scan.nextInt(); //如果输入的是aaa,InputMisMatchException
        System.out.println(score);
    }
    //ArithmeticException
    @Test
    public void test6() {
        int a = 10;
        int b = 0;
        System.out.println(a / b); //ArithmeticException
    }
}
 
三、异常的处理
- 在编写程序时,经常要在可能出现错误的地方加上检测代码,如进行x/y运算时,要检测分母为0,数据为空,输入的不是数据而是字符等。 过多的if-else分支会导致程序的代码臃肿,可读性差。因此采用异常处理机制
 - 异常处理机制是将异常处理的程序代码集中在一起,与正常的程序代码分开,使得程序简洁、优雅、易于维护
 
1. 异常的处理:抓抛模型
- 过程1 —— “抛”:程序在执行过程中,一旦出现异常,就会在异常代码处==生成一个对应异常类的对象==。并将此异常对象抛出。一旦抛出异常对象,其后的代码就不再执行 
  
- 异常类型对象的产生: 
    
- 在程序执行过程中出现异常时,由系统自动生成的异常对象,并抛出
 - 手动生成异常对象,并抛出(throw)
 
 
 - 异常类型对象的产生: 
    
 - 过程2 —— “抓”:可以理解为异常的处理方式:① try-catch-finally ② throws
 
2. 异常处理方式一:try-catch-finally
try {
...... //可能产生异常的代码
} catch(异常类型1 变量名1) {
...... //当产生异常类型1的对象时的处理措施
} catch(异常类型2 变量名2) {
...... //当产生异常类型2的对象时的处理措施
} catch(异常类型3 变量名3) {
...... //当产生异常类型3的对象时的处理措施
} finally{
...... //无论是否发生异常,一定会被执行的语句
}
 
- catch和finally是可选的,但至少包含其一:如果未声明任何一个catch代码块,则必须声明finally代码块,且finally代码块只能有一个;若未声明finally代码块,则必须声明至少一个catch代码块(可以有多个)
 - try-catch-finally结构可以嵌套使用
 - 异常类型对象的引用类型变量的变量名通常都设置为e
 
2.1 try-catch的使用
- 使用 try{…} 代码块选定捕获异常的范围,即将可能出现异常的代码放在 try{…} 代码块中。程序在执行过程中一旦出现异常,就会生成一个对应异常类的对象,根据异常对象的类型,去 catch 中进行匹配
 - 一旦 try{…} 中生成的异常对象匹配到某一个 catch 后,就会进入该catch代码块中对异常对象进行处理;一旦处理完成,就跳出当前的try-catch结构(在没有写 finally 的情况下),继续执行try-catch结构之后的代码
 - 不同catch的异常类型如果没有子父类关系,则无所谓声明的先后顺序;若存在子父类关系,则要求子类异常类型一定要声明在父类异常类型之前(范围小的在前),否则会报错
 - 常用的处理异常对象的方法:① getMessage() 获取异常信息,返回值类型为String;② printStackTrace() 获取异常类名和异常信息,以及异常出现在程序中的位置,返回值类型为void
 - 在 try{…} 代码块中声明的变量是局部变量,出了 try{…} 结构后,就不能再调用
 
import org.junit.Test;
import java.io.*;
public class Day16_ExceptionTest1 {
    //**************************** 编译时异常 ****************************
    @Test
    public void test7() {
        try {
            File file =new File("hello.txt");
            FileInputStream fis = new FileInputStream(file); //FileNotFoundException
            int data = fis.read(); //IOException
            while (data != -1) {
                System.out.print((char)data);
                data = fis.read(); //IOException
            }
            fis.close(); //IOException
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    //**************************** 运行时异常 ****************************
    @Test
    public void test1() {
        String str =  "123a";
        try {
            int num = Integer.parseInt(str); //NumberFormatException
            System.out.println("111"); //在try代码块中,一旦抛出异常对象,其后的代码就不再执行
        } catch (NullPointerException e) {
            System.out.println(e.getMessage());
        } catch (NumberFormatException e) {
            System.out.println(e.getMessage());
            e.printStackTrace();
        }
        //一旦处理完成,跳出当前的try-catch结构(没有finally的情况下),继续执行try-catch结构之后的代码
        System.out.println("222");
    }
}
 
2.2 finally的使用
- 捕获异常的最后一步是通过finally语句为异常处理提供一个统一的出口,使得在控制流转到程序的其它部分以前,能够对程序的状态作统一的管理
 - finally是可选的,根据实际情况选用
 - finally中声明的是一定会被执行的语句:不论try代码块中是否发生了异常,catch语句是否执行,catch语句是否有异常,try代码块中是否有return语句,catch语句中是否有return语句等情况,finally中的语句都会被执行
 - 像数据库连接、输入输出流、网络编程Socket等资源,JVM是不能自动垃圾回收的,需要手动的进行资源的释放。这些资源释放的操作,通常就需要声明在finally代码块中
 

import org.junit.Test;
import java.io.*;
public class Day16_FinallyTest {
	//**************************** 运行时异常 ****************************
    @Test
    public void test1() {
        try {
            int a = 10;
            int b = 0;
            System.out.println(a / b); //ArithmeticException
        } catch (ArithmeticException e) {
            e.printStackTrace();
            //catch语句有异常
            int[] arr = new int[10];
            System.out.println(arr[10]);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println("finally一定会被执行");
        }
    }
    @Test //@Test单元测试的方法一定要是void的
    public void test2() {
        int num = method();
        System.out.println(num);
    }
    public int method() {
        try {
            int[] arr = new int[10];
            System.out.println(arr[10]);
            return 1;
        } catch (ArrayIndexOutOfBoundsException e) {
            e.printStackTrace();
            //try代码块中有return语句,catch语句中有return语句
            return 2;
        } finally {
            System.out.println("finally一定会被执行");
            return 3; //若finally中有return语句,则最终的返回值由finally中的return决定
        }
    }
	
    //**************************** 编译时异常 ****************************
    @Test
    public void test3() {
        FileInputStream fis = null;
        try {
            File file = new File("hello.txt");
            fis = new FileInputStream(file); //FileNotFoundException
            int data = fis.read(); //IOException
            while (data != -1) {
                System.out.print((char) data);
                data = fis.read(); //IOException
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (fis != null) {
                    fis.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
 
3. 异常处理方式二:throws
- 如果一个方法中的语句执行时可能生成某种异常,但是并不能确定如何处理这种异常,则此方法应显式的声明抛出异常,表明该方法将不对这些异常进行处理,而由该方法的调用者负责处理
 - 在方法声明中用throws语句可以声明抛出异常的列表(可以包含多个异常类型,用逗号隔开),throws后面的异常类型可以是方法中产生的异常类型,也可以是它的父类:
 
public void readFile(String file) throws FileNotFoundException {
    ...
    // 读文件的操作可能产生FileNotFoundException类型的异常
    FileInputStream fis = new FileInputStream(file);
    ...
}
 
- 当方法体执行时,一旦出现异常,仍会在异常代码处生成一个异常类的对象,若此对象符合throws后声明的异常类型时,就会被抛出。异常代码后的代码,不再被执行
 
import java.io.*;
public class Day16_ExceptionTest2 {
    //main()方法理论上也可以把异常向上抛,但一般在main方法中使用try-catch-finally把异常处理掉
    public static void main(String[] args) {
        try {
            method2();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    //method2()往上抛,抛给调用method2()的main()方法
    public static void method2() throws IOException {
        method1();
    }
    //**************************** 编译时异常 ****************************
    //method1()往上抛,抛给调用method1()的method2()
    public static void method1() throws FileNotFoundException, IOException {
        File file = new File("hello.txt");
        FileInputStream fis = new FileInputStream(file); //FileNotFoundException
        int data = fis.read(); //IOException
        while (data != -1) {
            System.out.print((char) data);
            data = fis.read(); //IOException
        }
        fis.close(); //IOException
    }
}
 
4. 两种异常处理方式的体会
- 使用 try-catch-finally 处理编译时异常,使得程序在编译时不再报错,但在运行时仍有可能报错。相当于使用 try-catch-finally 能够将一个编译时可能出现的异常,延迟到运行时再出现
 - 开发中,由于运行时异常比较常见,我们通常不针对运行时异常编写try-catch-finally;但对于编译时异常,一定要考虑异常的处理
 - throws只是将异常抛给了方法的调用者,并没有处理异常;而try-catch-finally才是真正地把异常处理掉了
 
5. 重写方法异常抛出的规则
- 子类重写的方法声明的抛出的异常类型不大于父类被重写方法的异常类型
 
import java.io.FileNotFoundException;
import java.io.IOException;
public class Day16_OverrideTest {
    public static void main(String[] args) {
        Day16_OverrideTest test = new Day16_OverrideTest();
        //如果子类重写的方法抛出的异常类型大于父类被重写方法的异常类型,display()方法中的catch无法匹配
        test.display(new SubClass1()); //多态
    }
    public void display(SuperClass1 s) {
        try {
            s.method();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
class SuperClass1 {
    public void method() throws IOException {
    }
}
class SubClass1 extends SuperClass1 {
    //子类重写的方法抛出的异常类型不大于父类被重写方法的异常类型
    @Override
    public void method() throws FileNotFoundException {
    }
}
 
6. 开发中如何选择使用 try-catch-finally & throws
- 如果父类中被重写的方法没有使用throws抛出异常,则子类重写的方法也不能使用throws;此时,如果子类重写的方法存在异常,必须使用try-catch-finally的方式处理异常
 - 执行的方法 a 中,先后调用了其他几个方法,且这几个方法是呈递进关系的(即前者的输出是后者的输入),则通常对这几个方法使用throws抛给调用者 a,然后在执行的方法 a 中使用 try-catch-finally 来处理异常
 
四、手动抛出异常对象:throw
- 异常类型对象的产生: 
  
- 在程序执行过程中出现异常时,由系统自动生成的异常对象,并抛出
 - 手动生成异常对象,并抛出(throw)
 
 
public class Day16_ThrowTest {
    public static void main(String[] args) {
        try {
            Student7 s = new Student7();
            s.register(-1001);
            System.out.println(s);
        } catch (Exception e) {
            System.out.println(e.getMessage()); //你输入的数据非法!
        }
    }
}
class Student7 {
    private int id;
    public void register(int id) throws Exception {
        if (id > 0) {
            this.id = id;
        } else {
            //手动生成一个异常对象,由于这里的异常对象是Exception类,包含了编译时异常,需要显式的处理异常
            //异常类型的非空参构造器,输入的字符串赋给Message属性,可以通过异常对象调用getMessage()方法
            throw new Exception("你输入的数据非法!");
        }
    }
    @Override
    public String toString() {
        return "Student7{" +
                "id=" + id +
                '}';
    }
}
 
五、自定义异常类
如何自定义一个异常类:
- 自定义的异常类需要继承Java现有的异常类,一般为:RuntimeException 或 Exception 
  
- RuntimeException:运行时异常,一般不用显式的处理
 - Exception:包含运行时异常和编译时异常,必须要显式的处理(try-catch-finally & throws),否则无法编译
 
 - 提供全局常量(static final):serialVersionUID,相当于对自定义异常类的序列版本号
 - 提供重载的构造器
 
public class Day16_ThrowTest {
    public static void main(String[] args) {
        try {
            Student7 s = new Student7();
            s.register(-1001);
            System.out.println(s);
        } catch (Exception e) {
            System.out.println(e.getMessage()); //你输入的数据非法!
        }
    }
}
class Student7 {
    private int id;
    public void register(int id) throws Exception {
        if (id > 0) {
            this.id = id;
        } else {
            //手动抛出一个异常对象,由于自定义异常类继承的是Exception类,需要显式的处理异常
            throw new Day16_MyException("你输入的数据非法!");
        }
    }
    @Override
    public String toString() {
        return "Student7{" +
                "id=" + id +
                '}';
    }
}
//*************************** 自定义异常类 ***************************
//1.自定义的异常类需要继承Java现有的异常类,一般为:RuntimeException 或 Exception
class Day16_MyException extends Exception {
	//2.提供全局常量(static final):serialVersionUID
    static final long serialVersionUID = -7034897345745766939L;
    //3.提供重载的构造器
    public Day16_MyException() {
    }
    public Day16_MyException(String message) {
        super(message);
    }
}
 
六、异常处理练习题
例1 finally不管任何情况一定会执行
public class ReturnExceptionDemo {
    static void methodA() {
        try {
            System.out.println("进入方法A"); //1
            throw new RuntimeException("制造异常"); //手动抛异常
        } finally {
            System.out.println("用A方法的finally"); //2:finally不管任何情况一定会执行
        }
    }
    static void methodB() {
        try {
            System.out.println("进入方法B"); //4
            return;
        } finally {
            System.out.println("调用B方法的finally"); //5:finally不管任何情况一定会执行
        }
    }
    public static void main(String[] args) {
        try {
            methodA();
        } catch (Exception e) {
            System.out.println(e.getMessage()); //3:制造异常
        }
        methodB();
    }
}
 
例2 综合考察
编写应用程序EcmDef.java,接收命令行的两个参数,要求不能输入负数,计算两数相除。对数据类型不一致(NumberFormatException)、缺少命令行参(ArrayIndexOutOfBoundsException)、除0(ArithmeticException)及输入负数(EcDef 自定义的异常)进行异常处理
- 提示:
(1) 在主类(EcmDef)中定义异常方法(ecm)完成两数相除功能
(2) 在main()方法中使用异常处理语句进行异常处理
(3) 在程序中, 自定义对应输入负数的异常类(EcDef)
(4) 运行时接受参数 java EcmDef 20 10 //args[0]=“20” args[1]=“10”
(5) Interger类的static方法parseInt(String s)将s转换成对应的int值
如:int a=Interger.parseInt(“314”); //a=314; 
public class EcmDef {
    public static void main(String[] args) {
        try {
            //String转换成基本数据类型(包装类)
            int a = Integer.parseInt(args[0]);
            int b = Integer.parseInt(args[1]);
            System.out.println(ecm(a, b));
        } catch (NumberFormatException e) {
            System.out.println("数据类型不一致");
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("缺少命令行参数");
        } catch (ArithmeticException e) {
            System.out.println("除0");
        } catch (EcDef e) {
            System.out.println(e.getMessage());
        }
    }
    public static int ecm(int a, int b) throws EcDef{ //声明异常,抛给方法调用者,即main方法
        if (a < 0 || b < 0) {
            throw new EcDef("输入负数"); //手动抛异常
        }
        return a / b;
    }
}
//1.自定义的异常类需要继承Java现有的异常类,一般为:RuntimeException 或 Exception
class EcDef extends Exception {
    //2.提供全局常量(static final):serialVersionUID
    static final long serialVersionUID = -7034897345745766939L;
    //3.提供重载的构造器
    public EcDef() {
    }
    public EcDef(String message) {
        super(message);
    }
}
 
七、总结异常处理的5个关键字
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wE87U7AV-1648403099307)(F:\Java\JavaSE\笔记\总结异常处理的5个关键字.png)]
//声明异常,抛给方法调用者,即main方法
 if (a < 0 || b < 0) {
 throw new EcDef(“输入负数”); //手动抛异常
 }
 return a / b;
 }
 }
//1.自定义的异常类需要继承Java现有的异常类,一般为:RuntimeException 或 Exception
 class EcDef extends Exception {
//2.提供全局常量(static final):serialVersionUID
static final long serialVersionUID = -7034897345745766939L;
//3.提供重载的构造器
public EcDef() {
}
public EcDef(String message) {
    super(message);
}
 
}
## 七、总结异常处理的5个关键字











