ByteArrayInputStream 类

在 Java 的 IO 体系里,ByteArrayInputStream 类是一个强大且实用的工具。它允许你把字节数组当作字节流来读取数据,也就是说,该类能够将字节数组无缝转换为 InputStream 类型。

ByteArrayInputStream 类继承自 InputStream 类,这一继承关系带来了极大的灵活性。你完全可以把 ByteArrayInputStream 对象当作普通的 InputStream 使用,在各类需要 InputStream 的场景中自由替换。

此外,ByteArrayInputStream 类自身还提供了一组特有的附加方法。在后续章节中,我们会对其中的部分方法展开详细介绍。

设想这样一种情况:你的数据以数组形式存储,但某些组件却只能处理 InputStream 类型的数据。此时,ByteArrayInputStream 就能发挥巨大的作用。它可以将字节数组封装起来,并将其转化为流,让数据能够顺利地在不同组件之间流转和处理。

ByteArrayInputStream 类常用方法如下:

  • int read()  从输入流里读取下一个字节,返回值是 0 到 255 范围内的 int 字节值。若到达流的末尾,则返回 -1。

  • int read(byte[] b, int off, int len)  尝试从输入流读取最多 len 个字节到字节数组 b 里,从偏移量 off 处开始存储。返回实际读取的字节数,若到达流的末尾,则返回 -1。

  • int available()  返回可以从输入流中读取(或跳过)的剩余字节数。

  • void reset()  将此输入流的 mark 位置重置为最后一次调用 mark 方法时设置的位置。

  • void mark(int readAheadLimit)  在此输入流中标记当前位置,后续调用 reset() 方法时可以回到这个标记位置。

  • long skip(long n)  跳过输入流中的 n 个字节。

创建 ByteArrayInputStream

若要使用 Java 的 ByteArrayInputStream,首要步骤是创建一个 ByteArrayInputStream 类的实例。在调用其构造函数时,你需要传入一个字节数组,这个数组将作为输入流的数据源。以下是创建 ByteArrayInputStream 实例的示例:

byte[] bytes = ... // 从某处获取字节数组
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);

在这个示例中,我们创建了一个 ByteArrayInputStream(字节数组输入流)实例。借助这个实例,我们能够读取传递给构造函数的字节数组中的全部字节。

此外,你还可以让 ByteArrayInputStream 仅读取给定字节数组的特定部分。具体做法是,在调用构造函数时额外传入偏移量和长度这两个参数,以此明确指定要读取字节数组的哪一部分。示例如下:

byte[] bytes = ... // 从某处获取字节数组
int offset = 20;
int length = 45;
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes, offset, length);

在这个示例里,我们创建了一个 ByteArrayInputStream(字节数组输入流)。该输入流会从字节数组中偏移量为 20 的位置开始读取数据,并且仅读取从该位置起往后的 45 个字节。

read()

你可以利用 Java 的 ByteArrayInputStream 类中的 read() 方法,从字节数组里读取字节,其操作方式与从普通 InputStream 读取字节并无二致。read() 方法会返回字节数组中的下一个字节,若已抵达字节数组(或者指定的字节数组部分)的末尾,该方法则会返回 -1。

以下是一个从 ByteArrayInputStream 读取字节的示例:

int data = byteArrayInputStream.read();
while(data != -1) {
    //对数据进行一些处理
    data = byteArrayInputStream.read();
    System.out.println(data);
}

available()

Java 中,ByteArrayInputStream 类的 available() 方法返回的是从当前流中还能读取的字节数量。对于 ByteArrayInputStream 而言,它返回的是从当前读取位置到字节数组末尾(或者指定的字节数组部分的末尾)的字节数。这个方法通常用于在读取数据之前,了解流中剩余的字节数量,从而帮助你合理规划读取操作。

下面为你呈现一个示例:

byte[] byteArray = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
ByteArrayInputStream bais = new ByteArrayInputStream(byteArray);
System.out.println(bais.available()); // 10

mark()

ByteArrayInputStream(字节数组输入流)类的 mark() 方法用于在当前字节位置设置一个内部标记,确切地说,这个标记会被设置在刚刚读取完前一个字节后的位置。

mark() 方法需要传入一个参数,该参数的作用是指定在这个标记失效之前,能够从流中读取的字节数量。在默认情况下,若没有显式地调用 mark() 方法来设置标记,ByteArrayInputStream 会将标记位置设定为 0,或者是传递给构造函数的偏移位置。

下面为你展示一个运用 mark() 方法在 ByteArrayInputStream 里设置标记的示例:

package com.hxstrive.java_io;

import java.io.ByteArrayInputStream;

public class ByteArrayInputStreamMarkExample {
    public static void main(String[] args) {
        byte[] byteArray = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        ByteArrayInputStream bais = new ByteArrayInputStream(byteArray);

        // 读取两个字节
        int firstByte = bais.read();
        int secondByte = bais.read();
        System.out.println("读取的前两个字节: " + firstByte + ", " + secondByte);
        //读取的前两个字节: 1, 2

        // 设置标记,允许在标记失效前读取 3 个字节
        bais.mark(3);

        // 再读取三个字节
        int thirdByte = bais.read();
        int fourthByte = bais.read();
        int fifthByte = bais.read();
        System.out.println("接着读取的三个字节: " + thirdByte + ", " + fourthByte + ", " + fifthByte);
        //接着读取的三个字节: 3, 4, 5

        // 回到标记位置
        bais.reset();

        // 再次读取三个字节
        thirdByte = bais.read();
        fourthByte = bais.read();
        fifthByte = bais.read();
        System.out.println("重置后再次读取的三个字节: " + thirdByte + ", " + fourthByte + ", " + fifthByte);
        //重置后再次读取的三个字节: 3, 4, 5
    }
}

reset()

reset() 方法主要用于重置字节数组的读取位置。具体而言,当调用 reset() 方法时,读取索引会被重置到 ByteArrayInputStream 上所设置的最后一个标记处(见 mark() 方法)。这就好比我们在阅读一本书时,使用书签标记了某一页,之后若想回到标记的页面继续阅读,调用 reset() 方法就能实现这一目的。

在默认情形下,要是没有显式地调用 mark() 方法来设置标记,ByteArrayInputStream 会把标记位置设定为 0,或者是传递给其构造函数的偏移位置。这意味着,若你没有手动设置标记,当调用 reset() 方法时,读取位置会回到字节数组的起始位置,或者是构造函数中指定的偏移位置开始处。

reset() 方法在实际开发中非常实用。比如,当你在处理字节数组中的数据时,可能需要对某一部分数据进行多次处理或者重新检查。此时,你可以先使用 mark() 方法设置一个标记,在完成部分处理后,若发现需要重新处理这部分数据,就可以调用 reset() 方法回到标记位置,重新读取数据进行处理。

简单示例:

byte[] bytes = "abcdef".getBytes();
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);

int data = byteArrayInputStream.read();  // 读取 'a'
    data = byteArrayInputStream.read();  // 读取 'b'
    data = byteArrayInputStream.read();  // 读取 'c'

    byteArrayInputStream.mark(1024);     // 在读取 “d” 之前设置标记
    data = byteArrayInputStream.read();  // 读取 'd'
    data = byteArrayInputStream.read();  // 读取 'e'
    data = byteArrayInputStream.read();  // 读取 'f'

    byteArrayInputStream.reset();        // 重置到 “d” 之前的标记处
    data = byteArrayInputStream.read();  // 读取 'd'
    data = byteArrayInputStream.read();  // 读取 'e'
    data = byteArrayInputStream.read();  // 读取 'f'

skip()

ByteArrayInputStream 类的 skip() 方法十分实用,借助它能够跳过底层字节数组中的指定字节数。使用时,你只需把想要跳过的字节数作为参数传递给该方法就行。

下面给出一个运用 ByteArrayInputStream 的 skip() 方法跳过若干字节的示例:

byte[] byteArray = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
ByteArrayInputStream bais = new ByteArrayInputStream(byteArray);
bais.skip(5); // 跳过5个字节
System.out.println(bais.read()); // 输出 6
System.out.println(bais.read()); // 输出 7

关闭 ByteArrayInputStream

当你使用完 ByteArrayInputStream 后,是否需要关闭它呢?从技术角度讲,ByteArrayInputStream 使用完后关闭并非必要操作。

ByteArrayInputStream 主要用于从字节数组中读取数据,其数据来源是已经存在于内存中的字节数组,并不涉及像文件、网络连接这类需要额外管理的外部资源。所以即使不调用 close() 方法,也不会造成文件句柄未释放、网络连接未断开这类资源泄漏问题。另外,ByteArrayInputStream 的 close() 方法实际上是一个空实现,调用它不会执行任何释放资源的操作,如下图:

image.png

但从良好的编程规范和代码可维护性角度出发,关闭它是值得推荐的做法。原因如下:

  • 代码一致性:在 Java 的输入输出操作中,关闭流是一种广泛遵循的编程习惯。对于大多数 InputStream 的子类,如 FileInputStream、SocketInputStream 等,关闭流是释放资源的必要步骤。如果在使用 ByteArrayInputStream 时不关闭,可能会让后续维护代码的开发者产生困惑,他们可能会误以为这里遗漏了关闭流的操作。

  • 便于代码扩展:在开发过程中,代码可能会随着需求的变化而进行修改和扩展。如果一开始就养成关闭 ByteArrayInputStream 的习惯,那么当后续需要将数据源从字节数组更换为其他需要管理资源的类型时,就不需要额外添加关闭流的代码,减少了出错的可能性。

下面为你展示一个完整的示例,其中包含了打开 ByteArrayInputStream、读取其中所有数据,最后关闭该输入流的具体操作:

byte[] bytes = "abcdef".getBytes();
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);

// 读取数据
int data = byteArrayInputStream.read();
while(data != -1) {
    data = byteArrayInputStream.read();
}

// 关闭 ByteArrayInputStream
byteArrayInputStream.close();

请注意观察 while 循环的执行逻辑,它会持续运行,直至从 ByteArrayInputStream 的 read() 方法中读取到 -1。一旦读取到 -1,这就表明已经抵达字节流的末尾,此时 while 循环会退出,随后会调用 ByteArrayInputStream 的 close() 方法,以此来关闭输入流。

不过,上述代码的健壮性存在不足。要是在从 ByteArrayInputStream 读取数据的过程中出现异常,close() 方法将永远不会被调用。这就可能导致资源无法被正确释放,特别是在涉及到其他可能需要释放资源的输入流时,问题会更加严重。

为了增强代码的健壮性,建议使用 Java 的 try-with-resources 结构。这种结构能够自动管理资源的生命周期,确保无论是否发生异常,资源都能被正确关闭。

下面为你展示一个运用 try-with-resources 结构来关闭 Java ByteArrayInputStream 的示例:

try( ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream("abcdef".getBytes()) ) {
    int data = byteArrayInputStream.read();
    while(data != -1){
        data = byteArrayInputStream.read();
    }
}

需要注意的是,ByteArrayInputStream 如今是在 try 关键字后的括号内进行声明的。这一操作向 Java 明确传达了一个信息:该 ByteArrayInputStream 实例将由 try-with-resources 结构负责管理。

当执行线程离开 try 代码块时,byteArrayInputStream 变量所代表的输入流会自动关闭。倘若在 try 代码块内部抛出异常,try-with-resources 结构会捕获该异常,先关闭 ByteArrayInputStream,再重新抛出异常。所以,当你在使用 try-with-resources 代码块处理 ByteArrayInputStream 时,能够确保 ByteArrayInputStream 最终会被关闭,有效避免资源泄漏问题。

说说我的看法
全部评论(
没有评论
关于
本网站专注于 Java、数据库(MySQL、Oracle)、Linux、软件架构及大数据等多领域技术知识分享。涵盖丰富的原创与精选技术文章,助力技术传播与交流。无论是技术新手渴望入门,还是资深开发者寻求进阶,这里都能为您提供深度见解与实用经验,让复杂编码变得轻松易懂,携手共赴技术提升新高度。如有侵权,请来信告知:hxstrive@outlook.com
公众号