杰瑞科技汇

Java InputStream读取数据如何高效且不遗漏?

什么是 InputStream

InputStream 是一个抽象类,位于 java.io 包中,它代表了一个字节输入流,即从某个数据源(如文件、网络连接、内存数组等)中读取字节数据的流。

Java InputStream读取数据如何高效且不遗漏?-图1
(图片来源网络,侵删)

你可以把它想象成一个水管,数据源是水库,InputStream 就是连接水库和水龙头(你的程序)的水管,数据(字节)像水一样,通过这根水管流进你的程序。

核心特点:

  • 字节流:它以字节(byte)为单位进行读取,而不是字符,这使得它可以处理任何类型的数据,包括文本、图片、音频、视频等二进制文件。
  • 单向流动:数据只能从源流向程序(输入),不能反向。
  • 阻塞式:在默认情况下,读取方法是阻塞的,如果流中没有数据,read() 方法会一直等待,直到有数据可读或流关闭。

InputStream 的核心方法

InputStream 提供了几个核心的读取方法,了解它们是掌握 InputStream 的关键。

方法 描述 返回值
read() 从输入流中读取一个字节的数据,如果已到达流的末尾,则返回 -1 int (返回的字节,范围 0-255)
read(byte[] b) 从输入流中读取最多 b.length 个字节的数据到字节数组 b 中,如果已到达流的末尾,则返回 -1 int (实际读取的字节数)
read(byte[] b, int off, int len) 从输入流中读取最多 len 个字节的数据,存入字节数组 b 的从 off 开始的位置。 int (实际读取的字节数)
close() 关闭此输入流,并释放与该流关联的所有系统资源。 void
available() 返回此输入流下一个方法调用可以不受阻塞地读取(或跳过)的估计字节数。 int
skip(long n) 跳过和丢弃此输入流中数据的 n 个字节。 long (实际跳过的字节数)
reset() 将此流重新定位到最后一次对此输入流调用 mark 方法时的位置。 void
mark(int readlimit) 标记此输入流中的当前位置。 void

注意read() 方法返回的是 int 类型,而不是 byte,这是因为需要用 -1 来表示流的末尾,而 byte 类型无法表示 -1,返回的 int 值只有在 0-255 范围内才代表一个有效的字节。

Java InputStream读取数据如何高效且不遗漏?-图2
(图片来源网络,侵删)

如何使用 InputStream(标准步骤)

使用 InputStream 的标准流程遵循一个模板模式,非常重要:

  1. 创建 InputStream 对象:根据数据源的不同,创建其子类的实例(如 FileInputStream, ByteArrayInputStream 等)。
  2. 使用 try-with-resources 语句包裹:这是强烈推荐的方式,它可以自动在代码块执行完毕后关闭流,即使发生了异常,也能确保资源被正确释放,避免了资源泄漏。
  3. 读取数据:在一个循环中,调用 read() 方法读取数据,直到返回 -1(表示流结束)。
  4. 处理数据:将读取到的字节数据转换为你需要的形式(如字符串、图片等)。
  5. 关闭流:由 try-with-resources 自动完成。

代码示例

示例 1:从文件读取(最常见)

假设我们有一个名为 test.txt 的文件,内容为 "Hello, World!"。

FileInputStreamExample.java

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class FileInputStreamExample {
    public static void main(String[] args) {
        // 定义文件路径
        String filePath = "test.txt";
        // 使用 try-with-resources 语句,会自动关闭流
        try (InputStream inputStream = new FileInputStream(filePath)) {
            int byteRead;
            // read() 方法会返回一个字节,如果到达末尾则返回 -1
            System.out.println("开始读取文件内容(按字节读取):");
            while ((byteRead = inputStream.read()) != -1) {
                // 将读取到的 int 强制转换为 char 并打印
                // 注意:这种方式只适合读取纯文本文件,且编码要匹配
                System.out.print((char) byteRead);
            }
            System.out.println("\n文件读取完毕。");
        } catch (IOException e) {
            System.err.println("读取文件时发生错误: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

更高效的方式:使用 byte[] 缓冲区

Java InputStream读取数据如何高效且不遗漏?-图3
(图片来源网络,侵删)

逐个字节读取效率很低,因为每次调用 read() 都可能涉及一次 I/O 操作,更好的方式是使用字节数组作为缓冲区。

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class FileInputStreamBufferExample {
    public static void main(String[] args) {
        String filePath = "test.txt";
        // 定义一个缓冲区,大小为 1024 字节
        byte[] buffer = new byte[1024];
        try (InputStream inputStream = new FileInputStream(filePath)) {
            int bytesRead;
            System.out.println("开始读取文件内容(使用缓冲区):");
            // read(buffer) 会尝试将数据读入 buffer,返回实际读取的字节数
            while ((bytesRead = inputStream.read(buffer)) != -1) {
                // 将 buffer 中从 0 到 bytesRead 的内容转换为字符串并打印
                // 使用 String 的构造函数指定编码,避免乱码
                System.out.write(buffer, 0, bytesRead);
            }
            System.out.println("\n文件读取完毕。");
        } catch (IOException e) {
            System.err.println("读取文件时发生错误: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

示例 2:从内存中的字节数组读取 (ByteArrayInputStream)

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
public class ByteArrayInputStreamExample {
    public static void main(String[] args) {
        String sourceString = "This is a test string from memory.";
        byte[] byteArray = sourceString.getBytes(); // 将字符串转为字节数组
        // 使用 try-with-resources
        try (InputStream inputStream = new ByteArrayInputStream(byteArray)) {
            int byteRead;
            System.out.println("从内存字节数组读取内容:");
            while ((byteRead = inputStream.read()) != -1) {
                System.out.print((char) byteRead);
            }
            System.out.println("\n读取完毕。");
        } catch (IOException e) {
            // 对于 ByteArrayInputStream,这个异常通常不会发生
            e.printStackTrace();
        }
    }
}

示例 3:从网络读取 (SocketInputStream)

这是网络编程中的核心,当从 Socket 获取输入流时,你实际上是在读取从远程服务器发送过来的数据。

import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
public class NetworkInputStreamExample {
    public static void main(String[] args) {
        String hostname = "time-a.nist.gov";
        int port = 13; // Daytime protocol port
        try (Socket socket = new Socket(hostname, port);
             InputStream inputStream = socket.getInputStream()) {
            System.out.println("已连接到 " + hostname + ",正在接收时间信息...");
            byte[] buffer = new byte[1024];
            int bytesRead;
            // NIST 服务器返回的是一个 ASCII 字符串,以换行符结尾
            while ((bytesRead = inputStream.read(buffer)) != -1) {
                System.out.write(buffer, 0, bytesRead);
            }
            System.out.println("\n连接已关闭。");
        } catch (IOException e) {
            System.err.println("网络通信出错: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

InputStream 的主要子类

InputStream 是一个抽象基类,日常开发中我们通常使用它的具体子类:

子类 数据源 用途
FileInputStream 文件 从文件系统中读取文件内容。
ByteArrayInputStream 字节数组 从内存中的字节数组读取数据。
StringBufferInputStream (已过时) String String 读取字节(不推荐,使用 StringReaderByteArrayInputStream)。
PipedInputStream 管道 用于线程间的通信,读取从 PipedOutputStream 写入的数据。
SequenceInputStream 多个 InputStream 将多个输入流连接成一个输入流,按顺序读取。
FilterInputStream 其他 InputStream 抽象类,作为“装饰器”的基类,为其他输入流提供附加功能,其子类包括:
   BufferedInputStream 任何 InputStream 为输入流提供缓冲,提高读取效率。强烈推荐使用!
   DataInputStream 任何 InputStream 允许应用程序以与机器无关的方式从底层输入流中读取基本 Java 数据类型(如 int, double, boolean 等)。
ObjectInputStream 任何 InputStream 用于“反序列化”,即从流中恢复 Java 对象。
AudioInputStream 音频文件/流 读取音频格式的流数据。

最佳实践和常见问题

总是使用 try-with-resources

这是 Java 7 引入的黄金法则,它可以自动管理资源,避免因忘记调用 close() 而导致的资源泄漏。

优先使用缓冲流 (BufferedInputStream)

对于文件、网络等 I/O 操作,直接使用 FileInputStreamSocketInputStream 效率很低,将它们包装在 BufferedInputStream 中,可以大幅减少 I/O 操作次数,显著提升性能。

// 低效
try (InputStream is = new FileInputStream("large_file.zip")) { ... }
// 高效
try (InputStream is = new BufferedInputStream(new FileInputStream("large_file.zip"))) { ... }

处理中文等非 ASCII 文本

InputStream 是字节流,它不知道字符的编码,如果你用它来读取文本文件(如 .txt, .java, .xml),直接转换为 char 很可能会导致乱码。

正确做法:使用 InputStreamReader 将字节流转换为字符流,并指定正确的编码。

import java.io.*;
import java.nio.charset.StandardCharsets;
public class ReadTextWithEncoding {
    public static void main(String[] args) {
        String filePath = "test_utf8.txt"; // 假设这是一个 UTF-8 编码的文件
        try (
            // 1. 创建字节输入流
            InputStream inputStream = new FileInputStream(filePath);
            // 2. 使用 InputStreamReader 将字节流转换为字符流,并指定编码
            Reader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
            // 3. 使用 BufferedReader 提供高效的按行读取功能
            BufferedReader bufferedReader = new BufferedReader(reader)
        ) {
            String line;
            System.out.println("按行读取文件内容(UTF-8编码):");
            while ((line = bufferedReader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

二进制数据(图片、视频、PDF)用 InputStream,读文本数据InputStreamReader + BufferedReader

检查 read() 的返回值

while 循环中,必须检查 read() 的返回值是否为 -1,这是判断流是否结束的唯一标准,忘记检查会导致无限循环。


特性 描述
本质 字节输入流的抽象基类,用于从源头读取字节数据。
核心方法 read(), read(byte[]), close()
使用模式 try-with-resources + while ((read = is.read()) != -1) 循环。
性能优化 使用 BufferedInputStream 进行包装,提供缓冲机制。
文本处理 InputStream 不适合直接读文本,应通过 InputStreamReader 转换为字符流。
资源管理 务必使用 try-with-resources,确保流被正确关闭。

掌握 InputStream 是 Java I/O 编程的基石,理解了它,你就能轻松处理文件、网络等各种数据源的输入问题。

分享:
扫描分享到社交APP
上一篇
下一篇