• Java IO: InputStream和OutputStream


    1.InputStream

    InputStream就是Java标准库提供的最基本的输入流。它位于java.io这个包里。java.io包提供了所有同步IO的功能。

    要特别注意的一点是,InputStream并不是一个接口,而是一个抽象类,它是所有输入流的超类。这个抽象类定义的一个最重要的方法就是int read(),签名如下:

    public abstract int read() throws IOException;
    

    这个方法会读取输入流的下一个字节,并返回字节表示的int值(0~255)。如果已读到末尾,返回-1表示不能继续读取了。

    FileInputStreamInputStream的一个子类。顾名思义,FileInputStream就是从文件流中读取数据。下面的代码演示了如何完整地读取一个FileInputStream的所有字节:

    public void readFile() throws IOException {
        // 创建一个FileInputStream对象:
        InputStream input = new FileInputStream("src/readme.txt");
        for (;;) {
            int n = input.read(); // 反复调用read()方法,直到返回-1
            if (n == -1) {
                break;
            }
            System.out.println(n); // 打印byte的值
        }
        input.close(); // 关闭流
    }
    

    在计算机中,类似文件、网络端口这些资源,都是由操作系统统一管理的。应用程序在运行的过程中,如果打开了一个文件进行读写,完成后要及时地关闭,以便让操作系统把资源释放掉,否则,应用程序占用的资源会越来越多,不但白白占用内存,还会影响其他应用程序的运行。

    InputStreamOutputStream都是通过close()方法来关闭流。关闭流就会释放对应的底层资源。我们还要注意到在读取或写入IO流的过程中,可能会发生错误,例如,文件不存在导致无法读取,没有写权限导致写入失败,等等,这些底层错误由Java虚拟机自动封装成IOException异常并抛出。因此,所有与IO操作相关的代码都必须正确处理IOException

    仔细观察上面的代码,会发现一个潜在的问题:如果读取过程中发生了IO错误,InputStream就没法正确地关闭,资源也就没法及时释放。因此,我们需要用try ... finally来保证InputStream在无论是否发生IO错误的时候都能够正确地关闭:

    public void readFile() throws IOException {
        InputStream input = null;
        try {
            input = new FileInputStream("src/readme.txt");
            int n;
            while ((n = input.read()) != -1) { // 利用while同时读取并判断
                System.out.println(n);
            }
        } finally {
            if (input != null) { input.close(); }
        }
    }
    

    try ... finally来编写上述代码会感觉比较复杂,更好的写法是利用Java 7引入的新的try(resource)的语法,只需要编写try语句,让编译器自动为我们关闭资源。推荐的写法如下:

    public void readFile() throws IOException {
        try (InputStream input = new FileInputStream("src/readme.txt")) {
            int n;
            while ((n = input.read()) != -1) {
                System.out.println(n);
            }
        } // 编译器在此自动为我们写入finally并调用close()
    }
    

    实际上编译器并不会特别地为InputStream加上自动关闭。编译器只看try(resource = ...)中的对象是否实现了java.lang.AutoCloseable接口,如果实现了,就自动加上finally语句并调用close()方法。InputStreamOutputStream都实现了这个接口,因此,都可以用在try(resource)中。

    缓冲

    在读取流的时候,一次读取一个字节并不是最高效的方法。很多流支持一次性读取多个字节到缓冲区,对于文件和网络流来说,利用缓冲区一次性读取多个字节效率往往要高很多。InputStream提供了两个重载方法来支持读取多个字节:

    • int read(byte[] b):读取若干字节并填充到byte[]数组,返回读取的字节数
    • int read(byte[] b, int off, int len):指定byte[]数组的偏移量和最大填充数

    利用上述方法一次读取多个字节时,需要先定义一个byte[]数组作为缓冲区,read()方法会尽可能多地读取字节到缓冲区, 但不会超过缓冲区的大小。read()方法的返回值不再是字节的int值,而是返回实际读取了多少个字节。如果返回-1,表示没有更多的数据了。

    利用缓冲区一次读取多个字节的代码如下:

    public void readFile() throws IOException {
        try (InputStream input = new FileInputStream("src/readme.txt")) {
            // 定义1000个字节大小的缓冲区:
            byte[] buffer = new byte[1000];
            int n;
            while ((n = input.read(buffer)) != -1) { // 读取到缓冲区
                System.out.println("read " + n + " bytes.");
            }
        }
    }
    

    阻塞

    在调用InputStreamread()方法读取数据时,我们说read()方法是阻塞(Blocking)的。它的意思是,对于下面的代码:

    int n;
    n = input.read(); // 必须等待read()方法返回才能执行下一行代码
    int m = n;
    

    执行到第二行代码时,必须等read()方法返回后才能继续。因为读取IO流相比执行普通代码,速度会慢很多,因此,无法确定read()方法调用到底要花费多长时间。

    InputStream实现类

    FileInputStream可以从文件获取输入流,这是InputStream常用的一个实现类。此外,ByteArrayInputStream可以在内存中模拟一个InputStream

    import java.io.*;
    
    public class Main {
        public static void main(String[] args) throws IOException {
            byte[] data = { 72, 101, 108, 108, 111, 33 };
            try (InputStream input = new ByteArrayInputStream(data)) {
                int n;
                while ((n = input.read()) != -1) {
                    System.out.println((char)n);
                }
            }
        }
    }
    

    image-20200820102749127

    ByteArrayInputStream实际上是把一个byte[]数组在内存中变成一个InputStream,虽然实际应用不多,但测试的时候,可以用它来构造一个InputStream

    举个栗子:我们想从文件中读取所有字节,并转换成char然后拼成一个字符串,可以这么写:

    public class Main {
        public static void main(String[] args) throws IOException {
            String s;
            try (InputStream input = new FileInputStream("C:\test\README.txt")) {
                int n;
                StringBuilder sb = new StringBuilder();
                while ((n = input.read()) != -1) {
                    sb.append((char) n);
                }
                s = sb.toString();
            }
            System.out.println(s);
        }
    }
    

    要测试上面的程序,就真的需要在本地硬盘上放一个真实的文本文件。如果我们把代码稍微改造一下,提取一个readAsString()的方法:

    import java.io.*;
    
    public class Main {
        public static void main(String[] args) throws IOException {
            
            /*String s;
            try (InputStream input = new FileInputStream("C:\test\README.txt")) {
                s = readAsString(input);
            }*/
            
            byte[] data = { 72, 101, 108, 108, 111, 33 };
            try (InputStream input = new ByteArrayInputStream(data)) {
                String s = readAsString(input);
                System.out.println(s);
            }
        }
    
        public static String readAsString(InputStream input) throws IOException {
            int n;
            StringBuilder sb = new StringBuilder();
            while ((n = input.read()) != -1) {
                sb.append((char) n);
            }
            return sb.toString();
        }
    
    }
    

    对这个String readAsString(InputStream input)方法进行测试就相当简单,因为不一定要传入一个真的FileInputStream

    这就是面向抽象编程原则的应用:接受InputStream抽象类型,而不是具体的FileInputStream类型,从而使得代码可以处理InputStream的任意实现类。

    2.OutputStream

    InputStream相反,OutputStream是Java标准库提供的最基本的输出流。

    InputStream类似,OutputStream也是抽象类,它是所有输出流的超类。这个抽象类定义的一个最重要的方法就是void write(int b),签名如下:

    public abstract void write(int b) throws IOException;
    

    这个方法会写入一个字节到输出流。要注意的是,虽然传入的是int参数,但只会写入一个字节,即只写入int最低8位表示字节的部分。

    flush()方法

    InputStream类似,OutputStream也提供了close()方法关闭输出流,以便释放系统资源。要特别注意:OutputStream还提供了一个flush()方法,它的目的是将缓冲区的内容真正输出到目的地。

    为什么要有flush()?因为向磁盘、网络写入数据的时候,出于效率的考虑,操作系统并不是输出一个字节就立刻写入到文件或者发送到网络,而是把输出的字节先放到内存的一个缓冲区里(本质上就是一个byte[]数组),等到缓冲区写满了,再一次性写入文件或者网络。对于很多IO设备来说,一次写一个字节和一次写1000个字节,花费的时间几乎是完全一样的,所以OutputStream有个flush()方法,能强制把缓冲区内容输出。通常情况下,我们不需要调用这个flush()方法,因为缓冲区写满了OutputStream会自动调用它,并且,在调用close()方法关闭OutputStream之前,也会自动调用flush()方法。

    但是,在某些情况下,我们必须手动调用flush()方法。举个栗子:小明正在开发一款在线聊天软件,当用户输入一句话后,就通过OutputStreamwrite()方法写入网络流。小明测试的时候发现,发送方输入后,接收方根本收不到任何信息,怎么肥四?

    原因就在于写入网络流是先写入内存缓冲区,等缓冲区满了才会一次性发送到网络。如果缓冲区大小是4K,则发送方要敲几千个字符后,操作系统才会把缓冲区的内容发送出去,这个时候,接收方会一次性收到大量消息。解决办法就是每输入一句话后,立刻调用flush(),不管当前缓冲区是否已满,强迫操作系统把缓冲区的内容立刻发送出去。

    实际上,InputStream也有缓冲区。例如,从FileInputStream读取一个字节时,操作系统往往会一次性读取若干字节到缓冲区,并维护一个指针指向未读的缓冲区。然后,每次我们调用int read()读取下一个字节时,可以直接返回缓冲区的下一个字节,避免每次读一个字节都导致IO操作。当缓冲区全部读完后继续调用read(),则会触发操作系统的下一次读取并再次填满缓冲区。

    FileOutputStream

    FileOutputStream为例,演示如何将若干个字节写入文件流:

    public void writeFile() throws IOException {
        OutputStream output = new FileOutputStream("out/readme.txt");
        output.write(72); // H
        output.write(101); // e
        output.write(108); // l
        output.write(108); // l
        output.write(111); // o
        output.close();
    }
    

    每次写入一个字节非常麻烦,更常见的方法是一次性写入若干字节。这时,可以用OutputStream提供的重载方法void write(byte[])来实现:

    public void writeFile() throws IOException {
        OutputStream output = new FileOutputStream("out/readme.txt");
        output.write("Hello".getBytes("UTF-8")); // Hello
        output.close();
    }
    

    InputStream一样,上述代码没有考虑到在发生异常的情况下如何正确地关闭资源。写入过程也会经常发生IO错误,例如,磁盘已满,无权限写入等等。我们需要用try(resource)来保证OutputStream在无论是否发生IO错误的时候都能够正确地关闭:

    public void writeFile() throws IOException {
        try (OutputStream output = new FileOutputStream("out/readme.txt")) {
            output.write("Hello".getBytes("UTF-8")); // Hello
        } // 编译器在此自动为我们写入finally并调用close()
    }
    

    阻塞

    InputStream一样,OutputStreamwrite()方法也是阻塞的。

    OutputStream实现类

    FileOutputStream可以从文件获取输出流,这是OutputStream常用的一个实现类。此外,ByteArrayOutputStream可以在内存中模拟一个OutputStream

    import java.io.*;
    
    public class Main {
        public static void main(String[] args) throws IOException {
            byte[] data;
            try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
                output.write("Hello ".getBytes("UTF-8"));
                output.write("world!".getBytes("UTF-8"));
                data = output.toByteArray();
            }
            System.out.println(new String(data, "UTF-8"));
        }
    }
    

    image-20200820112306312

    ByteArrayOutputStream实际上是把一个byte[]数组在内存中变成一个OutputStream,虽然实际应用不多,但测试的时候,可以用它来构造一个OutputStream

    3.总结

    Java标准库的java.io.InputStream定义了所有输入流的超类:

    • FileInputStream实现了文件流输入;
    • ByteArrayInputStream在内存中模拟一个字节流输入。

    总是使用try(resource)来保证InputStream正确关闭。

    Java标准库的java.io.OutputStream定义了所有输出流的超类:

    • FileOutputStream实现了文件流输出;
    • ByteArrayOutputStream在内存中模拟一个字节流输出。

    某些情况下需要手动调用OutputStreamflush()方法来强制输出缓冲区。

    总是使用try(resource)来保证OutputStream正确关闭。

  • 相关阅读:
    服务返返回状态码详解
    LeetCode#28 Implement strStr()
    LeetCode#58 Length of Last Word
    LeetCode#66 Plus One
    spooling技术
    文件的打开与关闭
    DMA方式与通道方式
    中断向量、向量中断、向量地址
    中断响应优先级和中断处理优先级
    I/O接口
  • 原文地址:https://www.cnblogs.com/jianhaoscnu/p/13539975.html
Copyright © 2020-2023  润新知