Java关键字之try、catch、finally

2020-06-27  本文已影响0人  l1fe1

1 前言

这三个关键字常用于捕捉异常的一整套流程,try 用来确定需要捕获异常的代码的执行范围,catch 捕捉可能会发生的异常,finally 用来执行一定要执行的代码块。除此之外,我们还需要清楚,每个语句块如果发生异常会怎么办,让我们来看下面这个例子:

public class TryCatchFinallyDemo {
    private static Logger log = Logger.getLogger("TryCatchFinallyDemo");

    public static void testCatchFinally() {
        try {
            log.info("try is run");
            if (true) {
                throw new RuntimeException("try exception");
            }
        } catch (Exception e) {
            log.info("catch is run");
            if (true) {
                throw new RuntimeException("catch exception");
            }
        } finally {
            log.info("finally is run");
        }
    }

    public static void main(String[] args) {
        testCatchFinally();
    }
}

这个代码演示了在 try、catch 中都遇到了异常的情况,从输出结果可以看出来:代码的执行顺序为:try -> catch -> finally。

六月 27, 2020 2:30:04 下午 com.l1fe1.exception.TryCatchFinallyDemo testCatchFinally
信息: try is run
六月 27, 2020 2:30:04 下午 com.l1fe1.exception.TryCatchFinallyDemo testCatchFinally
信息: catch is run
六月 27, 2020 2:30:04 下午 com.l1fe1.exception.TryCatchFinallyDemo testCatchFinally
信息: finally is run
Exception in thread "main" java.lang.RuntimeException: catch exception
    at com.l1fe1.exception.TryCatchFinallyDemo.testCatchFinally(TryCatchFinallyDemo.java:17)
    at com.l1fe1.exception.TryCatchFinallyDemo.main(TryCatchFinallyDemo.java:25)

此外,我们还可以看出两点:

  1. finally 先执行后,再抛出 catch 的异常;
  2. 最终捕获的异常是 catch 的异常,try 抛出来的异常已经被 catch 吃掉了,所以当我们遇见 catch 也有可能会抛出异常时,我们可以先打印出 try 的异常,这样 try 的异常在日志中就会有所体现。

2. try语句

在 try 关键字之后紧跟着的 Block 被称为 try 语句 的 try 块。
在 finally 关键字之后紧跟着的 Block 被称为 try 语句 的 finally 块。
try 语句可以有 catch 子句,这些子句也被称为异常处理器。catch 子句有且只有一个参数,这个参数被称为异常参数。异常参数可以将它的类型表示成单一的类类型(uni-catch子句),也可以表示成两个或者更多类类型(multi-catch子句)的联合体(这些类型称为可选择项)。联合体中的可选择项在语法上用 | 隔开。
用来表示异常参数的每个类类型都必须是 Throwable 类 或 Throwable 的子类,否则就会产生编译错误。
如果类型变量被用来表示异常参数的类型,会产生编译错误。
如果类型联合体包含两个可选项Di和Dj(i ≠ j),其中Di是Dj的子类型,那么就会产生编译错误。
用单个类类型表示的异常参数的声明类型就是该类类型。
用可选项D1 | D2 | ... | Dn表示为联合体的异常参数的声明类型是lub(D1,D2,...,Dn)。
multi-catch子句的异常参数如果没有被显式声明为 final,那么就会被隐式声明为 final。如果显式或隐式声明为 final 的异常参数在 catch 子句体内被赋值,那么就会产生编译错误。
uni-catch子句的异常参数从来都不会被隐式声明为 final,但是它可以被显式声明为 final 或是有效的 final。

隐式 final 的异常参数是因其声明的特性而是 final 的,然后有效的 final 的异常参数是因其被使用方式的特性而是 final 的。multi-catch子句的异常参数隐式的声明为 final,因此永远不会作为赋值操作的左操作数而出现,但是它不会被认为是有效的 final。
如果uni-catch子句的异常参数被显式声明为 final 的,那么移除 final 修饰符会引入编译时错误。这是因为这样的异常参数尽管仍旧是有效的 final,但是再也不能被像 catch 子句体中声明的匿名类和局部类这样的类引用了。另一方面,如果没有任何编译时错误,那么可以在将来变更程序,使得异常参数被重新赋值,这时它就不再是有效的 final 了。

异常处理器会按照从左到右的顺序被考虑是否合适:最靠前的可以接受异常的 catch 子句将被抛出的异常对象当作其引元而接收。
multi-catch 子句可以被看作是uni-catch 子句序列。即异常参数类型表示为联合体D1 | D2 | ... | Dn的catch子句等价于 n 个异常类型分别是D1,D2,...,Dn的 catch 子句序列。在这 n 个 catch 子句的每个 Block 中,异常参数的声明类型都是lub(D1,D2,...,Dn)。例如,下面的代码:

try {
   ... throws ReflectiveOperationException ...
}
catch (ClassNotFoundException | IllegalAccessException ex) {
 // ... body ...
}

在语义上等价于下面的代码:

try {
  ... throws ReflectiveOperationException ...
}
catch (final ClassNotFoundException ex1) {
 final ReflectiveOperationException ex = ex1;
 // ... body ...
}
catch (final IllegalAccessException ex2) {
 final ReflectiveOperationException ex = ex2;
 // ... body ...
}

其中,具有两个可选项的 multi-catch 子句已经被转译成两个分离的 catch 子句,每个对应一个选项。Java 编译器既不要求也不推荐以这种方式通过重复代码来编译 multi-catch 子句,因为在 class 文件中无需重复就可以表示 multi-catch 子句。

2.1 try-catch 的执行

不带 finally 块的 try 语句是由先执行 try 块而开始的。然后有以下选择:

class BlewIt extends Exception {
    BlewIt() { }
    BlewIt(String s) { super(s); }
}
public class TestTryCatch {
    static void blowUp() throws BlewIt { throw new BlewIt(); }
    public static void main(String[] args) {
        try {
            blowUp();
        } catch (RuntimeException r) {
            System.out.println("Caught RuntimeException");
        } catch (BlewIt b) {
            System.out.println("Caught BlewIt");
        }
    }
}

在这里,BlewIt 异常是 blowUp 方法抛出的。在 main 方法体中的 try-catch 语句有两个 catch 子句。异常的运行时类型是 BlewIt,它对 RuntimeException 类型的变量是不可赋值的,但是它对 BlewIt 类型的变量是可赋值的,因此这个示例的输出为:

Caught BlewIt

2.2 try-finally 和 try-catch-finally 的执行

带 finally 块的 try 语句也是由先执行 try 块而开始的。然后有以下选择:

public class TestTryCatchFinally {
    static void blowUp() throws BlewIt {
        throw new NullPointerException();
    }
    public static void main(String[] args) {
        try {
            blowUp();
        } catch (BlewIt b) {
            System.out.println("Caught BlewIt");
        } finally {
            System.out.println("Uncaught Exception");
        }
    }
}

这个程序会产生以下输出:

Uncaught Exception
Exception in thread "main" java.lang.NullPointerException
    at com.l1fe1.exception.TestTryCatchFinally.blowUp(TestTryCatchFinally.java:5)
    at com.l1fe1.exception.TestTryCatchFinally.main(TestTryCatchFinally.java:9)

blowup 方法抛出的 NullPointerException(RuntimeException的一种)没有被 main 中的任何 catch 语句捕获,因为 NullPointerException 对象对于 BlewIt 类型的变量来说,不是赋值兼容的。finally 子句会被执行,之后执行 main 的线程,也就是该测试程序的唯一线程,将会因为未捕获的异常而终止,这通常会导致打印异常名和简单的回溯追踪。但是本规范并不要求回溯追踪。
强制回溯追踪的问题在于异常可以在程序中的某一点创建,并在之后的另一点抛出。在异常中存储栈轨迹所付出的代价是无法令人接受的,除非它确实被抛出了(在这种情况下,轨迹会在释放栈资源时生成)。因此我们不强制在每个异常中回溯追踪。

2.3 try-with-resources

带资源的 try 语句是用变量(被称为资源)来参数化的,这些资源在 try 块执行之前被初始化,并且会在 try 块执行之后,自动地以与初始化相反地顺序被关闭。当资源会被自动化关闭时,catch 子句 和 finally 子句通常就不是必需的了。

TryWithResourcesStatement:
  try ResourceSpecification Block [Catches] [Finally]
ResourceSpecification:
  ( ResourceList [;] )
ResourceList:
  Resource {; Resource}
Resource:
  {VariableModifeier} UnannType VariableDeclaratorId = Expression

ResourceSpecification 用初始化器表达式声明了一个或多个局部变量作为 try 语句中的 Resource 。
对于ResourceSpecification 来说,声明两个具有相同名字的变量是一种编译时错误。
如果 final 作为修饰符在每一个在 ResourceSpecification 中声明的变量中出现了多次,那么就是一个编译时错误。
如果没有被显式地声明为 final ,那么在 ResourceSpecification 中声明的资源会被隐式地声明为final。
在 ResourceSpecification 中声明的变量的类型必须是 AutoCloseable 的子类型,否则就会产生编译时错误。
资源是按照从左到右的顺序初始化的。如果某个资源初始化失败了(即,其初始化器表达式抛出了异常),那么所有已经被带资源的 try 语句初始化的资源都将被关闭。如果所有资源都成功初始化了,那么 try 块会正常执行,然后该带资源的 try 语句的所有非空资源都将被关闭。
资源将以与它们被初始化的顺序相反的顺序被关闭。资源只有在其被初始化为非空值时才会被关闭。在关闭资源时抛出的异常不会阻止其他资源的关闭。如果之前在某个初始化器、try 块或资源关闭中抛出过异常,那么这种异常会被压制。
带有声明了多种资源的 ResourceSpecification 子句的带资源 try 语句会被当作多个带资源的 try 语句对待,其中每个都有一个声明了单一资源的 ResourceSpecification 子句。当带有 n (n > 1) 个资源的带资源 try 语句被转译时,其结果是带有 n - 1 个资源的带资源 try 语句。在 n 次这样的转译之后,就会产生 n 个嵌套的 try-catch-finally 语句,至此所有的转译就结束了。

2.3.1 基本的带资源的 try 语句

不带任何 catch 子句或 finally 子句的带资源的 try 语句被称为基本的带资源的 try语句。
基本的带资源的 try 语句:

try ({VariableModifier} R Identifier = Expression ...)
  Block

其含义是由下面转译成的局部变量声明和 try-catch-finally 语句给出的:

{
  final {VariableModifierNoFinal} R Identifier = Expression;
  Throwable #primaryExc = null;
 
  try ResourceSpecification_tail
    Block
  catch (Throwable #t) {
    #primaryExc = #t;
    throw #t;
  } finally {
    if (Identifier != null) {
      if (#primaryExc != null) {
        try {
          Identifier.close();
        } catch (Throwable #suppressedExc) {
          #primaryExc.addSuppressed(#suppressedExc);
        }
      } else {
        Identifier.close();
      }
    }
  }
}

{VariableModifierNoFinal}是作为不带 final 的{VariableModifier}而定义的(如果它存在的话)。
#t、#primaryExc 和 #suppresedExc 是自动生成的标识符,它们有别于在带资源的 try 语句出现之处位于其作用域中的其他任何标识符(无论是自动生成的还是其他)。
如果 ResourceSpecification 声明了一个资源,那么 ResourceSpecification_tail 就是空的(并且该 try-catch-flnally 语句自身并不是一个带资源的 try 语句)。
如果 ResourceSpecification 声明了 n > 1 个资源,那么在 ResourceSpecification_tail 中就以同样的顺序包含了在 ResourceSpecification 中的第2个、第3个、…、第 n 个资源(并且该 try-catch-finally 语句自身也是一个带资源的 try 语句)。
用于基本的带资源 try 语句的可达性和明确赋值规则由上面的转译隐式地进行了说明。
在只管理单一资源的基本的带资源 try 语句中:

2.3.2 扩展的带资源的 try 语句

带有至少一个 catch 子句或 finally 子句的带资源的 try 语句被称为扩展的带资源的try 语句。
扩展的带资源的 try 语句:
try ResourceSpecification
Block
{Catches}
{Finally}
其含义是由下面转译成的嵌套在try-catch、try-finally 或 try-catch-flnally 语句中的基本的带资源的 try 语句给出的:
try {
try ResourceSpecification
Block
}
{Catches}
{Finally}
这种转译的效果就像是将 ResourceSpecification 放置到 try 语句的“内部” 一样。这使得扩展的带资源的 try 语句的 catch 子句可以捕获异常,因为任何资源都会自动地初始化和关闭。
更进一步,所有资源在 finally 块被执行的时刻都已经被关闭(或尝试被关闭),这与 finally 关键词的意图也保持了一致。

3. 面试题

catch 中发生了未知异常,finally 还会执行么?
会的,catch 无论是否发生异常,finally 总会执行,并且 catch 中的异常是在 finally 执行完成之后,才会抛出的。
不过 catch 会吃掉 try 中抛出的异常,为了避免这种情况,在一些可以预见 catch 中会发生异常的地方,先把 try 抛出的异常打印出来,这样从日志中就可以看到完整的异常了。

参考资料

上一篇下一篇

猜你喜欢

热点阅读