作者:明明如月学长, CSDN 博客专家,大厂高级 Java 工程师,《性能优化方法论》作者、《解锁大厂思维:剖析《阿里巴巴Java开发手册》》、《再学经典:《EffectiveJava》独家解析》专栏作者。
热门文章推荐:
- (1)《为什么很多人工作 3 年 却只有 1 年经验?》
- (2)《从失望到精通:AI 大模型的掌握与运用技巧》
- (3)《AI 时代,程序员的出路在何方?》
- (4)《如何写出高质量的文章:从战略到战术》
- (5)《我的技术学习方法论》
- (6)《我的性能方法论》
- (7)《AI 时代的学习方式: 和文档对话》
- (8)《人工智能终端来了,你还在用过时的 iterm?》
最近对某段代码进行代码审查,无意间发现一个哭笑不得的“神操作”!
该同学代码中用最标准的释放资源的方法,可是并没有正确释放资源。
本文将模拟该问题,讲述背后的原因,希望大家编码时要特别注意该问题。
package com.demo.demo;
import okhttp3.*;
import java.io.IOException;
public class OkHttpExample {
public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
OkHttpClient client = new OkHttpClient();
public SomeResult post(String url, String json) throws IOException {
RequestBody body = RequestBody.create(JSON, json);
Request request = new Request.Builder()
.url(url)
.post(body)
.build();
Response response = null;
try{
return someMethod(response,request);
}finally {
if(response != null){
response.close();
}
}
}
private SomeResult someMethod(Response response, Request request) throws IOException {
response = client.newCall(request).execute();
// 其他逻辑
return SomeUtils.toSomeResult(response);
}
public static void main(String[] args) {
OkHttpExample example = new OkHttpExample();
String json = "{\"name\":\"mkyong\"}";
String response;
try {
response = example.post("https://api.yourdomain.com/v1/api", json);
System.out.println(response);
} catch (IOException e) {
// 打印错误日志
}
}
}
上述代码看似很正确,非常专业地在 finally 中释放资源,然而你仔细读读代码,可能会发现问题。
下面给你 2 分钟的时间思考一下,存在什么问题。
[2 分钟]
如果你还看不出来原因,那么请你说出下面代码运行的结果:
public class CatDemo {
public static void main(String[] args) {
Cat cat = new Cat();
cat.setName("tom");
System.out.println(cat);
}
private void test(Cat cat){
cat = new Cat();
cat.setName("cat");
}
}
public class Cat {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Cat{" +
"name='" + name + '\'' +
'}';
}
}
答案是:
Cat{name='tom'}
如果到这里还搞不明白,说明基础有问题,需要加强了。
在Java中,所有的方法参数都是按值传递的。这意味着当你传递一个变量给一个方法时,你实际上是传递了一个这个变量的复制品。
但我们需要区分两种情况:传递原始类型的变量和传递对象引用变量。
对于原始类型(如int
, double
, char
等),按值传递很直观。你创建了一个原始类型的变量,当你将其传递给一个方法时,方法接收到的是一个新的变量,它包含的是原始变量的一个副本。
如果方法修改了这个新变量,它不会影响原始变量。
示例:
public class Main {
public static void main(String[] args) {
int a = 5;
modify(a);
System.out.println(a); // 输出:5
}
public static void modify(int number) {
number = 10;
}
}
在上述代码中,虽然modify
方法修改了number
变量的值,但这不影响main
方法中a
变量的值。
注:图只是为了说明问题,细节上可能未必合理,如果有出入请勿较真。
对于对象引用变量,情况略有不同。虽然是按值传递,但传递的是对象引用的值,而不是对象本身。这意味着方法接收到的是原始对象引用的一个副本。因此,该方法可以通过这个引用来修改原始对象的状态。
但是,如果该方法试图将新的对象赋值给它的对象引用变量,这不会影响原始对象引用变量,因为它只修改了副本的指向,而不是原始引用的指向。
示例:
public class Main {
public static void main(String[] args) {
Cat cat = new Cat();
cat.setName("Tom");
modifyReference(cat);
System.out.println(cat.getName()); // 输出:Tom
modifyObject(cat);
System.out.println(cat.getName()); // 输出:Jerry
}
public static void modifyReference(Cat cat) {
cat = new Cat();
cat.setName("Spike");
}
public static void modifyObject(Cat cat) {
cat.setName("Jerry");
}
static class Cat {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
在上述代码中:
modifyReference
方法试图修改cat
引用变量的指向,但这不影响main
方法中的cat
变量。modifyObject
方法通过cat
引用变量来修改Cat
对象的状态,这实际上影响了main
方法中的cat
对象。因此,在示例一中:
private SomeResult someMethod(Response response, Request request) throws IOException {
response = client.newCall(request).execute();
// 其他逻辑
return SomeUtils.toSomeResult(response);
}
client.newCall(request).execute();
创建了新的实例赋值给对象引用变量 response
,只是修改了副本指向了新的对象而已。
因此 :
Response response = null;
try{
return someMethod(response,request);
}finally {
// 永远为 null,从来没有调用到 close 方法
if(response != null){
response.close();
}
}
public static void modifyObject(Cat cat) {
cat.setName("Jerry");
}
该方法中 cat 引用副本也指向 cat 对象,可以修改其名称。
public static void modifyReference(Cat cat) {
cat = new Cat();
cat.setName("Spike");
}
该方法则和示例1 的情况类似,引用副本指向了新的对象,并不会影响到原始的引用。
知道原因,修改起来就很容易了。
推荐的做法:
try (Response response = client.newCall(request).execute()) {
if(response.success() && response.body() != null){
return SomeUtils.toSomeResult(response.body().string()) ;
}
return null;
}
也可以在 someMethod 内部使用 finally 释放资源。
其实只要不对 IDEA 的警告视而不见,这种问题基本都可以避免。
有两个非常明显的提示,一个是 if 这里警告说条件 always flase ,就需要分析为什么。
调用时也提示 responds always null。
还有 someMethod 方法中 responds 是灰色,意味着传入引用没有意义,可以定义成局部变量。
很多人讨厌面试中问一些“八股文”,认为这是在浪费时间。
然而,实际工作中,很多问题都是因为所谓的八股文并没有掌握好才出的问题。
一个是对于实现了 Closeable
接口的类,推荐使用 try-with-resource 的方式使用和自动释放资源。
写代码时,如果没有必要,请不要定义一个空对象作为参数传到下游,最后再取出对象中的值来使用。如果确实需要这么做,建议使用 Holder 类 或者上下文类。
虽然这个问题并不难,但工作中还是会见到很多类似的错误。
希望加大能够养成良好的编码习惯,希望能够真正做到知行合一、学以致用。
创作不易,如果本文对你有帮助,欢迎点赞、收藏加关注,你的支持和鼓励,是我创作的最大动力。
欢迎加入我的知识星球,知识星球ID:15165241 (已经营五年多,会持续经营)一起交流学习。
https://t.zsxq.com/Z3bAiea 申请时标注来自CSDN。