异常分类

图示梳理

Exception.png

各类含义

  • Throwable:异常的基类,所有异常都继承自 java.lang.Throwable 类,Throwable 类有两个直接子类:Error 类和 Exception 类。
  • Error:是 Java 应用程序本身无法恢复的严重错误,应用程序不需要捕获、处理这些严重错误。通常情况下,程序员无需处理此类异常。
  • Exception:由 Java 应用程序抛出和处理的非严重错误(即异常),也是我们本章重点学习的对象。异常可分为运行时异常(RuntimeException)和检查时异常(CheckedException)两种。
  • RuntimeException:运行时异常,即程序运行时抛出的异常,程序员在编程时即使不做任何处理,程序也能通过编译。前面数组下标越界异常和除数为 0 的异常都是运行时异常。
  • CheckedException:检查时异常,又称为非运行时异常,这样的异常要求程序员必须在编程时进行处理,否则就会编译不通过。需要特别注意的是,在 JDK 的异常定义体系中(即在所有 Throwable 的子类中),并不存在真正的 CheckedException 类。也就是说,上图中的所有类名,都能在 JDK 中找到对应的 API,但唯独 CheckedException 类并不是真实存在的。一般而言,如果一个类继承自 RuntimeException,就称此类为运行时异常;反之,如果一个类没有继承 RuntimeException,但继承了 ExceptionThrowable,就称此类为检查时异常。

异常的两种处理方式

捕获异常并处理

先在一段可能抛出异常的代码外,用trycatch结构包裹起来。如果运行时发生了异常,那么此次异常就会进入catch代码块;如果一直没有异常发生,程序就不会进入catch代码块。并且catch可以针对抛出的不同类型的异常捕获后进行分类处理。

基本语法格式
try {
    //可能抛出异常的语句块
}catch(SomeException1 e) {    // SomeException1 特指某些异常,非 Java 中具体异常,下同
    //当捕获到 SomeException1 类型的异常时执行的语句块
} catch(SomeException2 e) {
    //当捕获到 SomeException2 类型的异常时执行的语句块
} finally {
    //无论是否发生异常都会执行的代码
}

还可以使用catch(Exception 基类)一次性捕获所有类型的异常

try{
    //可能抛出异常的语句块
} catch(Exception1 e) { // 一次性捕获所有类型的异常
    //当捕获到异常时执行的语句块
} finally {
    //无论是否发生异常都会执行的代码
}

例子

将孩子人数列出

public class exception {
        public static void main(String[] args){
            try{
                String teachers[] = {"柳海龙", "孙传杰", "孙悦"};
                for (int i = 0; i < 4; i++) {
                    System.out.println(teachers[i]);
                }
            }catch(Exception e){
                System.out.println("数组下标越界,请修改程序!");
            }
            System.out.println("显示完毕!");
        }

}

egexception.png

接下来进阶一下,了解孩子之后给孩子分苹果,将try...catch...插入其中进行捕获异常

import java.util.Scanner;

public class TestEx6 {
    public static void main(String[] args) {
        try {
            String teachers[] = {"柳海龙", "孙传杰", "孙悦"};
            for (int i = 0; i < teachers.length; i++) {
                System.out.println(teachers[i]);
            }
            int appleNum = 0; //苹果数
            int stuNum = 0; //学生数
            System.out.println("**_现在给孩子们分苹果_**");
            Scanner input = new Scanner(System.in);
            System.out.print("请输入桌子上有几个苹果:");
            appleNum = input.nextInt();
            System.out.print("请输入班上有几个孩子:");
            stuNum = input.nextInt();
            System.out.println("班上每个孩子分得多少苹果:" + appleNum / stuNum);
            System.out.println("孩子们非常开心!");
        } catch (Exception e) {
            System.out.println("数组下标越界,请修改程序!");
        }
        System.out.println("显示完毕!");
    }
}

egexception2.png

由图中运行的结果可知本应为数字0的算数异常,显示的确实数字下标异常,其原因为catch(Exception e)捕获了所有的异常,无论为啥异常都会抛出数字下标越界异常 。所以,不能因为快捷而一直使用catch(Exception)捕获异常,有时还需要对特殊情况进可能捕获不同类型的异常。

接下来我们对catch(Exception)进行一些修改

tyr{
    //.........省略
}catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("数组下标越界,请修改程序!");
   }catch (ArithmeticException e){
            System.out.println("算术异常");
      }

egexception3.png

出图中可以看到数字的算数异常现在不会被数组下标越界异常捕获

我们将代码完整性写出来,并加以修改

package Exception;
import javax.naming.AuthenticationException;
import java.util.Scanner;

public class exception2 {
    public static void main(String[] args) {
        try {
            String teachers[] = {"柳海龙", "孙传杰", "孙悦"};
            for (int i = 0; i < teachers.length; i++) {
                System.out.println(teachers[i]);
            }
            input();
            System.out.println("孩子们非常开心!");
        }catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("数组下标越界,请修改程序!");
            input();
         }catch (ArithmeticException e){
            System.out.println("算术异常");
            input();
          }catch(Exception e){
            System.out.println("其他异常");
           }
        System.out.println("显示完毕!");
    }
    //出入数据入口
    public static void input(){
        int appleNum = 0; //苹果数
        int stuNum = 0; //学生数
        System.out.println("**_现在给孩子们分苹果_**");
        Scanner input = new Scanner(System.in);
        System.out.print("请输入桌子上有几个苹果:");
        appleNum = input.nextInt();
        System.out.print("请输入班上有几个孩子:");
        stuNum = input.nextInt();
        System.out.println("班上每个孩子分得多少苹果:" + appleNum / stuNum);
    }
}

egexception4.png

当特定的实现异常完成后通常在后面在接上catch(Exception e)来排除其他异常

egexception5.png

效果见图所示,当输入的数值异常被捕获在递归调用input方法实现重新输入

抛出异常

通过 throws关键字,将异常抛给上一级处理。

抛出异常的规则:如数组下标越界异常 ArrayIndexOutOfBoundsException 是运行时异常 RuntimeException 的子类,而运行时异常 RuntimeException 又是 Exception 异常的子类,在捕获异常的时候,应该按照“从小到大”的顺序捕获异常,这样才能保证逐层捕获。将上面的代码调整为先捕获数组下标越界异常ArrayIndexOutOfBoundsException,再捕获运行时异常 RuntimeException,最后,为了防止遗漏对某个异常的处理,一般建议在最后用catch (Exception e) 捕获剩余的异常

基本语法格式
修饰符 返回值类型 方法名(参数) throws 异常类名1,异常类名2…{   }	

例子

import java.net.*; //导入 Java 网络包
import java.io.*; //导入 I/O 包

public class TestEx8 {
//声明服务器端套接字对象
public static ServerSocket ss = null;
    //暂不理会throws IOException代码的含义,之后的课程会详细介绍
    public static void main(String[] args) throws IOException {
        try {
            //实例化服务器端套接字,服务器端套接字等待请求通过网络传入
            ss = new ServerSocket(5678);    //其中5678为端口号
            //侦听并接受此套接字的连接
            Socket socket = ss.accept();
            //省略其他代码
        //当发生某种I/O异常时,抛出IOException异常
        } catch (IOException e) {
            //关闭此套接字
            ss.close();
            //省略其他代码
        }
        //省略其他代码
    }
}
finally

try 语句块中实例化出一个服务器端套接字对象并调用对象的方法,如果 try 语句块中出现 IOException 异常,则 catch 语句块进行捕获和处理,关闭这个服务器套接字,并执行其他操作。但如果程序没有抛出 IOException 异常,正常执行,则关闭服务器端套接字的代码将不会执行,这个套接字不会被关闭,而是继续占用系统资源,这并不是程序开发人员希望的,所以finally语句正是避免这种现象的存在。

使用 finally 语句块,保证无论是否发生异常,finally 语句块中的代码总被执行

package Exception;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

public class sockexception {
    public static ServerSocket ss = null;

    public static void main(String[] args) throws IOException {
        try {
            ss = new ServerSocket(5678);
            Socket socket = ss.accept();
            //省略其他代码
        } catch (IOException e) {
            //省略其他代码
        } finally {
            //关闭此套接字
            ss.close();
        }
        //省略其他代码
    }

}
思考?

如果在 try 语句块中或者 catch 语句块中存在 return 语句,finally 语句块中的代码还会执行吗?不是说 return 语句的作用是将结果返回给调用者,而不再执行 return 语句后面的代码吗?

答:Java 异常处理机制对这个问题的处理是,当 trycatch 语句块中有 return 语句时,先执行 trycatch 语句块中 return 语句之前的代码,再执行 finally 语句块中的代码,之后再返回。所以,即使在 trycatch 语句块中有 return 语句,finally 语句块中的代码仍然会被执行。在异常处理结构中,finally 语句块不执行的唯一一种情况就是如果在 catch 语句中出现 System.exit(),该方法表示关闭 JVM

package Exception;

import java.util.Scanner;

public class exception3 {
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        int no1 = 0;
        int no2 = 0;
        while (true){
            try {
                no1 = input.nextInt();
                no2 = input.nextInt();
                int num = no1 / no2;
            }catch (ArithmeticException e){
                System.out.println("算数错误");
            }catch (Exception e){
                System.out.println("出错");
                //return ;  finally语句还是能够执行
                //System.exit(1)  finally不会执行
            }finally {
                System.out.println("程序完成");
            }
        }
    }
}

将return注释消掉,如图所示

egfinally.png

将System.exit(1)注释消掉,如图所示

egfinally1.png

异常对象

在实际编程中,常用的异常对象的方法有两个:一个方法是 printStackTrace(),用于输出异常的堆栈信息,其中堆栈信息包括程序运行到当前类的执行流程,显示方法调用序列;另一个方法是 getMessage(),用于返回异常详细信息的字符串

下面看例子进行解释

package Exception;

public class exceptionObject {
    public static void main(String[] args) {
        String[] people = {"老王","李丽","小风","大兵"};
        try{
            for (int i = 0; i <= people.length; i++) {
                System.out.println(people[i]);
            }
        }catch(ArrayIndexOutOfBoundsException e){
            System.out.println("调用异常对象的getMessage()方法");
            System.out.println(e.getMessage());
            System.out.println("调用异常对象的 printStackTrace()方法");
            e.printStackTrace();
        }finally {
            System.out.println("排除完毕");
        }
    }
}

exceptionObject.png

程序捕获数组下标越界异常之后,先输出异常对象的 getMessage() 方法的结果,之后再调用异常对象的 printStackTrace() 方法输出堆栈信息

throw 和 throws 关键字

thow:

在 Java 语言中,可以使用 throw 关键字手工抛出一个异常,语法形式如下

//手工抛出一个算数异常的代码
throw new ArithmeticException();

简单例子

package Exception;

public class exception4 {
    public static void main(String[] args) {
        System.out.print("我");
        try {
            System.out.print("是");
            throw new NullPointerException("你");
        }catch (NullPointerException e){ //捕获抛出的异常
            System.out.print(e.getMessage());
        }
        System.out.println("朋友");
    }
}

exception5.png

catch 语句捕获到异常对象 e 并处理后,还可以在 catch 语句块中,继续使用 throw e; 语句再次将这个异常抛出

thows:

throws 用于声明一个方法会抛出异常,就是当方法本身不知道或者不愿意处理某个可能抛出的异常时,可以选择用 throws 关键字将该异常提交给调用该方法的方法进行处理

package Exception;

import java.util.Scanner;

public class exception5 {
    public static void main(String[] args) {
        try{
            arithmetic();
        }catch (ArithmeticException e){
            System.out.println("算数错误");
        }
    }

    //随便乱写一个方法
    public static void arithmetic() throws ArithmeticException{
        Scanner input = new Scanner(System.in);
        int num1 = 0,num2 = 0;
        num1 = input.nextInt();
        num2 = input.nextInt();
        System.out.println("除法结果" + (num1/num2));
    }
}

exception6.png

arithmetic() 方法声明该方法可能抛出 ArithmeticExceptio 异常,调用 arithmetic() 方法的 main() 方法处理了 arithmetic() 方法中不处理并声明抛出的这个异常

自定义异常类

自定义异常,顾名思义,就是程序员自己定义的异常,当 Java 类库中的异常不能满足程序需求时,程序员可以自己定义并使用异常。自定义异常可以任意的继承一个异常类。但 Exception 类是 Java 中所有异常类的父类,所以在定义自定义异常类时,通常继承自该类

//自定义异常类,处理年龄大于 120 或小于 0 的 Person
class AgeException extends Exception {
    private String message;

    public AgeException(int age)        //自定义异常类构造方法
    {
        message = "年龄设置为:" + age + "不合理!";
    }

    public String toString()            //自定义异常类toString()方法
    {
        return message;
    }
}

class Person {
    private int age;

    //声明setAge(int age)方法可能抛出AgeException自定义异常
    public void setAge(int age) throws AgeException {
        if (age <= 0 || age >= 120) {
            throw new AgeException(age);//抛出AgeException自定义异常
        } else {
            this.age = age;
        }
    }

    public int getAge() {
        return age;
    }

}

public class TestEx18 {
    public static void main(String[] args) {
        Person p1 = new Person();
        Person p2 = new Person();
        try {
            p1.setAge(150); //会抛出 AgeException 自定义异常
            System.out.println("正确输出年龄为:" + p1.getAge());
        } catch (AgeException e) { //进行异常捕获处理
            System.out.println(e.toString());
        }
        try {
            p2.setAge(60); //不会抛出 AgeException 自定义异常
            System.out.println("正确输出年龄为:" + p2.getAge());
        } catch (AgeException e1) {
            System.out.println(e1.toString());
        }
    }
}

exception7.png

异常类型

异常的类型Java中的异常分为两大类:

  1. Checked Exception(非Runtime Exception)
  2. Unchecked Exception(Runtime Exception)
名称类型
算数异常类ArithmeticExecption
空指针异常类型NullPointerException
类型强制转换类型ClassCastException
数组负下标异常NegativeArrayException
数组下标越界异常ArrayIndexOutOfBoundsException
违背安全原则异常SecturityException
文件已结束异常EOFException
文件未找到异常FileNotFoundException
字符串转换为数字异常NumberFormatException
操作数据库异常SQLException
输入输出异常IOException
方法未找到异常NoSuchMethodException
下标越界异常IndexOutOfBoundsExecption
系统异常SystemException
创建一个大小为负数的数组错误异常NegativeArraySizeException
数据格式异常NumberFormatException
安全异常SecurityException
网络操作在主线程异常NetworkOnMainThreadException
请求状态异常IllegalStateException
父类IllegalComponentStateException
网络请求异常HttpHostConnectException
子线程Thread更新UI view 异常ViewRootImpl$CalledFromWrongThreadException
证书不匹配的主机名异常SSLExceptionero
反射Method.invoke(obj, args...)方法抛出异常InvocationTargetException
EventBus使用异常EventBusException
非法参数异常IllegalArgumentException
参数不能小于0异常ZeroException

总结

异常处理机制,用于处理程序中可能发生的异常,主要涉及了以下知识点:

  1. 异常处理机制可以将程序中的业务代码和异常代码相分离,从而使得开发者们可以更加专注于编写业务代码,并使程序更加优雅。
  2. 异常的继承结构:异常的顶层基类是 Throwable;继承自 RuntimeException 的类称为运行时异常,否则称为检查异常。
  3. 运行时异常在编译阶段没有提示,只是在运行时存在异常才抛出;而检查异常是会在编译时就提示开发者必须处理的异常。
  4. 在异常可能发生的地方,通常有两种预防性处理策略:如果开发者认为此时出现的异常最好立即进行处理,那么就可以采用 try...catch 的方式;反之,如果开发者认为此时出现的异常,暂时可以不进行处理,就可以通过 throws 将异常抛出给上一级去处理。
  5. 异常对象常用方法有两个:一个方法是 printStackTrace(),用于输出异常的堆栈信息;另一个方法是 getMessage(),用于返回异常详细信息字符串。
  6. 异常的 5 个关键字:try 中编写可能存在异常的代码,catch 用于捕获异常并书写异常处理代码,finally 中的代码无论是否出现异常都会被执行(此前有 System.exit()除外),throws 用于声明方法可能会抛出的异常,而 throw 表示手工抛出异常即制造异常并抛出。
  7. 自定义异常:如果 JDK 中已有的异常不能满足开发需求,就需要开发者自定义异常。自定义异常需要继承 JDK 中已有的异常,并且通常会结合 if 语句一起使用。
  8. 常见的异常类型有 NullPointerExceptionClassNotFoundExceptionIllegalArgumentExceptionInputMismatchExceptionIllegalAccessExceptionClassCastExceptionSQLExceptionIOException 等,并且随着学习的深入,大家也会接触更多类型的异常。
  9. 如果某异常类继承自 RuntimeException,则该异常可以不显式的使用“try...catch...”或throws进行处理。

Exception8.png