Java基础八--异常

异常字面翻译就是“意外、例外”的意思,也就是说非正常情况。

在程序运行过程中,意外发生的情况,背离我们程序本身的意图的表现,都可以理解为异常

Java 中的异常本质上是程序中的一些错误,但并不是所有的错误都是异常,并且错误有时候是可以避免的。

利用 Java 中的异常处理机制,我们可以更好地提升程序的健壮性

要理解 Java 异常处理是如何工作的,需要掌握以下三种类型的异常:

  • 检查型异常:最具代表的检查性异常时用户错误或问题引起的异常,这是我们无法预见的。例如要打开一个不存在的文件,一个异常就发生了,这些异常在编译时不能被简单的忽略。
  • 运行时异常:运行时异常时可能被程序员避免的异常。与检查性异常相反,运行时异常可以在编译时被忽略
  • 错误:错误不是异常,而是脱离我们控制的问题。错误在代码中通常被忽略。例如,当栈溢出时,一个错误就发生了,他们在编译也检查不到。

异常的分类

在程序开发中,异常指不期而至的各种状况。他是一个事件,当发生在程序运行期间时,会干扰正常的指令流程。

在 Java 中,通过 Throwable 及其子类描述各种不同的异常类型

Throwable及其子类

Throwable 有两个重要的子类:Exception 和 Error


Error

Error 是程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而代表代码运行时 JVM 出现的问题

例如,Java 虚拟机运行错误,当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError

  • 这些错误是不可查的,因为它们在应用程序的控制和处理能力之外,而且绝大多数是程序运行时不允许出现的状况。

  • 对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。

  • 因此我们编写程序时不需要关心这类异常

Exception

Exception 是程序本身可以处理的异常。异常处理通常指针对这种类型异常的处理。

Exception 类的异常包括 unchecked exception(非检查型异常) 和 checked exception(检查型异常)

unchecked exception

  • unchecked exception:编译器不要求强制处置的异常。

  • 包含 RunntimeException 类及其子类异常

  • 如 NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)等,这些异常时unchecked exception。

  • Java 编译器不会检查这些异常,在程序中可以选择捕获处理,也可以不处理,照样正常编译通过

checked exception

  • checked exception:编译器要求必须处置的异常

  • 是 RunntimeException 类及其子类以外,其他的 Exception 类的子类

  • 如 IOException、SQLException 等
  • Java 编译器会检查这些异常,当程序中可能出现这类异常时,要求必须进行异常处理,否则编译不会通过

异常处理

在 Java 应用程序中,异常处理机制为:抛出异常、捕获异常

抛出异常

  • 当一个方法出现错误引发异常时,方法创建异常对象并交付运行时系统
  • 异常对象中包含了异常类型和异常出现时的程序状态等异常信息
  • 运行时系统负责寻找处置异常的代码并执行

捕获异常

  • 在方法抛出异常之后,运行时系统将转为寻找合适的异常处理器
  • 运行时系统从发生异常的方法开始,依次回查调用栈中的方法,当异常处理器所能处理的异常类型与方法抛出的异常类型相符时,即为合适的异常处理器
  • 当运行时系统遍历调用栈而未找到合适的异常处理器,则运行时系统终止。同时,意味着 Java 程序的终止
  • 对于运行时异常、错误或可查异常,Java 技术所要求的的异常处理方式有所不同
  • 总体来说,Java 规定:对于可查异常必捕获、或者声明抛出。允许忽略不可查的 RuntimeException 和 Error
  • 简单的来说,异常总是先被抛出,后被捕获的

异常处理

通过5个关键字来实现:try、catch、finally、throw、throws

异常处理

try-catch-finally

1
2
3
4
5
6
7
8
9
10
public void method(){
try{
//代码段1
//产生异常的代码段2
}catch(异常类型 e){
//对异常进行处理的代码段3
}finally{
//代码段4
}
}

try-catch

  • 使用 try-catch 块捕获并处理异常
1
2
3
4
5
6
7
8
public void method(){
try{
//代码段
}catch(异常类型 e){
//对异常进行处理的代码段
}
//代码段
}
  • 使用 try-catch 块捕获并处理异常——无异常
1
2
3
4
5
6
7
8
public void method(){
try{
//代码段(此处不会产生异常)
}catch(异常类型 e){
//对异常进行处理的代码段
}
//代码段
}

try-catch无异常

  • 使用 try-catch 块捕获并处理异常——有异常并能正常匹配处理
1
2
3
4
5
6
7
8
9
10
public void method(){
try{
//代码段1
//产生异常的代码段2
//代码段3
}catch(异常类型 e){
//对异常进行处理的代码段4
}
//代码段5
}

try-catch有异常且能处理

  • 使用 try-catch 块捕获并处理异常——有异常不能正常匹配处理
1
2
3
4
5
6
7
8
9
10
public void method(){
try{
//代码段1
//产生异常的代码段2
//代码段3
}catch(异常类型 e){
//对异常进行处理的代码段4
}
//代码段5
}

try-catch有异常但不能处理

多重 catch 块

  • 一旦某个 catch 捕获到匹配的异常类型,将进入异常处理代码。一经处理结束,就意味着整个 tr-catch 语句结束。其他的 catch 子句不再有匹配和捕获异常类型的机会。
  • 对于有多个 catch 子句的异常程序而言,应该尽量将捕获底层异常类的 catch 子句放在前面,同时尽量将捕获相对高层的异常类的 catch 子句放在后面。否则,捕获底层异常类 catch 子句将可能会被屏蔽。
  • 引发多种类型的异常

    • 排列 catch 语句的顺序:先子类后父类

    • 发生异常时按顺序逐个匹配

      多重catch块发生异常时按顺序逐个匹配

    • 只执行第一个与异常类型匹配的 catch 语句

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      public void method(){
      try{
      //代码段
      //产生异常(类型2)
      }catch(异常类型1 e){
      //对异常处理的代码
      }catch(异常类型2 e){
      //对异常处理的代码
      }catch(异常类型3 e){
      //对异常处理的代码
      }
      //代码段
      }

try-ctach-finally

  • try 块后可以接零个或者多个 catch 块

  • 如果没有 catch,则必须跟一个 finally 块

  • catch、finally可选

    • 语法组合:
      • try-catch
      • try-finally
      • try-catch-finally
      • try-catch-catch-finally
  • 在 try-catch 块后加入 finally 块

    • 是否发生异常都执行

    • 不执行的唯一情况

      finally不执行的情况

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      public class Main{
      public static void main(String args[]) {
      System.out.println("普通代码");
      try {
      System.out.println("try");
      throw new Exception();
      }catch (Exception e){
      System.out.println("catch");
      System.exit(1);
      }finally {
      System.out.println("finally");
      }
      }
      }

      运行结果:

      普通代码
      try
      catch

    • 一旦在 try 块或者 catch 块中加入 System.exit(1) 这个语句 finally 语句块将会强制终止执行

      其实还要一种情况,当 try 块没有执行的话,finally 也不会被执行,也就是说,当一个方法在 try 块之前就返回了,那么他的 finally 块就不会被执行。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      public class Main{
      public static void main(String args[]) {
      String str= method();
      System.out.println(str);
      }

      public static String method(){
      int a = 3;
      if (a<4){
      return "finally 没有执行";
      }
      System.out.println("普通代码");
      try {
      System.out.println("try");
      throw new Exception();
      }catch (Exception e){
      System.out.println("catch");
      System.exit(1);
      }finally {
      System.out.println("finally");
      }
      return "finally 执行了";
      }
      }

      运行结果:

      finally 没有执行

  • try-catch-finally的执行情况

    try-catch-finally的执行流程


实际应用中的经验与总结

  • 处理运行时异常时,采用逻辑去合理规避同时辅助 try-catch 处理
  • 在多重 catch 块后面,可以加上 catch(Exception e)来处理可能会被遗漏的异常
  • 对于不确定的代码,也可以加上 try-catch,处理潜在的异常
  • 尽量去处理异常,切记只是简单地调用 printStackTrace() 去打印输出
  • 具体如何处理异常,要根据不同的业务需求和异常类型去决定
  • 尽量添加 finally 语句块去释放占用的资源
  • 不执行 finally 块有两种方式
    • try 语句没有被执行到,如在 try 语句之前 return 就返回了,这样 finally 语句就不会执行。这也说明了 finally 语句被执行的必要而非充分条件是:相应的 try 语句一定被执行到
    • 在 try 块 catch 块中有 System.exit(0); 这样的语句。System.exit(0) 是终止 Java 虚拟机 JVM 的,连 JVM 都停止了,所有都结束了,当然 finally 语句也不会被执行到。
  • 在 try-catch-finally 中, 当 return 遇到 finally,return 对 finally 无效
    • 在try catch块里return的时候,finally也会被执行
    • finally 里的 return 语句会把 try catch 块里的 return 语句效果给覆盖掉

throw&throws

可以通过 throws 声明将要抛出何种类型的异常,通过 throw将产生的异常抛出

throws

  • 如果一个方法可能会出现异常,但没有能力处理这种异常,可以在方法声明处用 throws 子句来声明抛出异常

  • 例如:汽车在运行时可能会出现故障,汽车本身没有办法处理这个故障,那就让开车的人来处理

  • throws 语句用在方法定义时声明该方法要抛出的异常类型

    1
    2
    3
    public void method() throws Exception1,Exception2,...,ExceptionN{
    //可能产生异常的代码
    }
  • 当方法抛出异常列表中的异常时,方法不对这些类型及其子类类型的异常作处理,而抛向调用该方法的方法,由他去处理

throws的使用规则

  • 如果是不可检查异常(unchecked exception),即 Error 、RuntimeException 或他们的子类,那么可以不适用 throws 关键字来声明要抛出的异常,编译仍能顺利通过,但在运行时会被系统抛出
  • 如果一个方法中可能出现可查异常,要么用 try-catch 语句捕获,要么用 throws 子句声明将它抛出,否则会导致编译错误
  • 当抛出了异常,则该方法的调用者必须处理或者重新抛出该异常
  • 当子类重写父类抛出异常的方法时,声明的异常必须是父类方法所声明异常的同类或子类

throw

  • throw 用来抛出一个异常

    例如:throw new IOException();

  • trhow 抛出的只能是可抛出类 Throwable 或其子类的实例对象

    例如:throw new String(“出错啦!”);//是错误的

1
2
3
4
5
6
7
8
public void method(){
try{
//代码段1
throw new 异常类型();
}catch(异常类型 e){
//对异常进行处理的代码段2
}
}
1
2
3
4
public void method() throw 异常类型{
//代码段1
throw new 异常类型();
}

自定义异常

  • 使用 Java 内置的异常类可以描述在编程时出现的大部分异常情况
  • 也可以通过自定义异常描述特定业务产生的异常类型
  • 所谓自定义异常,就是定义一个类,去继承 Throwable 类或者它的子类
  • 如果希望写一个检查性异常类,则需要继承 Exception 类
  • 如果你想写一个运行时异常类,那么需要继承 RuntimeException 类

异常链

  • 有时候我们会捕捉一个异常后再抛出另一个异常

  • 顾名思义就是:将异常发生的原因一个传一个穿起来,即把底层的异常信息传给上层,这样逐层抛出


Java 常见异常类型及原因分析

常见的异常类型

异常类型 说明
Exception 异常层次结构的父类
ArithmeticException 算术错误情形,如以零作除数
ArrayIndexOutOfBoundsException 数组下标越界
NullPointerException 尝试访问 null 对象成员
ClassNotFoundException 不能加载所需的类
IllegalArgumentException 方法接受到非法参数
ClassCastException 对象强制类型转换出错
NumberFormatException 数字格式转换异常,如把“abc”转换为数字

NullPointerException 异常

顾名思义,空指针异常,这可能是最常遇见的异常了。但是在 Java 中没有指针,怎么会有空指针异常呢?

在 C++ 中,声明的指针需要指向一个实例(通过 new 方法构造),这个指针可以理解为地址。在 Java 中,虽然没有指针,但是有引用(通常称为对象引用,一般直接说对象),引用也是要指向一个实例对象(通过 new 方法构造)的,从某种意义上说,Java 中的引用与 C++ 中的指针没有本质区别,不同的是,出于安全的目的,在 Java 中不能对引用进行操作,而在 C++ 中可以直接进行指针的运算。

所以这里的 NullPointerException 虽然不是真正的空指针异常,但本质上差不多,是因为引用没有指向具体的实例,所以当访问这个引用的时候就会产生这种异常。例如:

1
2
3
4
5
6
public class Main{
public static void main(String args[]) {
String str = "这是一个测试字符串";
System.out.println(str.length());
}
}

运行结果:

9

这段代码是没有问题的,但是如果改成下面的代码:

1
2
3
4
5
6
public class Main{
public static void main(String args[]) {
String str = null;
System.out.println(str.length());
}
}

运行结果:

Exception in thread “main” java.lang.NullPointerException
​ at Main.main(Main.java:4)

这就产生了 NullPointerException 异常了

这种异常是如何产生的呢

  • 把调用某个方法的返回值直接赋值给某个引用,然后调用这个引用的方法。在这种情况下,如果返回的值是Null,必然会产生 NullPointerException 异常。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public class People {
    private String Name;
    private int age;

    public String getName() {
    return Name;
    }

    public void setName(String name) {
    Name = name;
    }

    public int getAge() {
    return age;
    }

    public void setAge(int age) {
    this.age = age;
    }
    }
    1
    2
    3
    4
    5
    6
    7
    public class Main{
    public static void main(String args[]) {
    People p = null;
    p.setName("张三");
    System.out.println(p.getName());
    }
    }

    分析:声明一个 People对象,并打印出该对象的 Name 值

    说明:这个时候你的 p 就出现了空指针异常,因为你只是声明了 People 类型的对象,并没有创建对象,所以它的堆里面没有地址引用,切记你要用对象调用方法的时候,一定要先创建对象。

  • 在方法体中调用参数的方法

    如果调用方法的时候传递进来的值是 null,也要产生 NullPointerException 异常。

    要避免程序产生这种异常,比较好的解决方法是在调用某个对象的方法时候判断这个对象是否可能为空,如果可能,则增加判断语句,例如上面的代码就可以写为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class Main{
    public static void main(String args[]) {
    People p = null;
    if (p == null) {
    p = new People();
    p.setName("张三");
    }else {
    p.setName("张三");
    }
    System.out.println(p.getName());
    }
    }

ClassCastException 异常

从字面上看,是类型转换异常,通常是进行强制类型转换时候出现的错误。下面对产生 ClassCastException 异常的原因进行分析,然后给出这种异常的解决方法

这种异常是如何产生的呢

我们来看下面这段代码

这里 Animal 表示动物类,Dog 类表示狗类,Cat 类表示猫类,Dog 和 Cat 是 Animal 类的子类。

1
2
3
4
5
6
7
8
public class Main{
public static void main(String args[]) {
Animal a1 = new Dog();
Animal a2 = new Cat();
Dog d1 = (Dog) a1;
Dog d2 = (Dog) a2;
}
}

运行结果

Exception in thread “main” java.lang.ClassCastException: Cat cannot be cast to Dog
​ at Main.main(Main.java:6)

第5行和第6行代码基本相同,从字面意思上来看都是把 Animal 强制转换为 Dog,但是第六行代码产生了 ClassCastException 异常,说 Cat cannot be cast to Dog 。

这下就很好理解了,我在 Java 基础七-多态里的向下转型中有说到,因为 a1 本身就是 Dog 对象,所以它理所当然的可以转型为 Dog,a2 本身是 Cat 对象,所以它也理所当然的不能转为 Dog 。猫狗之间能互换吗?

从上面的例子可以看出,ClassCastException 是进行强制类型转换的时候产生的异常。

强制类型转换的前提是父类引用指向的对象的类型是子类的时候才可以进行强制类型转换,如果父类引用指向的对象类型不是子类的时候就会产生 ClassCastException 异常。

遇到这个异常该怎么办呢?如果你知道要访问的对象的具体类型,直接转换成该类型即可。如果不能确定类型可以通过下面的两种方式进行处理(假设对象为 o ):

  • 通过o.getClass().getName()>得到具体的类型,可以通过输出语句输出这个类型,然后根据类型进行具体的处理
  • 通过if(o instanceof 类型)的语句来判断 o 的类型是什么

ArrayIndexOutOfBoundsException 异常

这是一个非常常见的异常,从名字上看是数组下标越界错误,解决这个异常的方法就是查看为什么数组下标越界。

下面有一个错误实例。

1
2
3
4
5
6
7
8
public class Main{
public static void main(String args[]) {
int[] a = {1,2,3,4,5};
for (int i = 0; i < 6; i++) {
System.out.print(a[i] + " ");
}
}
}

运行结果

1 2 3 4 5 Exception in thread “main” java.lang.ArrayIndexOutOfBoundsException: 5
​ at Main.main(Main.java:5)

1.我们可以看到错误在第5行

2.发生错误的时候,下标的值是5

接下来,我们分析为什么下标值是5的会出错就可以了。

NumberFormatException 异常

数字转换异常,在把一个表示数字的字符串转换成数字类型的时候会后可能会报这个异常,原因是作为参数的字符串不是由数字组成的。

1
2
3
4
5
6
7
8
public class Main{
public static void main(String args[]) {
String num1 = "123";
String num2 = "123 ";
System.out.println(Integer.parseInt(num1));
System.out.println(Integer.parseInt(num2));
}
}

运行结果

123
Exception in thread “main” java.lang.NumberFormatException: For input string: “123 “
​ at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
​ at java.lang.Integer.parseInt(Integer.java:580)
​ at java.lang.Integer.parseInt(Integer.java:615)
​ at Main.main(Main.java:6)

这是因为 num2 后面有一个空格。

堆栈溢出和内存溢出

在递归调用的时候可能会产生堆栈溢出的情况,因为递归调用的时候需要把调用的状态保存起来,如果递归的深度达到一定程度,将产生堆栈溢出的异常。

如果虚拟机内存比较小,而程序对内存要求比较高,则可能产生内存溢出错误

-------------本文结束感谢您的阅读-------------

本文标题:Java基础八--异常

文章作者:Cui Zhe

发布时间:2018年10月25日 - 11:10

最后更新:2018年10月26日 - 17:10

原始链接:https://cuizhe1023.github.io/2018/10/25/Java基础八/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。