Java关闭流的多种方法(源码级分析)

文章较长, 建议收藏观看


目录

1.基本关闭方法

上代码

分析代码:

2.进阶关闭方法try-with-resources

Automatic Resource Management (ARM)

3.最终版本Pro Plus Max(手搓代码)

*手搓代码, 写一个通用的函数, 写成静态的当成工具类便于调用

*批量关闭传进去的流对象

*最终版本Pro Plus Max 


前言:

关闭IO流等是我们操作文件的基本操作

JVM只会帮我们回收堆栈中的内存, 而对于IO流这种物理连接它无能为力, 得我们手动释放

如果不释放可能 会导致内存泄漏


1.基本关闭方法

大家平时关闭流可能是这么做的

try{
    FileInputStream fis = new FileInputStream("hello.txt");//读文件的流

    byte[] buffer = new byte[1024];
    int len;

    while((len = fis.read(buffer)) != -1){
        // 你要做的操作
    }

        // !!! 不建议的写法 !!!
    fis.close();

}catch (IOException e) {
    e.printStackTrace();
}

Q: 为什么直接 fis.close() 不建议呢?

A: 因为这么写, 程序不具有健壮性

在close()关闭流对象之前

比如说fis.read(buffer)这一行出错了

抛出的异常就会被catch语句捕获, 不再执行fis.read(buffer)下面的语句

这意味着, 我们的流并没有关闭 !


怎么解决呢?

有些同学可能会说, 那我在catch语句里面再写一次 fis.close() 不就得了

这显然不够优雅, 也不符合代码复用的原则


我们的解决方法是这样的

异常处理中, try catch finally是常见的结构

finally代码段, 是异常处理的"善后"操作, 不论是否发生异常, 最后都会执行(finally代码段)

我们把关闭资源的操作, 放到finally代码段即可


上代码

改进版本

FileInputStream fis = null;//读文件的流
try {
    fis = new FileInputStream("hello.txt");

    byte[] buffer = new byte[1024];
    int len;

    while ((len = fis.read(buffer)) != -1) {
        // 你要做的操作
    }
} catch (IOException e) {
    e.printStackTrace();
} finally {
    if (fis != null) {
        try {
            fis.close();
        } catch (IOException e) {
            e.printStackTrace();
        }   
    }
}

乍一看, 还复杂了, 其实都是很有必要的操作, 我们一行行分析

分析代码:

首先看finally代码段

程序可能会在new IO流对象的时候出错,
        if (fis != null)这里判断有没有生成IO流的对象,如果为null,说明还没有生成IO流对象, 也就不用关闭

finally {
    //程序可能会在new IO流对象的时候出错,
    //if (fis != null)这里判断有没有生成IO流的对象,如果为null,说明还没有生成IO流对象
    //也就不用关闭
    if (fis != null) {
        try {        //这里用到try语句是因为close()本身就会抛异常,正常处理即可
            fis.close();
        } catch (IOException e) {
            e.printStackTrace();
        }   
    }
}

再看看开头的代码 

FileInputStream fis = null;//读文件的流
try {
    fis = new FileInputStream("hello.txt");

这里为什么看着这么奇怪?

拿着一个空的对象进去try代码段再赋值 

因为, 我们的IO流资源 最终是要在finally里面关闭的,

如果只是像下面这样写

try{
    FileInputStream fis = new FileInputStream("hello.txt");//读文件的流

finally里面 就访问不到 , 就类似于全局变量的意思

(如果实在理解不了的同学, 可以把代码写在try里面试试, 行不通)

到这里, 我们可以说是写出了  能容纳一定错误的代码


2.进阶关闭方法try-with-resources

上面的代码已经看着近乎完美了, 为什么还有个 2.0呢?

我们举例子

Java关闭流的多种方法(源码级分析)_第1张图片

如果我们在项目中要用到这么多 流 , 都要一个个去关闭, 即使有IDEA代码补全, 是不是也有点崩溃?

finally {
    if (ps != null) {
        try {        
            ps.close();
        } catch (IOException e) {
            e.printStackTrace();
        }   
    }
    if (rs != null) {
        try {        
            rs.close();
        } catch (IOException e) {
            e.printStackTrace();
        }   
    }
    if (is != null) {
        try {        
            is.close();
        } catch (IOException e) {
            e.printStackTrace();
        }   
    }
    if (fos != null) {
        try {        
            fos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }   
    }
}

那我们该怎么办呢?


文章引用自 try-with-resources - Javapapers

Automatic Resource Management (ARM)

自动资源管理(ARM)

In Java 7, we got a nice feature to manage these resources automatically. Manage is really a hype word here, all it does is close the resources. Automatic resource management, helps to close the resources automatically.

在Java 7中,我们有一个很好的功能来自动管理这些资源。管理在这里真的是一个炒作的(笔者注: 类似于炒作)词,它所做的只是关闭资源。自动资源管理,有助于自动关闭资源。

Resource instantiation should be done within try(). A parenthesis () is introduced after try statement and the resource instantiation should happen within that paranthesis as below,

资源实例化应该在try()中完成。在try语句后面有一个小括号(),资源实例化应该发生在这个小括号内,如下所示。

try (InputStream is = new FileInputStream("test")) {
	is.read();
	...
} catch(Exception e) {
	...
} finally {
	//no need to add code to close InputStream(笔者注:不需要添加关闭InputStream的代码)
	//it's close method will be internally called(笔者注:它的关闭方法将被内部调用)
}

这意味着什么?

流资源在使用完成时将自动被关闭.

还是不太理解?

我们看代码


上文的代码:

FileInputStream fis = null;//读文件的流
try {
    fis = new FileInputStream("hello.txt");

    byte[] buffer = new byte[1024];
    int len;

    while ((len = fis.read(buffer)) != -1) {
        // 你要做的操作
    }
} catch (IOException e) {
    e.printStackTrace();
} finally {
    if (fis != null) {
        try {
            fis.close();
        } catch (IOException e) {
            e.printStackTrace();
        }   
    }
}


-----进阶代码----- 

直接把IO对象的创建放在try语句的括号里面,让Java帮我们管理它的释放

try (FileInputStream fis = new FileInputStream("hello.txt")) {
    byte[] buffer = new byte[1024];
    int len;

    while ((len = fis.read(buffer)) != -1) {
        // 你要做的操作
    }
} catch (IOException e) {
    e.printStackTrace();
}

这对比我们上文给出的代码, 是不是优雅了许多?

我们无需添加关闭InputStream输入流的代码,其close()方法会被自动调用


3.最终版本Pro Plus Max(手搓代码)

??? 还能写??

是的, 这是我前段时间做项目总结出来的

如果上面那个方法真的完美, 就不会有我现在的最终版本Pro Plus Max了(笑)

我写的是TCP文件传输程序, 源代码见我仓库:mobeiCanyue/FileMaster: Java实现TCP传输文件或消息,局域网无忧 (github.com)

DataOutputStream dos1 = null;//写文件到本地的

try (DataInputStream dis1 = new DataInputStream(socket.getInputStream()))//获取socket套接字{
    String fileName = dis1.readUTF();//1.从socket读取文件名
    File file = new File(fileName);
    dos1 = new DataOutputStream(new FileOutputStream(file));//写文件的流
    ...
}



......(由于它是在半路创建的流,所以最后只能用常规方法来释放)

if (t != null) {
try {
        t.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

上面服务器的代码逻辑就是:

0.客户端给服务器传输文件, 服务器接收(当然包括文件名等必要信息)

1.服务器接收到客户端传来的文件名

2.服务器根据文件名才能 new 一个OutputStream流对象

那么上文所述-----进阶代码----- 

的缺陷就来了:

如果我有一个读取流和写出流, 都想放到try 代码段里是做不到的......

因为写出流outputstream的创建, 要依靠读取流读inputstream取文件名的操作来完成

由于它是在半路创建的流,所以最后只能用常规方法来释放

难受了, 又绕回来了,难道真的没有办法避免了吗?

先不急

我们进入IDEA 查看  FileOutputStream 类

Java关闭流的多种方法(源码级分析)_第2张图片

右键显示关系图

Java关闭流的多种方法(源码级分析)_第3张图片

我们可以看到这么个关系

Java关闭流的多种方法(源码级分析)_第4张图片

 重点关注左边的方法 咦? closeable 是不是和close有关系 ?

果然, close()方法就是从这里来的...

Java关闭流的多种方法(源码级分析)_第5张图片

第一处翻译:

一个Closeable是一个可以被关闭的数据源或目的地。

第二处翻译: 

关闭此流并释放与之相关的任何系统资源

它还有一个父亲  AutoCloseable, 我们来看看

Java关闭流的多种方法(源码级分析)_第6张图片

我们看下翻译:

一个可以持有资源(如文件或套接字句柄)的对象,直到它被关闭。

AutoCloseable对象的close()方法在退出try-with-resources块时被自动调用,该对象已在资源规范头中声明。这种结构确保了及时释放,避免了可能发生的资源耗尽的异常和错误。
API说明。

即使基类的所有子类或实例都持有可释放的资源,基类也有可能实现 AutoCloseable,而且事实上这种情况很常见。对于必须完全通用的代码,或者当已知AutoCloseable实例需要释放资源时,建议使用try-with-resources结构。然而,当使用诸如java.util.stream.Stream这样同时支持基于I/O和非基于I/O形式的设施时,一般来说,当使用非基于I/O形式时,try-with-resources块是不必要的

关闭此资源,放弃任何基础资源。这个方法在由try-with-resources语句管理的对象上被自动调用。
虽然这个接口方法被声明为抛出Exception,但我们强烈鼓励实现者声明close方法的具体实现,以抛出更具体的异常,或者如果关闭操作不会失败,则根本不抛出异常。
关闭操作可能失败的情况需要实现者的仔细关注。我们强烈建议在抛出异常之前,放弃基础资源,并在内部将资源标记为关闭。关闭方法不太可能被多次调用,因此这可以确保资源被及时释放。此外,它还减少了资源被其他资源包裹时可能出现的问题。
我们也强烈建议这个接口的实现者不要让close方法抛出InterruptedException。这个异常与线程的中断状态相互作用,如果抑制了InterruptedException,很可能会发生运行时的错误行为。更一般地说,如果抑制一个异常会导致问题,AutoCloseable.close方法就不应该抛出它。
注意,与java.io.Closeable的close方法不同,这个close方法不需要是empotent的。换句话说,多次调用这个close方法可能会产生一些可见的副作用,而不像Closeable.close被要求在多次调用时不产生影响。然而,我们强烈建议这个接口的实现者使他们的关闭方法具有可执行性。
抛出。
异常 - 如果该资源不能被关闭

意味着, 我们JDK7.0以后, 流资源的关闭, 都和AutoCloseable挂钩了

说得再通俗一点:

直接或间接实现AutoCloseable接口的类, 都能调用close()方法

 好的, 那话不多说, 

手搓代码, 写一个通用的函数, 写成静态的当成工具类便于调用

其中的函数用到了泛型, 意味传进来的参数是实现了AutoCloseable接口的

public class NetFunction {
    public static  void closeStream(T t) {
        if (t != null) {
            try {
                t.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

这样, 即使流放不进try里面, 也可以愉快的关闭啦.

Java关闭流的多种方法(源码级分析)_第7张图片

什么?你还想再少一点...?

把所有的东西都放进去一起关闭?

比如说

NetFunction.closeStream(dos1,dos2,socket);

额, 那就再改改代码

批量关闭传进去的流对象

public static  void close(T... ts) {
        for (T t : ts) {
            if (t != null) {
                try {
                    t.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

怎么样, 是不是满足了所有要求了?

可以批量关闭传进去的流对象

是的, 但是我看编译器的时候强迫症犯了...

Java关闭流的多种方法(源码级分析)_第8张图片

 百度了一下

Java关闭流的多种方法(源码级分析)_第9张图片

 Java关闭流的多种方法(源码级分析)_第10张图片

 额, 大概就是 使用泛型的时候用可变形参会导致这个问题发生, 加个注解即可

但是我强迫症, 加个注解显然不优雅, 又想消除这一警告

最后沉吟许久, 想到了Java基础知识里面的转型...

最终版本Pro Plus Max 

public static void close(AutoCloseable... t) {
    for (AutoCloseable closeable : t) {
        if (closeable != null) {
            try {
                closeable.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}
NetFunction.close(dos1,dos2,socket);//一次性关闭好多的流 写的还很短,是不是很赞?

PS:这种最终加强方法适合于,不在try里面创建的流,也就是说需要手动关闭的,try里面自动关闭的不需要我们释放了。

如果能用第二种,就优先第二种

Java关闭流的多种方法(源码级分析)_第11张图片

如果还有更好的办法, 欢迎在评论区指出 

你可能感兴趣的:(Java,IDEA,java,开发语言,后端)