那些你学了又忘的Java IO(四):字节流

2022-02-22 18:13:05 浏览数 (1)

“人生苦短,不如养狗 ”

一、概要

1. 什么是字节

  由于计算机是通过逻辑电路组成的,因而在数据在计算机中都是通过二进制的形式进行存储和通信,其中每一个二进制数都会占据存储空间的一位(即1bit)。但是单纯的二进制数据对于数据的处理来说是毫无意义的,因此在实际数据处理过程中中会按照 字节(即1Byte,1Byte=8bit) 为单位进行数据的取用。

  这里刚好引申出字节和字符的一个不同点,即字节是计算机数据存储的度量单位,而字符则是编码集中文字/符号的抽象基本单位,后者更多的是一种抽象概念。

2. 使用场景

  从上面的介绍中可以看到,凡是涉及到数据读写的地方都可以使用字节流来处理,也就是在本系列开篇中提到的文件、管道、网络以及系统的输入/输出都可以使用字节流来完成对应数据操作的处理,但是在进行文本类数据处理时使用字节流可能会导致文本出现乱码的问题(乱码问题在下一篇关于字符流的文章中会详细介绍)。

二、输入/输出字节流及使用

  在Java IO类库中,所有的字节流均是InputStream/OutputStream的子类,并且无论是输入流还是输出流的名称都会以"Stream"结尾,这是一个默认的规范。也就是说如果你想实现一个自定义的输入流或者输出流,在进行类的命名时都需要在结尾加上"Stream"。下面我们按照输入流和输出流来分别举例,通过这些例子让大家了解一些常见的字节流及其使用方式。

1. 输入流

  在开始使用具体输入流实现类之前,让我们先来回顾一下“输入字节流之父”InputStream。在之前的文章当中曾说过输入流的实际功能为读取数据,在InputStream当中提供以下三个方法进行数据的读取:

  • int read() : 该方法是用于顺序读取流当中下一个字节的数据,这里返回值返回的是下一个字节的内容(值的范围为0~255),当读取到流的最后时,此时会返回-1表示当前输入流已经读取完毕。在InputStream的一些实现类中可以看到使用了pos指针来标记当前读取的位置,每次调用read方法都会导致pos增加1;
  • int read(byte b[]) : 该方法是用于将流中的数据读取到字节数组b当中,每次读取的字节数据长度为字节数组b的长度,且每次读取的数据都会从字节数组b的开始元素进行写入。当前方法的返回值为每次读取的字节数,当读取到流的最后时,此时会返回-1;
  • int read(byte b[], int off, int len) : 该方法和上面一个方法的功能相同,只不过在这个方法中提供了更加自由的设置,开发者可以指定每次读取的字节数据长度,以及写入到目标字节数组b的开始元素下标;

  基于上面提供的读取方法,我们可以总结出一个相对普适的输入字节流的编码范式:

代码语言:javascript复制
        // 创建一个输入流,这里需要指定一个具体的子类实现,即这里的XXXInputStream
        try (InputStream inputStream = new XXXInputStream(data)) {
            // 将输入流中的第一个字节数据读取到缓冲区中
            int read = inputStream.read();
            // 如果read返回的值小于0,表示读取完毕
            while (read != -1) {
                // 对读取到的数据进行处理,这里简单的将字节数转成了字符变量进行输出
                System.out.println((char) read);
                // 继续读取下一个字节
                read = inputStream.read();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

  需要额外注意一下,在上面的编码范式中使用了Java8以后提供的try-with-resource来进行输入流的结束关闭以及异常捕获的简化处理,在之前的版本中则需要在finally代码块中显示的调用close()方法来进行流的关闭。

  借助下面两个Demo我们来感受一下输入字节流的使用过程。

ByteArrayInputStream

  针对字节数组,我们可以使用ByteArrayInputStream来完成读取操作,具体代码如下:

代码语言:javascript复制
        String data = "this is a test.";
        try (InputStream inputStream = new ByteArrayInputStream(data.getBytes())) {
            // 将输入流中的第一个字节数据读取到缓冲区中
            int read = inputStream.read();
            // 如果read返回的值小于0,表示读取完毕
            while (read != -1) {
                // 对读取到的数据进行处理
                System.out.println((char) read);
                // 继续读取下一个字节
                read = inputStream.read();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

  上面的例子实际上是按字节读取数据然后转成字符并逐个输出到控制台,在这个例子中原始数据中只包含英文字符。从处理结果来看,ByteArrayInputStream在文本类数据的处理上并没有明显的优势可言,并且在处理非英文字符时还会出现乱码的情况,有兴趣的朋友可以把测试文本换成中文就可以在控制台展示出来。当然,处理文本类数据会出现乱码问题不但只有ByteArrayInputStream会出现,所有的字节流都会发生相应的问题,这里暂时不详细说明。

  其实在实际工程开发中,ByteArrayInputStream更多的是用在将字节数组转换成输入流的场景,然后在配合组合流的概念完成输入和输出的整个流程,将原始数据最终持久化到文件或者通过网络通信发送到远程服务器上。

FileInputStream

  针对文件类数据,我们可以使用FileInputStream来完成读取操作,具体代码如下:

代码语言:javascript复制
        // 创建一个输入流
        try (InputStream inputStream = new FileInputStream("/Users/test.txt")) {
            // 将输入流中的第一个字节数据读取到缓冲区中
            int read = inputStream.read();
            // 如果read返回的值小于0,表示读取完毕
            while (read != -1) {
                // 对读取到的数据进行处理
                System.out.println((char) read);
                // 继续读取下一个字节
                read = inputStream.read();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

  从上面的代码中可以看到,FileInputStream类内部封装了关于文件访问相关的细节,使用者只需要指明文件路径就可以进行文件内容的读取。这里我们简单看下FileInputStream中关于read()的具体实现:

代码语言:javascript复制
    public int read() throws IOException {
        return read0();
    }
		// 调用本地方法
    private native int read0() throws IOException;

  在read()方法内部实际上调用的是系统提供的内地方法,其内部处理时会发生阻塞,直到读取到数据或读取到文件最后。

2. 输出流

  同上,先来回顾一下“输出字节流之父”OutputStream。对于OutputStream而言,其主要功能为写入数据,在OutputStream当中提供了以下三个方法进行数据的写入操作:

  • void write(int b) : 该方法是用于将传入参数b的低8位(也即1字节数据)写入到输出流当中,而b的高24位则会被舍弃;
  • void write(byte b[]) : 该方法是用于将指定的字节数组b写入到输入流当中;
  • void write(byte b[], int off, int len) : 和上面的方法功能类似,但该方法提供了指定被写入输入流的字节数组的起点和长度的能力,让开发者能够进行更精细话的操作;

  除了写入操作,在OutputStream当中还提供了flush()操作,即刷缓存操作。提供这一操作主要是因为在OutputStream的一些子类实现中,写入操作会先写入到缓存区域中,不会立即写入到输出流的目标数据空间,即在这一过程中会出现读写不一致的情况。通过flush()操作会立即将缓存区中的数据写入到输出流的目标数据空间。需要注意的是这一方法并不能保证数据一定能够写入到目标数据空间,其中一个特例就是文件输出流,由于底层实现是调用的操作系统提供的本地方法,在Java层面只负责将数据提供给操作系统进行对应本地方法的执行,最终是否写入到目标磁盘并不能保证。

  基于上面提供的方法,我们可以总结出一个相对普适的输出字节流编码范式:

代码语言:javascript复制
        // 测试文本
        String text = "this is a test.";
        // 创建一个输出流,这里需要指定一个具体的子类实现,即这里的XXXOutputStream
        try (OutputStream outputStream = new XXXOutputStream()) {
            // 将字符串转换为字节数组,并写入到输出流中
            outputStream.write(text.getBytes());
          	// 根据实际子类决定是否需要调用flush进行强制刷入操作
            // outputStream.flush();
            // 进行输入流的后续操作,比如将流中的输入写入到另一个对象空间
        } catch (Exception e) {
            e.printStackTrace();
        }

  下面我们借助FileOutputStream来感受一下输出字节流的使用过程。

FileOutputStream

  针对需要将字节数据写入到文件中的需求,我们可以借助FileOutputStream来完成对应的写入操作:

代码语言:javascript复制
        // 测试文本
        String text = "测试";
        // 创建一个输出流
        try (OutputStream outputStream = new FileOutputStream("/Users/suntianyu/Desktop/test.json")) {
            // 将字符串转换为字节数组,并写入到输出流中
            outputStream.write(text.getBytes());
            // 进行输入流的后续操作
        } catch (Exception e) {
            e.printStackTrace();
        }

  和FileInputStream类似,FileOutputStream封装了关于文件访问的细节提供较为简单的使用API。需要注意的是,在这个例子中我们使用了中文字符,但是实际的文件并没有出现乱码的情况,这主要是因为getBytes()方法当中在进行编码集设置时采用了JVM默认的编码集(笔者电脑中JVM默认的编码集为UTF-8),所以最终输出到文件中也是按照相同的编码集进行字节数组输出。

三、总结

  以上就是字节流的基本概念和常见的编码范式。从字节流的概念中不难发现,字节流除了在非英文文本类型数据处理的场景下表现不佳,其余所有的场景都可以很好的进行数据的读写。下一章节我们将会继续学习文本类型数据的“读写利器”——字符流。

0 人点赞