MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java 输入输出流中节点流与处理流的搭配使用

2021-08-301.3k 阅读

Java 输入输出流概述

在 Java 编程中,输入输出(I/O)操作是非常重要的部分。输入流用于从数据源读取数据,而输出流则用于将数据写入到目的地。Java 的 I/O 流体系非常庞大且灵活,其核心概念包括节点流(Node Stream)和处理流(Filter Stream)。

Java 的 I/O 流类库位于 java.io 包中。这个包提供了大量的类来处理各种 I/O 操作,从简单的文件读写到网络通信中的数据传输。流(Stream)本质上是一个连续的字节序列,它为程序与外部设备(如文件、网络连接等)之间的数据传输提供了一种抽象。

节点流

节点流是直接与数据源或目的地连接的流。它们是 I/O 操作的基础,负责实际的数据传输。例如,FileInputStreamFileOutputStream 分别用于从文件读取数据和向文件写入数据,它们就是典型的节点流。

FileInputStream

FileInputStream 用于从文件中读取数据。以下是一个简单的示例,展示如何使用 FileInputStream 读取文件中的字节数据:

import java.io.FileInputStream;
import java.io.IOException;

public class FileInputStreamExample {
    public static void main(String[] args) {
        try (FileInputStream fis = new FileInputStream("example.txt")) {
            int data;
            while ((data = fis.read()) != -1) {
                System.out.print((char) data);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在上述代码中,首先创建了一个 FileInputStream 对象,并指定要读取的文件名为 example.txt。然后,通过 while 循环调用 read() 方法读取文件中的字节数据。read() 方法每次读取一个字节,并返回读取到的字节值,如果到达文件末尾则返回 -1。在循环中,将读取到的字节转换为字符并输出到控制台。

FileOutputStream

FileOutputStream 用于向文件中写入数据。下面的示例演示了如何使用 FileOutputStream 将数据写入文件:

import java.io.FileOutputStream;
import java.io.IOException;

public class FileOutputStreamExample {
    public static void main(String[] args) {
        String data = "Hello, World!";
        try (FileOutputStream fos = new FileOutputStream("output.txt")) {
            fos.write(data.getBytes());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在这个例子中,定义了一个字符串 data。然后创建了一个 FileOutputStream 对象,并指定要写入的文件名为 output.txt。接着,通过调用 write(byte[] b) 方法将字符串转换为字节数组并写入文件。

处理流

处理流是在节点流的基础上,对数据进行处理的流。它们不直接与数据源或目的地连接,而是“包装”在节点流之上,提供额外的功能,如缓冲、数据转换等。常见的处理流包括 BufferedInputStreamBufferedOutputStreamDataInputStreamDataOutputStream 等。

BufferedInputStream 和 BufferedOutputStream

BufferedInputStreamBufferedOutputStream 为输入输出操作提供了缓冲功能。它们内部维护一个缓冲区,减少了实际的 I/O 操作次数,从而提高了性能。

以下是使用 BufferedInputStreamBufferedOutputStream 进行文件复制的示例:

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class BufferedStreamCopyExample {
    public static void main(String[] args) {
        try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("source.txt"));
             BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("destination.txt"))) {
            int data;
            while ((data = bis.read()) != -1) {
                bos.write(data);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在这个示例中,BufferedInputStream 包装了 FileInputStreamBufferedOutputStream 包装了 FileOutputStream。通过缓冲区,每次读取和写入操作会批量处理数据,而不是逐个字节地处理,大大提高了文件复制的效率。

DataInputStream 和 DataOutputStream

DataInputStreamDataOutputStream 用于按照特定的数据类型读写数据。它们可以处理基本数据类型(如 intfloatboolean 等)以及字符串,使得数据的读写更加方便和准确。

下面的示例展示了如何使用 DataOutputStream 将数据写入文件,以及使用 DataInputStream 从文件中读取数据:

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class DataStreamExample {
    public static void main(String[] args) {
        try (DataOutputStream dos = new DataOutputStream(new FileOutputStream("data.txt"))) {
            dos.writeInt(42);
            dos.writeFloat(3.14f);
            dos.writeBoolean(true);
            dos.writeUTF("Hello, DataStream!");
        } catch (IOException e) {
            e.printStackTrace();
        }

        try (DataInputStream dis = new DataInputStream(new FileInputStream("data.txt"))) {
            int intValue = dis.readInt();
            float floatValue = dis.readFloat();
            boolean booleanValue = dis.readBoolean();
            String stringValue = dis.readUTF();

            System.out.println("Int value: " + intValue);
            System.out.println("Float value: " + floatValue);
            System.out.println("Boolean value: " + booleanValue);
            System.out.println("String value: " + stringValue);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在这个例子中,首先使用 DataOutputStream 将一个整数、一个浮点数、一个布尔值和一个字符串按照特定格式写入文件 data.txt。然后,使用 DataInputStream 从文件中按照相同的顺序和格式读取这些数据,并输出到控制台。

节点流与处理流的搭配使用原则

  1. 节点流是基础:任何 I/O 操作都需要节点流来连接数据源或目的地。处理流不能单独存在,必须依赖于节点流。例如,BufferedInputStream 必须包装在 FileInputStream 等节点流之上才能工作。
  2. 根据需求选择处理流:如果需要提高性能,可以选择缓冲流(如 BufferedInputStreamBufferedOutputStream);如果需要按照特定数据类型读写数据,则可以选择 DataInputStreamDataOutputStream。在网络编程中,还可能会用到 ObjectInputStreamObjectOutputStream 来处理对象的序列化和反序列化。
  3. 多层包装:可以对一个节点流进行多层处理流的包装,以满足复杂的需求。例如,可以先将 FileInputStream 包装在 BufferedInputStream 中以提高性能,再将其包装在 DataInputStream 中以便按数据类型读取数据。

复杂场景下的搭配使用示例

假设我们要实现一个网络聊天程序,需要在客户端和服务器之间传输各种类型的数据,包括文本消息、用户状态(布尔值)以及一些统计数据(整数)。

服务器端代码

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

public class ChatServer {
    public static void main(String[] args) {
        try (ServerSocket serverSocket = new ServerSocket(12345)) {
            System.out.println("Server started on port 12345");
            try (Socket socket = serverSocket.accept();
                 DataInputStream dis = new DataInputStream(socket.getInputStream());
                 DataOutputStream dos = new DataOutputStream(socket.getOutputStream())) {

                boolean isOnline = dis.readBoolean();
                int messageCount = dis.readInt();
                String message = dis.readUTF();

                System.out.println("User is online: " + isOnline);
                System.out.println("Message count: " + messageCount);
                System.out.println("Message: " + message);

                dos.writeUTF("Message received successfully!");
            } catch (IOException e) {
                e.printStackTrace();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在服务器端代码中,首先创建了一个 ServerSocket 并监听端口 12345。当有客户端连接时,获取与客户端通信的 Socket。然后,通过 Socket 获取输入流和输出流,并分别包装在 DataInputStreamDataOutputStream 中。这样就可以按照特定数据类型读取客户端发送的数据,并向客户端发送响应消息。

客户端代码

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;

public class ChatClient {
    public static void main(String[] args) {
        try (Socket socket = new Socket("localhost", 12345);
             DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
             DataInputStream dis = new DataInputStream(socket.getInputStream())) {

            dos.writeBoolean(true);
            dos.writeInt(5);
            dos.writeUTF("Hello, Server!");

            String response = dis.readUTF();
            System.out.println("Server response: " + response);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

客户端代码创建一个 Socket 连接到服务器的 localhost:12345。同样,将 Socket 的输出流和输入流分别包装在 DataOutputStreamDataInputStream 中。通过这些流,客户端向服务器发送布尔值、整数和字符串数据,并读取服务器返回的响应消息。

节点流与处理流搭配使用的性能分析

  1. 缓冲流的性能提升:缓冲流通过减少实际的 I/O 操作次数来提高性能。例如,对于频繁的文件读写操作,如果不使用缓冲流,每次读写一个字节都需要进行系统调用,这会带来较大的开销。而缓冲流内部维护一个缓冲区,当缓冲区满或执行刷新操作时才进行实际的 I/O 操作,大大减少了系统调用的次数。
  2. 数据类型处理流的性能影响DataInputStreamDataOutputStream 在处理基本数据类型和字符串时,虽然提供了方便的接口,但由于需要进行额外的数据转换操作,可能会在一定程度上影响性能。例如,将一个整数转换为字节序列并写入流中,或者从流中读取字节序列并转换为整数,都需要一定的计算资源。在性能要求极高的场景下,需要权衡使用这些流的必要性。
  3. 多层包装的性能权衡:多层包装处理流可以满足复杂的需求,但也会增加额外的开销。每一层处理流都需要维护自己的状态和执行一些处理逻辑。例如,在一个 FileInputStream 上先包装 BufferedInputStream 再包装 DataInputStreamBufferedInputStream 需要管理缓冲区,DataInputStream 需要进行数据类型转换。因此,在设计 I/O 流结构时,需要根据实际需求和性能要求合理选择处理流的层数和类型。

异常处理与资源管理

在使用 Java I/O 流时,异常处理和资源管理是非常重要的。由于 I/O 操作涉及与外部设备的交互,可能会出现各种错误,如文件不存在、网络连接中断等。

  1. 异常处理:常见的 I/O 异常包括 IOException 及其子类,如 FileNotFoundException 等。在编写代码时,应该使用 try-catch 块来捕获这些异常,并进行适当的处理。例如,在前面的示例中,当读取或写入文件时,如果文件不存在或出现其他 I/O 错误,try-catch 块会捕获 IOException 并打印错误堆栈信息。
  2. 资源管理:Java 7 引入了“try-with-resources”语句,大大简化了资源的管理。在“try-with-resources”语句中声明的资源会在语句结束时自动关闭,无论是否发生异常。例如,前面的示例中使用“try-with-resources”来创建 FileInputStreamFileOutputStreamBufferedInputStreamBufferedOutputStream 等流对象,这些流对象会在 try 块结束时自动关闭,无需手动调用 close() 方法。这避免了因忘记关闭资源而导致的资源泄漏问题。

不同场景下的最佳实践

  1. 文件读写:在简单的文件读写场景中,如果对性能要求不是特别高,可以直接使用 FileInputStreamFileOutputStream。但如果文件较大或读写操作频繁,建议使用 BufferedInputStreamBufferedOutputStream 进行缓冲,以提高性能。如果需要按照特定数据类型读写文件,如存储程序配置信息(包含整数、字符串等),则可以使用 DataInputStreamDataOutputStream
  2. 网络通信:在网络编程中,通常会使用 Socket 进行通信。对于网络流的处理,同样可以根据需求选择合适的处理流。例如,在传输文本数据时,可以使用 BufferedReaderBufferedWriter 进行缓冲和字符处理;如果需要传输对象,则可以使用 ObjectInputStreamObjectOutputStream 进行对象的序列化和反序列化。
  3. 数据处理与转换:当需要对数据进行处理和转换时,如将字节数据转换为特定编码的字符数据,可以使用 InputStreamReaderOutputStreamWriter。这些流可以将字节流转换为字符流,并指定字符编码。例如,在读取一个 UTF - 8 编码的文本文件时,可以将 FileInputStream 包装在 InputStreamReader 中,并指定编码为“UTF - 8”。

总结与注意事项

  1. 理解流的层次结构:深入理解节点流和处理流的概念以及它们之间的关系是正确使用 Java I/O 流的关键。节点流提供了与数据源或目的地的直接连接,而处理流则在节点流的基础上提供额外的功能。
  2. 合理选择流的类型:根据具体的应用场景和需求,合理选择节点流和处理流的类型。例如,在性能敏感的场景中,优先考虑使用缓冲流;在需要按数据类型读写的场景中,选择相应的数据类型处理流。
  3. 注意资源管理和异常处理:始终使用“try-with-resources”语句来管理流资源,以确保资源在使用后正确关闭,避免资源泄漏。同时,要妥善处理 I/O 操作中可能出现的异常,提供友好的错误提示和适当的恢复机制。

通过深入掌握 Java 输入输出流中节点流与处理流的搭配使用,开发者可以更加高效、灵活地处理各种 I/O 操作,无论是简单的文件读写还是复杂的网络通信和数据处理。