希望你不要经历的那些坑:你确定资源正确释放了?

作者:明明如月学长, CSDN 博客专家,大厂高级 Java 工程师,《性能优化方法论》作者、《解锁大厂思维:剖析《阿里巴巴Java开发手册》》、《再学经典:《EffectiveJava》独家解析》专栏作者。

热门文章推荐

  • (1)《为什么很多人工作 3 年 却只有 1 年经验?》
  • (2)《从失望到精通:AI 大模型的掌握与运用技巧》
  • (3)《AI 时代,程序员的出路在何方?》
  • (4)《如何写出高质量的文章:从战略到战术》
  • (5)《我的技术学习方法论》
  • (6)《我的性能方法论》
  • (7)《AI 时代的学习方式: 和文档对话》
  • (8)《人工智能终端来了,你还在用过时的 iterm?》

希望你不要经历的那些坑:你确定资源正确释放了?_第1张图片

一、背景

最近对某段代码进行代码审查,无意间发现一个哭笑不得的“神操作”!
该同学代码中用最标准的释放资源的方法,可是并没有正确释放资源。
本文将模拟该问题,讲述背后的原因,希望大家编码时要特别注意该问题。

二、场景复现

2.1 示例1

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 分钟]

2.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变量的值。

希望你不要经历的那些坑:你确定资源正确释放了?_第2张图片

注:图只是为了说明问题,细节上可能未必合理,如果有出入请勿较真。

传递对象引用变量

对于对象引用变量,情况略有不同。虽然是按值传递,但传递的是对象引用的值,而不是对象本身。这意味着方法接收到的是原始对象引用的一个副本。因此,该方法可以通过这个引用来修改原始对象的状态。

但是,如果该方法试图将新的对象赋值给它的对象引用变量,这不会影响原始对象引用变量,因为它只修改了副本的指向,而不是原始引用的指向。

示例:

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对象。

希望你不要经历的那些坑:你确定资源正确释放了?_第3张图片

四、揭晓答案

4.1 示例1

因此,在示例一中:

    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();
            }
        }

4.1 示例2

    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 释放资源。

六、启示

6.1 关注 IDEA 警告

其实只要不对 IDEA 的警告视而不见,这种问题基本都可以避免。
希望你不要经历的那些坑:你确定资源正确释放了?_第4张图片
有两个非常明显的提示,一个是 if 这里警告说条件 always flase ,就需要分析为什么。

调用时也提示 responds always null。
希望你不要经历的那些坑:你确定资源正确释放了?_第5张图片
还有 someMethod 方法中 responds 是灰色,意味着传入引用没有意义,可以定义成局部变量。
希望你不要经历的那些坑:你确定资源正确释放了?_第6张图片

6.2 基础不牢,地动山摇

很多人讨厌面试中问一些“八股文”,认为这是在浪费时间。
然而,实际工作中,很多问题都是因为所谓的八股文并没有掌握好才出的问题。

6.3 遵循最佳实践

一个是对于实现了 Closeable 接口的类,推荐使用 try-with-resource 的方式使用和自动释放资源。
写代码时,如果没有必要,请不要定义一个空对象作为参数传到下游,最后再取出对象中的值来使用。如果确实需要这么做,建议使用 Holder 类 或者上下文类。

七、总结

虽然这个问题并不难,但工作中还是会见到很多类似的错误。
希望加大能够养成良好的编码习惯,希望能够真正做到知行合一、学以致用。


创作不易,如果本文对你有帮助,欢迎点赞、收藏加关注,你的支持和鼓励,是我创作的最大动力。
在这里插入图片描述

欢迎加入我的知识星球,知识星球ID:15165241 (已经营五年多,会持续经营)一起交流学习。
https://t.zsxq.com/Z3bAiea 申请时标注来自CSDN。

你可能感兴趣的:(问题积累,java)