最近在一次写代码的时候,出现了一个低级错误,但凡对异常有些了解,也不至于写出这样的代码:
try {
//不应该直接在try语句块中抛异常,catch直接获取后,相当于异常没抛出去
throw new ThirdPlatformException("第三方平台异常");
} catch {
}
说明自己对异常的处理机制和异常处理的规范都不太了解,趁着这次出现的问题,来好好学习Java异常的体系机制和原理。这篇文章主要通过三个部分阐释Java异常
try-catch
, try-finally
, try-with-resource
的字节码分析在Java中,异常就是指Java程序运行中出现的问题,比如网络资源读取失败,空指针,使用非法数组索引等等。而我们日常看到的各种xxxException
类就是对这些异常的描述,他们都是派生于Throwable
类(表示可以抛出这个异常)的一个类的实例,下面就来详细介绍一下Java中的异常分类:
Throwable
类:**是所有错误和异常的超类,其中包括该线程执行堆栈的快照,提供printStackTrace()
等接口用用获取堆栈异常等等信息。Error
类及其子类:表示运行应用程序中出现了严重错误,一般表示代码运行中的非代码错误。当此类异常发生时,应用程序不应该去处理此类错误。因此我们也不应该实现任何新的Error子类的。Exception
**类及其子类是程序本身可以捕获并且可以处理的异常。也是在日常写代码中接触的最多的一类异常,主要有两类:**运行时异常(RuntimeException)**和 编译异常(非运行时异常)
RuntimeException
类及其子类,比如NullPointerException
、IndexOutOfBoundsException
等。这类异常的特点是Java编译器不会检查,即便没有使用异常处理,代码也会编译通过RuntimeException
类及其子类外的Exception
子类,比如IOException
、SQLException
等。这类异常Java编译器肯定会检查,如果不作异常处理,代码就不能编译通过。上面提到Exception时,有些异常不会被编译通过。所以对于整个异常体系来说,在是否能被Java编译器检查的角度,又分为可查异常和不可查异常。
除了这些JDK自带的异常外,我们同样可以自定义异常,通过继承相关的异常类
class CheckedException extends Exception{}
class UnCheckedException extends RuntimeException{}
这些自定义检查异常的效果和JDK中自带的异常相同,自定义的检查异常不处理的话,同样会编译不通过。
try
(监听异常):主要用于监听try语句块的代码,当其中的代码出现异常,就会被抛出,需要和catch
、finally
等其他关键字一起使用catch
(捕获异常):用来捕获异常,在捕获try
语句块的异常后会执行该语句块中的代码finally
(总是会被执行):无论是否有异常,都会执行该语句块中的代码。throw
(抛出异常):抛出相关异常,如前言中的:throw new ThirdPlatformException("第三方平台异常");
然而在大多数情况中,都不需要手动抛出异常,一方面在调用JDK资源类时,已经处理过异常;另一方面在业务代码中,都会统一自定义异常类,所以尽量做捕获异常或者向上抛。
throws
(声明异常):在方法上声明可能会抛出的异常,作用是将异常传递给合适的处理程序,比如:public interface LargeModelSession throws RuntimeException{}
try
、catch
和finally
都不能单独使用,只能是try-catch
、try-finally
或者try-catch-finally
把大象放入冰箱需要几步?类似的,对于程序中的异常处理,可以分为发现异常,传递异常和处理异常这三步:
try
块来监听异常throws
显示声明可能会抛出的异常try
块的异常抛给catch
,或者不处理直接到达finally
块throws
抛给该方法的调用者throw
直接new一个异常实例抛出try-catch
类型则直接在catch
块中处理异常throws
则需要方法调用者进行处理常见的异常捕获主要有以下几种:
try-catch
try-catch-finally
try-finally
try-with-resource
try-catch
try-catch语句中可以通过多个catch捕获多个异常类型,并做不同的处理,并且也可以在一个catch中捕获不同的异常:
/**1. 多个catch捕获多个异常类型*/
try{
//监视的可能会出现异常的代码
} catch(Exception1 e1){
//Exception1类型的异常处理
} catch(Exception2 e2){
//Exception1类型的异常处理
}
/**2. 一个catch捕获多个异常类型*/
try{
//监视的可能会出现异常的代码
} catch(Exception1 |Exception2 e2){
//Exception1和Exception2类型的异常处理
}
当程序发生异常后,会按照异常从上到下(多个catch)或者从左到右(一个catch)的顺序依次匹配,匹配成功后就直接在该catch中进行处理。
try-catch-finally
try-catch-finally类型的特点是不管try块中是否监听到异常,finally块中的语句都会被执行:
try {
//监视可能会出现异常的代码
} catch(Exception e) {
//捕获异常并处理
} finally {
//一定会执行的代码
}
finally块主要用在IO读取、数据库连接、运行清理等需要关闭的场景
try-finally
try-finally比try-catch-finally更加直接,try块的代码出现异常不予处理,立即执行finally块中代码,一般用于不需要捕获异常的代码:
//截取DefaultMBeanServerInterceptor中的部分代码
final ResourceContext context = unregisterFromRepository(resource, instance, name);
try {
if (instance instanceof MBeanRegistration)
postDeregisterInvoke(name,(MBeanRegistration) instance);
} finally {
context.done();//无论是否出现问题直接执行
}
try-with-resource
try-with-resource是JDK1.7后引入的,如果一个类实现了AutoCloseable
接口,那么这个类就可以写在try后的括号中,并且能再try-catch块执行后自动执行close方法,也就不用再写finally块
它实际上将try-catch-finally简化成try-catch,在编译时会转化成try-catch-finally语句,主要包含三个部分:
try(Connection conn = newConnection()) {
conn.sendData()
}catch(Exception e) {
e.printStackTrace();
}
异常到底该如何处理,首先可以借鉴一下国内优秀开发团队的异常处理经验,也就是异常处理规范:
对于异常处理实践规范,最著名的就是阿里Java异常处理规约,在此基础上也请教了公司经验丰富的同事,总结列出如下异常处理规范:
异常类其实也是一种资源消耗,如果我们能够通过预先逻辑判断,检查出来可能会发生的问题,就可以避免使用异常:
类似的,在空指针问题的处理上,应该对远程调用的对象要做好预先检查和处理:
//比如知道会出现ArithmeticException,却捕获RuntimeException异常
try{
int a = 3/0;
} catch(RuntimeException e){ //
...
}
如果在catch后什么都不做,相当于把异常给吞了,这个异常什么也没有干,还消耗创建异常类的资源,因此捕获了异常后一定要描述清楚错误信息
//可以使用e.printStackTrace(),但是在日志中无法查看具体的信息
//因此可以尝试使用日志框架来打印错误信息
logger.error("说明信息,异常信息:{}", e.getMessage(), e)
try{
...
}catch(ArithmeticException e) {
throw new NullPointerException(e); //抛出和捕获异常完全不同的异常,会导致异常转译错误
}
try{
...
}catch(ArithmeticException e) {
throw new RuntimeException(e); //RuntimeException是ArithmeticException父类,传递过程会丢失异常信息
}
try{
...
}catch(ArithmeticException e) { //与其抛出完全相同的异常,还不如直接处理打印异常信息
throw new ArithmeticException(e);
}
在抛出异常时,可以自定义异常信息然后抛出,但这个时候尽量传入完整捕获异常的异常信息
try{
...
}catch(ArithmeticException e) { //自定义包装异常应该传入完整的异常信息
throw new MyException(e.getMessage()); //错误
throw new MyException(e); //正确
}
try{
...
}catch(Exception e) {
logger.error("有异常,小心",e);
throw new NullPointerException(e); //会产生多条日志信息,两者选一条即可
}
在记录异常信息的同时又抛出异常,会产生多条的日志信息,而且在后期如果出现异常,日志也不太好分析
try {
...
} catch (NumberFormatException e) { //NumberFormatException是IllegalArgumentException的子类
logger.error(e);
} catch (IllegalArgumentException e) {
logger.error(e)
}
finally语句相当于嵌入try块和catch块中
利用finally块关闭资源
FileInputStream inputStream = null;
try {
File file = new File("./test.txt");
inputStream = new FileInputStream(file);
} catch (FileNotFoundException e) {
logger.error(e);
} finally {
if (inputStream != null) { //如果finally中发现异常,可以继续用
try {
inputStream.close();
} catch (IOException e) {
logger.error(e);
}
}
}
利用try-with-resource关闭资源,注意该资源类必须实现AutoCloseable接口
File file = new File("./test.txt");
try (FileInputStream inputStream = new FileInputStream(file);) {
} catch (FileNotFoundException e) {
logger.error(e);
} catch (IOException e) {
logger.error(e);
}
下面我创建一个项目来详细讲解SpringBoot中的异常,项目详细地址Link为:
BasicExceptionController
转发到异常页面比如每当我们访问其他网页出现问题时,总会跳转到404页面,这就是一种处理异常的方式。一旦系统全局中出现异常,SpringBoot就会请求异常错误,然后通过BasicExceptionController
来处理这个请求,并让当前页面跳转至对应的异常页面。例如我在项目中没有创建任何接收网络请求的controller
,这个时候在浏览器发起请求,那么springboot框架会转发请求并跳转至默认异常页面:
用的最多的场景是在网络请求中出现问题,比如喜闻乐见
的404页面,就是对这种异常的一种处理与反馈。
@ExceptionHandler
处理局部异常该注解用在某个控制器类中的方法上,可以集中处理不同类型的异常。如果该控制器类中的其他方法抛出对应异常,该注解方法都能拦截并处理:
@Controller
@RequestMapping("/exception")
public class ExceptionController {
private static final Logger logger = LoggerFactory.getLogger(ExceptionController.class);
/**
* 在一个方法中统一处理异常,在该类下的其他方法出现异常,都会在该方法中处理
* @return
*/
@ExceptionHandler({ArithmeticException.class, NullPointerException.class})
public ModelAndView testExceptionHandler(Exception ex) {
ModelAndView mv = new ModelAndView();
mv.addObject("ex", ex);
if (ex instanceof ArithmeticException) {
mv.setViewName("ArithmeticException");
logger.info("当前是ArithmeticException异常");
} else if (ex instanceof NullPointerException){
mv.setViewName("NullPointerException");
logger.info("当前是NullPointerException异常");
} else{
mv.setViewName("error");
logger.info("当前是error异常");
}
return mv;
}
/**
* 运算式异常
* @return
*/
@GetMapping("/arthmetic")
@ResponseBody
public String testExceptionHandler2() throws ArithmeticException{
logger.error(String.valueOf(1/0));
return "testExceptionHandler";
}
/**
* 空指针异常
* @return
* @throws NullPointerException
*/
@GetMapping("/nullPointer")
@ResponseBody
public String testExceptionHandler3() throws NullPointerException{
String string = new String();
string = null;
java.lang.String s = string.toString();
logger.info(s);
return "testNullPointer";
}
}
对该ExceptionController
中的方法进行请求测试,得到如下结果:
GET http://localhost:8080/exception/arthmetic
GET http://localhost:8080/exception/nullPointer
----------------------------------------------
INFO 31092 --- [nio-8080-exec-2] c.e.s.controller.ExceptionController : 当前是ArithmeticException异常
INFO 31092 --- [nio-8080-exec-7] c.e.s.controller.ExceptionController : 当前是NullPointerException异常
此外,其他的controller
类可以通过继承ExceptionController
来获取到异常处理的方法
@Controller
@RequestMapping("/first")
public class FirstController extends ExceptionController{
private static final Logger logger = LoggerFactory.getLogger(ExceptionController.class);
@RequestMapping("/testException1")
public void testFirst() throws ArithmeticException{
logger.error(String.valueOf(1/0));
}
}
请求该接口后,同样会调用继承类中定义好的异常处理方法:
GET http://localhost:8080/first/testException1
----------------------------------------------
INFO 13860 --- [nio-8080-exec-2] c.e.s.controller.ExceptionController : 当前是ArithmeticException异常
这样如果其他的controller需要处理同样的异常,就必须继承该异常controller,会显得比较麻烦。能够使用更加优雅的方式,让全局所有的controller应用该异常类的处理方法嘛?有的,可以通过@ControllerAdvice
+@ExceptionHandler
:
@ControllerAdvice
+ @ExceptionHandler
处理全局异常我们可以通过定义一个全局的异常处理类,在这个类中加上@ControllerAdvice
注解,并在方法中加上@ExceptionHandler
注解,来处理所有Controller的异常:
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(ExceptionController.class);
@ExceptionHandler({ArithmeticException.class, NullPointerException.class})
public ModelAndView testExceptionHandler(Exception ex) {
ModelAndView mv = new ModelAndView();
mv.addObject("ex", ex);
if (ex instanceof ArithmeticException) {
mv.setViewName("ArithmeticException");
logger.info("当前是全局ArithmeticException异常");
} else if (ex instanceof NullPointerException){
mv.setViewName("NullPointerException");
logger.info("当前是全局NullPointerException异常");
} else{
mv.setViewName("error");
logger.info("当前是全局error异常");
}
return mv;
}
}
单独定义一个controller
,如果发生异常,也能通过全局异常处理类进行处理:
public class SecondController{
private static final Logger logger = LoggerFactory.getLogger(ExceptionController.class);
@RequestMapping("/testException2")
public void testFirst() throws ArithmeticException{
logger.error(String.valueOf(1/0));
}
}
请求测试结果:
INFO 33660 --- [nio-8080-exec-1] c.e.s.controller.ExceptionController : 当前是全局ArithmeticException异常
SimpleMappingExceptionResolver
类处理全局异常同样也可以定义一个全局异常类,来处理全局异常,和@ControllerAdvice
+ @ExceptionHandler
不同点是在全局异常类上加一个@Configuration
注解,并将SimpleMappingExceptionResolver注入Spring容器中:
@Configuration
public class GlobalException {
@Bean
public SimpleMappingExceptionResolver getSimpleMappingExceptionResolver() {
SimpleMappingExceptionResolver resolver = new SimpleMappingExceptionResolver();
Properties properties = new Properties();
//设置异常类型并映射到不同的jsp页面
properties.put("java.lang.ArithmeticException", "ArithmeticException");
properties.put("java.lang.NullPointerException", "NullPointerException");
//将配置文件映射到resolver中
resolver.setExceptionMappings(properties);
return resolver;
}
}
在项目中添加ArithmeticException.jsp
和 NullPointerException.jsp
文件,同样发送请求测试:
<%@ page language="java" contentType="text/html; charset=Utf-8"
pageEncoding="Utf-8"%>
DOCTYPE html>
<html>
<head>
<meta charset="Utf-8">
<title>ArithmeticExceptiontitle>
head>
<body>
发生ArithmeticException异常
body>
html>
GET http://localhost:8080/first/testException1
@Controller
@RequestMapping("/first")
public class FirstController extends ExceptionController{
private static final Logger logger = LoggerFactory.getLogger(ExceptionController.class);
@RequestMapping("/testException1")
public void testFirst() throws ArithmeticException{
logger.error(String.valueOf(1/0));
}
}
HandlerExceptionResolver
接口处理全局异常需要实现HandlerExceptionResolver
接口,并全局配置:
@Configuration
public class HandlerExceptionResolverImpl implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
ModelAndView modelAndView = new ModelAndView();
if (ex instanceof NullPointerException) {
modelAndView.setViewName("NullPointerException");
return modelAndView;
} else if (ex instanceof ArithmeticException) {
modelAndView.setViewName("ArithmeticException");
return modelAndView;
}
return modelAndView;
}
}
配置接口和jsp文件,进行测试验证:
<%@ page language="java" contentType="text/html; charset=Utf-8"
pageEncoding="Utf-8"%>
DOCTYPE html>
<html>
<head>
<meta charset="Utf-8">
<title>ArithmeticExceptiontitle>
head>
<body>
实现接口:发生ArithmeticException异常
body>
html>
@Controller
@RequestMapping("/first")
public class FirstController{
private static final Logger logger = LoggerFactory.getLogger(ExceptionController.class);
@RequestMapping("/testException1")
public void testFirst() throws ArithmeticException{
logger.error(String.valueOf(1/0));
}
}
GET http://localhost:8080/first/testException1
同样,作为spring框架的核心,我们可以使用切面来对异常进行处理:
@Aspect
@Component
public class WebRequestExceptionAspect {
private static final Logger logger = LoggerFactory.getLogger(WebRequestExceptionAspect.class);
//拦截带有@RequestMapping的注解方法
@Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
private void webRequestPointcut() {
}
@AfterThrowing(pointcut = "webRequestPointcut()", throwing = "ex")
public void handleException(Exception ex) {
//拦截异常并设置对应的异常信息
String exceptionMsg = StringUtils.isEmpty(ex.getMessage()) ? "出现异常" : ex.getMessage();
logger.error("发生了异常:{}",exceptionMsg);
}
}
测试:
GET http://localhost:8881/first/testException1
-------------------------------------------------------------------------------------------------
ERROR 29824 --- [nio-8881-exec-2] c.e.s.e.WebRequestExceptionAspect : 发生了异常:/ by zero
java.lang.ArithmeticException: / by zero
at com.ethan.springbootexception.controller.FirstController.testFirst(FirstController.java:21) ~[classes/:na]
at com.ethan.springbootexception.controller.FirstController$$FastClassBySpringCGLIB$$cfba05ad.invoke(<generated>) ~[classes/:na]
...
上面提到了在Springboot框架中的异常处理注解,但在实际项目中不仅要在框架层面考虑异常,而且还要在业务代码层面捕获和处理异常,此外需要根据不同业务逻辑、异常类型来分别处理:
也就是从用户的角度来看,如果用户能处理则抛出业务异常,不能处理就抛出系统异常。
借用12 | 异常处理:别让自己在出问题的时候变为瞎子-极客时间中的例子来说明:
对于自定义的业务异常,以 Warn 级别的日志记录异常以及当前 URL、执行方法等信息后,提取异常中的错误码和消息等信息,转换为合适的 API 包装体返回给 API 调用方;
对于无法处理的系统异常,以 Error 级别的日志记录异常和上下文信息(比如 URL、参数、用户 ID)后,转换为普适的“服务器忙,请稍后再试”异常信息,同样以 API 包装体返回给调用方。
@RestControllerAdvice
@Slf4j
public class RestControllerExceptionHandler {
private static int GENERIC_SERVER_ERROR_CODE = 2000;
private static String GENERIC_SERVER_ERROR_MESSAGE = "服务器忙,请稍后再试";
@ExceptionHandler
public APIResponse handle(HttpServletRequest req, HandlerMethod method, Exception ex) {
if (ex instanceof BusinessException) {
BusinessException exception = (BusinessException) ex;
log.warn(String.format("访问 %s -> %s 出现业务异常!", req.getRequestURI(), method.toString()), ex);
return new APIResponse(false, null, exception.getCode(), exception.getMessage());
} else {
log.error(String.format("访问 %s -> %s 出现系统异常!", req.getRequestURI(), method.toString()), ex);
return new APIResponse(false, null, GENERIC_SERVER_ERROR_CODE, GENERIC_SERVER_ERROR_MESSAGE);
}
}
}
这样,能够在异常出现时,方便维护人员根据日志上下文快速解决问题。
说用异常慢,首先来看看异常慢在哪里?有多慢?下面的测试用例简单的测试了建立对象、建立异常对象、抛出并接住异常对象三者的耗时对比:
public class ExceptionTest {
private int testTimes;
public ExceptionTest(int testTimes) {
this.testTimes = testTimes;
}
/**
* 创建Object对象
*/
public void newPureObject() {
long startTime = System.nanoTime();
for (int i = 0; i < testTimes; i++) {
new Object();
}
System.out.println("创建普通对象时间:" + (System.nanoTime() - startTime));
}
/**
* 创建Exception对象
*/
public void newException() {
long startTime = System.nanoTime();
for (int i = 0; i < testTimes; i++) {
new RuntimeException();
}
System.out.println("创建异常对象时间:" + (System.nanoTime() - startTime));
}
/**
* 创建异常并捕获Exception
*/
public void catchException() {
long startTime = System.nanoTime();
for (int i = 0; i < testTimes; i++) {
try {
throw new RuntimeException();
} catch (RuntimeException e) {
}
}
System.out.println("创建异常并捕获Exception对象时间:" + (System.nanoTime() - startTime));
}
public static void main(String[] args) {
//在1000次循环中三种创建方式的耗时对比
ExceptionTest exceptionTest = new ExceptionTest(1000);
exceptionTest.newPureObject();
exceptionTest.newException();
exceptionTest.catchException();
}
}
测试在1000次循环中三种创建方式的耗时对比,测试结果:
--------------------------------------
创建普通对象时间:91200
创建异常对象时间:1131900
创建异常并捕获Exception对象时间:1196900
说明创建一个对象时间是创建普通Object对象12倍,所以在流程业务中,最好不要用异常来进行处理。下面就从字节码角度看看异常的处理:
下面就来看一看字节码层面的异常处理过程,查看类的字节码信息需要使用jclasslib Bytecode Viewer,我是IDEA环境,直接在plugin搜索安装即可:
首先写一个包含try-catch的方法,并查看其字节码:
先说说代码对应的字节码含义:
System.out.println("查看try-catch的字节码")
这段代码try{} catch()
中的代码块catch{}
的代码块重点看try块中的代码:
new
指令: **new 指令用于创建一个新的对象。这里的操作是创建一个RuntimeException的新实例dup
指令:**dup 指令用于复制栈顶的值。这里的操作是复制栈顶的异常对象引用,以备后续使用invokespecial
指令: **invokespecial 指令用于调用对象的构造方法。这里的操作是调用RuntimeException的默认构造方法,初始化新创建的异常对象。athrow
指令: **throw的底层实现指令,其抛出的objectref
必须是引用类型,而且是Throwable
或其子类。抛出对应的异常后,会在异常表中查找第一个与该异常相匹配的异常类型(也就是捕获异常处),这里抛出的是RuntimeException
具体的解释可以查看此处的JDK文档
The objectref must be of type reference and must refer to an object that is an instance of class Throwable or of a subclass of Throwable. It is popped from the operand stack. The objectref is then thrown by searching the current method (§2.6) for the first exception handler that matches the class of objectref, as given by the algorithm in §2.10.
If an exception handler that matches objectref is found, it contains the location of the code intended to handle this exception. The pc register is reset to that location, the operand stack of the current frame is cleared, objectref is pushed back onto the operand stack, and execution continues.
If no matching exception handler is found in the current frame, that frame is popped. If the current frame represents an invocation of a synchronized method, the monitor entered or reentered on invocation of the method is exited as if by execution of a monitorexit instruction (§monitorexit). Finally, the frame of its invoker is reinstated, if such a frame exists, and the objectref is rethrown. If no such frame exists, the current thread exits.
astore_1
指令:将栈顶的引用类型变量放入局部变量表中索引为1的位置,这里的操作表示将捕获到的RuntimeException
存储到局部变量表为1的位置。那我们再来看看异常表:
Start PC
: 开始计数器位置
End PC
: 结束计数器位置
Handler PC
:发生异常后程序跳转的位置
Catch Type
: 捕获异常类型
具体在字节码层面怎么操作的呢?首先new
一个RuntimeException
异常类型实例,然后去异常表中查找是否存在这个类型,如果有则跳转到Handler PC
的位置,继续执行代码(捕获异常后的处理)
在try-catch基础上增加finally代码块,再来查看一下其字节码:
我们知道无论是否捕获异常,程序都会执行finally中的代码块,那么字节码中如何实现的呢?首先加上finally后,在字节码中出现了两次fianlly代码块中的内容,再来看看异常表:
发现比try-catch多了一条any类型的记录,这条记录说明在8~25行无论是否抛出异常,都会跳转到36行执行。
下面我们来从字节码的角度,无论是抛出异常还是捕获异常,是否都会执行finally中的代码:
假设执行过程中没有异常,程序会一直沿着字节码往下执行:
System.out.println("查看try-catch-finally的字节码");
假设执行过程中发生了异常,程序会按照如下顺序执行:
System.out.println("查看try-catch-finally的字节码");
RuntimeException
异常,在异常表中查找到第一条记录,按照异常表显示的16行继续执行再来看看try-finally语句的字节码:
以及异常表:
从异常表的记录我们知道,无论是否抛出异常,都会执行finally中的语句。
我们可以用一个例子来说明:
public int testFinally() {
int i = 0;
try {
i = 1;
return i;
} finally {
System.out.println("finally语句执行");
}
}
/**
* 执行结果:
* finally语句执行
* 1
*/
但是如果在 finally 中对变量进行修改,情况就有些不同:
我们同样举例来说明:
public int testFinally() {
int i = 0;
try {
i = 1;
return i;
} finally {
i = 2;
}
}
public StringBuilder testFinallyReturn() {
StringBuilder exception = new StringBuilder("Exception");
try {
exception.append("java");
return exception;
} finally {
exception.append("last");
}
}
public static void main(String[] args) {
AnalysisExceptionCode analysisExceptionCode = new AnalysisExceptionCode();
int I = analysisExceptionCode.testFinally();
System.out.println(I);
StringBuilder stringBuilder = analysisExceptionCode.testFinallyReturn();
System.out.println(stringBuilder.toString());
}
执行结果:
1
Exceptionjavacode
说明finally语句对引用数据类型中的值进行了修改,那么看看两个方法的字节码:
首先是对基本类型(int)的修改:
最后返回的结果是1,因此finally语句修改值后,并没有对其产生影响,我们再来看看字节码指令
0 iconst_0 #将0推送到操作数栈上
1 istore_1 #将操作数栈顶的0出栈,存储到局部变量表1的位置
2 iconst_1 #将1推送到操作数栈上
3 istore_1 #将操作数栈顶的1出栈,存储到局部变量表1的位置(1覆盖了之前存储的0)
4 iload_1 #将局部变量表1位置的1,加载到操作数栈上
5 istore_2 #将操作数栈顶的1出栈,存储到局部变量表2的位置
6 iconst_2 #将2推送到操作数栈上
7 istore_1 #将操作数栈顶的2出栈,存储到局部变量表1的位置(2覆盖了之前存储的1)
8 iload_2 #将局部变量表2位置的1,加载到操作数栈上
9 ireturn #返回操作数栈的值1
#如果发生异常,则将异常对象存储到局部变量表3的位置
10 astore_3
11 iconst_2
12 istore_1
13 aload_3
14 athrow
接着再来看看引用类型:
#这段创建StringBuilder,将对象引用放入操作数栈中,类似于基本类型中的int i = 0
0 new #11 //创建一个新的StringBuilder对象
3 dup //复制栈顶的引用,以备后续使用
4 ldc #12 //将字符串常量"Exception"加载到操作数栈中
6 invokespecial #13 : (Ljava/lang/String;)V> //调用StringBuilder的构造方法,将之前加载的字符串常量Exception作为参数传递进去,初始化新创建的StringBuilder对象。
#######################################################################
9 astore_1 //将操作数栈中的对象引用出栈,放入局部变量表的1位置
10 aload_1 //将局部变量表1中的对象引用加载到操作数栈中
#######################################################################
#这段类似于try语句块中的 i = 1
11 ldc #14 //将字符串常量"java"加载到操作数栈中
13 invokevirtual #15 //调用StringBuilder对象的append方法,将之前加载的字符串常量作为参数传递进去,实现字符串的拼接
16 pop //从操作数栈中弹出append方法的返回值
#######################################################################
17 aload_1 //将局部变量表1中的对象引用加载到操作数栈中
18 astore_2 //将栈顶的引用存储到局部变量表中的索引为2的位置。这里是将StringBuilder对象的引用存储到局部变量2的位置
19 aload_1 //将局部变量表中索引为1的对象引用加载到操作数栈中
#######################################################################
#这段类似于finally语句块中的 i = 2
20 ldc #16 //将字符串常量"code"加载到操作数栈中
22 invokevirtual #15 //调用StringBuilder对象的append方法,将之前加载的字符串常量作为参数传递进去,实现字符串的拼接
25 pop //从操作数栈顶弹出一个值,这里是丢弃append方法的返回值
#######################################################################
26 aload_2 //将局部变量表中索引为2的引用加载到操作数栈中
27 areturn //将栈顶的对象引用作为返回值返回
#如果有异常,则执行该段代码
28 astore_3
29 aload_1
30 ldc #16
32 invokevirtual #15
35 pop
36 aload_3
37 athrow
其实无论是基本类型和引用类型,其字节码执行流程都大致相同,但是最后finally语句块的修改还是影响到了引用类型,这是因为操作数栈,局部变量表中存储的变量是引用地址,而不是对象本身,因此每次局部变量表的覆盖操作,都影响了对象本身。因此引用类型内部的值才被修改。
先用例子来看看是否会存在这种现象:
public int testFinally() {
int i = 0;
try {
i = 1;
return i;
} finally {
i = 2;
return i;
}
}
/**
* 执行结果:
* 2
*/
果然出现了 try 块中 return 的失效现象,再来看看这个方法和不加 return 方法对比的字节码:
问题在于第8行:
这里借用Java异常处理和最佳实践(含案例分析)中的例子:通过打包文件来看一下其本质:
public static void zipFile(List<File> fileList) {
// 文件的压缩包路径
String zipPath = OUT + "/打包附件.zip";
// 获取文件压缩包输出流
try (OutputStream outputStream = new FileOutputStream(zipPath);
CheckedOutputStream checkedOutputStream = new CheckedOutputStream(outputStream, new Adler32());
ZipOutputStream zipOut = new ZipOutputStream(checkedOutputStream)) {
for (File file : fileList) {
// 获取文件输入流
InputStream fileIn = new FileInputStream(file);
// 使用 common.io中的IOUtils获取文件字节数组
byte[] bytes = IOUtils.toByteArray(fileIn);
// 写入数据并刷新
zipOut.putNextEntry(new ZipEntry(file.getName()));
zipOut.write(bytes, 0, bytes.length);
zipOut.flush();
}
} catch (FileNotFoundException e) {
System.out.println("文件未找到");
} catch (IOException e) {
System.out.println("读取文件异常");
}
}
实际上这是Java的一种语法糖,查看编译后的代码就知道编译器为我们做了什么,下面是反编译后的代码:
public static void zipFile(List<File> fileList) {
String zipPath = "./打包附件.zip";
try {
OutputStream outputStream = new FileOutputStream(zipPath);
Throwable var3 = null;
try {
CheckedOutputStream checkedOutputStream = new CheckedOutputStream(outputStream, new Adler32());
Throwable var5 = null;
try {
ZipOutputStream zipOut = new ZipOutputStream(checkedOutputStream);
Throwable var7 = null;
try {
Iterator var8 = fileList.iterator();
while(var8.hasNext()) {
File file = (File)var8.next();
InputStream fileIn = new FileInputStream(file);
byte[] bytes = IOUtils.toByteArray(fileIn);
zipOut.putNextEntry(new ZipEntry(file.getName()));
zipOut.write(bytes, 0, bytes.length);
zipOut.flush();
}
} catch (Throwable var60) {
var7 = var60;
throw var60;
} finally {
if (zipOut != null) {
if (var7 != null) {
try {
zipOut.close();
} catch (Throwable var59) {
var7.addSuppressed(var59);
}
} else {
zipOut.close();
}
}
}
} catch (Throwable var62) {
var5 = var62;
throw var62;
} finally {
if (checkedOutputStream != null) {
if (var5 != null) {
try {
checkedOutputStream.close();
} catch (Throwable var58) {
var5.addSuppressed(var58);
}
} else {
checkedOutputStream.close();
}
}
}
} catch (Throwable var64) {
var3 = var64;
throw var64;
} finally {
if (outputStream != null) {
if (var3 != null) {
try {
outputStream.close();
} catch (Throwable var57) {
var3.addSuppressed(var57);
}
} else {
outputStream.close();
}
}
}
} catch (FileNotFoundException var66) {
System.out.println("文件未找到");
} catch (IOException var67) {
System.out.println("读取文件异常");
}
}
在使用try-with-resource时,try(声明需要关闭的资源),并且需要其声明的变量实现AutoCloseable
接口,从编译代码可以看到,编译器能帮我们自动关闭资源,这样就可以不用写finally语句块,编译器具体的异常处理过程如下:
- try 块没有发生异常时,自动调用 close 方法,
- try 块发生异常,然后自动调用 close 方法,如果 close 也发生异常,catch 块只会捕捉 try 块抛出的异常,close 方法的异常会在catch 中通过调用 Throwable.addSuppressed 来压制异常,但是你可以在catch块中,用 Throwable.getSuppressed 方法来获取到压制异常的数组。