73、Java I/O流:字节流与字符流详解

Java I/O流:字节流与字符流详解

1. Java I/O流概述

Java的 java.io 包主要包含字符流和字节流两大部分。字符采用16位的UTF - 16编码,而字节则是8位。I/O操作可分为基于文本和基于数据(二进制)两种类型。基于文本的I/O处理人类可读的字符流,如程序的源代码;基于数据的I/O处理二进制数据流,如图像的位模式。字符流用于基于文本的I/O,字节流用于基于数据的I/O。字节流无法正确处理字符,一些与字符相关的问题在字节流中也没有意义,不过字节流也可用于使用7位或8位字符的旧文本协议。

字节流分为输入流和输出流,字符流则称为读取器(Reader)和写入器(Writer)。几乎每个输入流都有对应的输出流,大多数输入或输出流都有功能相似的对应的字符流读取器或写入器,反之亦然。

java.io 中的类和接口大致可分为以下五组:
- 用于构建不同类型的字节和字符流(输入和输出流、读取器和写入器)以及它们之间的转换类。
- 定义各种类型的流,如过滤流、缓冲流、管道流,以及这些流的一些特定实例,如行号读取器和流标记器。
- 用于读写基本值和字符串的数据流类和接口。
- 以系统无关的方式与文件交互的类和接口。
- 构成对象序列化机制的类和接口,该机制将对象转换为字节流,并允许从字节流读取的数据中重新构造对象。

部分输出流提供了使用 java.util.Formatter 类实例生成格式化输出的便捷方法。通过将输入流绑定到 java.util.Scanner 对象,可以实现格式化输入。

IOException 类被 java.io 中的许多方法用于表示异常情况。 IOException 的一些扩展类表示特定问题,但大多数问题由带有描述性字符串的 IOException 对象表示。当发生与流直接相关的错误时,任何抛出 IOException 的方法都会抛出该异常。特别是在已关闭的流上调用方法可能会导致 IOException

同样,当将空引用传递给方法,或提供的索引超出数组范围时,可能会抛出 NullPointerException IndexOutOfBoundsException 。只有不发生这种情况的情况才会明确记录。

所有代码示例都使用 java.io 中的类型,即使代码中没有显式的导入语句,每个示例也都导入了 java.io.*

下面是Java I/O流的分类总结表格:
| 分类 | 描述 |
| ---- | ---- |
| 构建流类 | 构建不同类型的字节和字符流及转换类 |
| 定义流类型类 | 过滤流、缓冲流等及特定实例 |
| 数据流类 | 读写基本值和字符串 |
| 文件交互类 | 系统无关方式与文件交互 |
| 对象序列化类 | 对象与字节流转换及重构 |

2. 字节流

2.1 字节流基础

java.io 包定义了基本字节输入和输出流的抽象类,这些抽象类被扩展以提供多种有用的流类型。流类型通常是成对出现的,例如,有用于从文件读取的 FileInputStream ,通常就有用于向文件写入的 FileOutputStream

在学习具体的输入和输出字节流之前,理解基本的 InputStream OutputStream 抽象类非常重要。所有字节流都有一些共同特点:
- 所有流都支持打开和关闭的概念。创建流时将其打开,打开状态下可以进行读写操作。可以使用 Closeable 接口中定义的 close 方法关闭流。关闭流会释放流可能使用的资源(如文件描述符),当这些资源不再需要时应尽快回收。如果没有显式关闭流,它将一直占用这些资源。虽然流类可以定义 finalize 方法在垃圾回收期间释放这些资源,但可能会太晚。因此,通常在使用完流后应关闭它们。
- 所有字节流还共享共同的同步策略和并发行为。

2.2 InputStream

抽象类 InputStream 声明了从特定源读取字节的方法,它是 java.io 中大多数字节输入流的超类,具有以下方法:
- public abstract int read() :读取单个字节的数据,并将读取的字节作为0到255范围内的整数返回,而不是 - 128到127。如果到达流的末尾没有可用字节,则返回 - 1。该方法会阻塞,直到有输入可用、到达流的末尾或抛出异常。返回 int 类型是因为需要返回所有有效的字节值以及一个标志值来表示流的结束。
- public int read(byte[] buf, int offset, int count) :读取到字节数组的一部分。最多读取 count 个字节,字节从 buf[offset] 开始存储,最多存储到 buf[offset + count - 1] buf 中的其他值保持不变。返回实际读取的字节数。如果到达流的末尾没有读取到字节,则返回 - 1。如果 count 为0,则不读取字节并返回0。该方法会阻塞,直到有输入可用、到达流的末尾或抛出异常。如果由于除到达流的末尾之外的任何原因无法读取第一个字节,特别是如果流已经关闭,则抛出 IOException 。一旦读取了一个字节,在尝试读取后续字节时发生的任何失败都不会以异常形式报告,而是视为遇到流的末尾,方法正常完成并返回失败发生前读取的字节数。
- public int read(byte[] buf) :等同于 read(buf, 0, buf.length)
- public long skip(long count) :跳过最多 count 个字节的输入,直到到达流的末尾。返回实际跳过的字节数。如果 count 为负数,则不跳过任何字节。
- public int available() :返回可以无阻塞读取(或跳过)的字节数。默认实现返回0。
- public void close() :关闭输入流。调用此方法以释放与流关联的任何资源(如文件描述符)。一旦流关闭,对该流的进一步操作将抛出 IOException 。关闭已关闭的流没有效果。默认实现不执行任何操作。

以下是一个使用输入流计算文件中字节总数的示例程序:

import java.io.*;
class CountBytes {
    public static void main(String[] args)
        throws IOException
    {
        InputStream in;
        if (args.length == 0)
            in = System.in;
        else
            in = new FileInputStream(args[0]);
        int total = 0;
        while (in.read() != -1)
            total++;
        System.out.println(total + " bytes");
    }
}

该程序从命令行获取文件名。如果未提供文件名,则使用标准输入流 System.in ;如果提供了文件名,则创建一个 FileInputStream 对象。 while 循环计算文件中的字节总数,并在最后打印结果。

2.3 OutputStream

抽象类 OutputStream 类似于 InputStream ,它提供了向目标写入字节的抽象。其方法如下:
- public abstract void write(int b) :将 b 作为字节写入。字节以 int 类型传递,因为它通常是对字节进行算术运算的结果。涉及字节的表达式类型为 int ,因此将参数设为 int 意味着结果可以直接传递而无需转换为字节。但实际上只写入整数的最低8位。该方法会阻塞,直到字节写入完成。
- public void write(byte[] buf, int offset, int count) :写入字节数组的一部分,从 buf[offset] 开始,写入 count 个字节。该方法会阻塞,直到字节写入完成。
- public void write(byte[] buf) :等同于 write(buf, 0, buf.length)
- public void flush() :刷新流。如果流缓冲了来自各种 write 方法的任何字节, flush 会立即将它们写入目标。如果目标是另一个流,也会刷新该流。一次 flush 调用将刷新流链中的所有缓冲区。如果流没有缓冲, flush 可能不执行任何操作(默认实现)。该方法在 Flushable 接口中定义。
- public void close() :关闭输出流。调用此方法以释放与流关联的任何资源(如文件描述符)。一旦流关闭,对该流的进一步操作将抛出 IOException 。关闭已关闭的流没有效果。默认实现不执行任何操作。

以下是一个将输入复制到输出,并将特定字节值转换为另一个值的程序:

import java.io.*;
class TranslateByte {
    public static void main(String[] args)
        throws IOException
    {
        byte from = (byte) args[0].charAt(0);
        byte to   = (byte) args[1].charAt(0);
        int b;
        while ((b = System.in.read()) != -1)
            System.out.write(b == from ? to : b);
    }
}

例如,如果调用程序 java TranslateByte b B 并输入文本 abracadabra! ,将得到输出 aBracadaBra!

通常,在读取流数据后或写入流数据前对数据进行操作,可通过编写过滤流来实现,而不是在程序中硬编码操作。

2.4 字节流操作流程

graph TD;
    A[创建输入流] --> B[打开流];
    B --> C{是否有数据可读};
    C -- 是 --> D[读取数据];
    D --> E[处理数据];
    E --> C;
    C -- 否 --> F[关闭输入流];
    G[创建输出流] --> H[打开流];
    I[准备数据] --> J[写入数据];
    J --> K[刷新流];
    K --> L[关闭输出流];

3. 字符流

3.1 字符流基础

用于读写字符流的抽象类是 Reader Writer ,它们各自支持的方法与对应的字节流( InputStream OutputStream )类似。例如, InputStream read 方法返回一个字节作为 int 的最低8位,而 Reader read 方法返回一个字符作为 int 的最低16位。 OutputStream 有写入字节数组的方法, Writer 有写入字符数组的方法。字符流是在字节流之后设计的,以提供对Unicode字符的全面支持,并且在设计过程中改进了类的契约,使其更易于使用。

与字节流一样,字符流也应显式关闭以释放与流关联的资源。字符流的同步策略也有相关规定。

3.2 Reader

抽象类 Reader 提供了类似于字节流 InputStream 的字符流,其方法如下:
- public int read() :读取单个字符并将其作为0到65535范围内的整数返回。如果到达流的末尾没有可用字符,则返回 - 1。该方法会阻塞,直到有输入可用、到达流的末尾或抛出异常。
- public abstract int read(char[] buf, int offset, int count) :读取到字符数组的一部分。最多读取 count 个字符,读取的字符从 buf[offset] 开始存储,最多存储到 buf[offset + count - 1] buf 中的其他值保持不变。返回实际读取的字符数。如果到达流的末尾没有读取到字符,则返回 - 1。如果 count 为0,则不读取字符并返回0。该方法会阻塞,直到有输入可用、到达流的末尾或抛出异常。如果由于除到达流的末尾之外的任何原因无法读取第一个字符,特别是如果流已经关闭,则抛出 IOException 。一旦读取了一个字符,在尝试读取后续字符时发生的任何失败都不会以异常形式报告,而是视为遇到流的末尾,方法正常完成并返回失败发生前读取的字符数。
- public int read(char[] buf) :等同于 read(buf, 0, buf.length)
- public int read(java.nio.CharBuffer buf) :尝试将尽可能多的字符读取到指定的字符缓冲区中,而不溢出。返回实际读取的字符数。如果到达流的末尾没有读取到字符,则返回 - 1。这等同于读取到与缓冲区可用容量相同长度的数组,然后将数组复制到缓冲区。该方法在 java.lang.Readable 接口中定义,在 InputStream 中没有对应方法。
- public long skip(long count) :跳过最多 count 个字符的输入,直到到达流的末尾。返回实际跳过的字符数。 count 的值不能为负数。
- public boolean ready() :如果流准备好读取,则返回 true ,即至少有一个字符可用。注意,返回 false 并不保证下一次调用 read 会阻塞,因为在调用时可能已经有数据可用。
- public abstract void close() :关闭流。调用此方法以释放与流关联的任何资源(如文件描述符)。一旦流关闭,对该流的进一步操作将抛出 IOException 。关闭已关闭的流没有效果。

以下是一个计算字符流中空白字符数量的示例程序:

import java.io.*;
class CountSpace {
    public static void main(String[] args)
        throws IOException
    {
        Reader in;
        if (args.length == 0)
            in = new InputStreamReader(System.in);
        else
            in = new FileReader(args[0]);
        int ch;
        int total;
        int spaces = 0;
        for (total = 0; (ch = in.read()) != -1; total++) {
            if (Character.isWhitespace((char) ch))
                spaces++;
        }
        System.out.println(total + " chars, "
            + spaces + " spaces");
    }
}

该程序从命令行获取文件名。如果未提供文件名,则使用标准输入流 System.in ,并通过 InputStreamReader 将其转换为字符输入流;如果提供了文件名,则创建 FileReader 对象。 for 循环计算文件中的字符总数和空白字符数量,并在最后打印结果。

3.3 Writer

抽象类 Writer 提供了类似于 OutputStream 的流,但设计用于处理字符而不是字节,其方法如下:
- public void write(int ch) :将 ch 作为字符写入。字符以 int 类型传递,但只写入整数的最低16位。该方法会阻塞,直到字符写入完成。
- public abstract void write(char[] buf, int offset, int count) :写入字符数组的一部分,从 buf[offset] 开始,写入 count 个字符。该方法会阻塞,直到字符写入完成。
- public void write(char[] buf) :等同于 write(buf, 0, buf.length)
- public void write(String str, int offset, int count) :从字符串 str 中写入 count 个字符到流中,从 str.charAt(offset) 开始。
- public void write(String str) :等同于 write(str, 0, str.length())
- public abstract void flush() :刷新流。如果流缓冲了来自各种 write 方法的任何字符, flush 会立即将它们写入目标。如果目标是另一个流,也会刷新该流。一次 flush 调用将刷新流链中的所有缓冲区。如果流没有缓冲, flush 可能不执行任何操作。
- public abstract void close() :关闭流,必要时刷新。调用此方法以释放与流关联的任何资源(如文件描述符)。一旦流关闭,对该流的进一步操作将抛出 IOException 。关闭已关闭的流没有效果。

Writer 的子类必须实现写入数组的 write 方法、 close 方法和 flush 方法,其他方法都基于这三个方法实现。与 OutputStream 不同, OutputStream 使用单字节的 write 方法作为基本写入方法,并提供 flush close 的默认实现。与 Reader 一样,许多子类也可以通过重写其他方法来提高性能。

Writer 还实现了 java.lang.Appendable 接口, append(char c) 方法等同于 write(c) ,接受 CharSequence append 方法等同于将 CharSequence 对象的字符串表示传递给 write(String str) 方法。

3.4 字符流与标准流

标准流 System.in System.out System.err 在字符流发明之前就已存在,因此这些流是字节流,尽管从逻辑上讲它们应该是字符流。这种情况会导致一些异常。例如,无法用 LineNumberReader 替换 System.in 来跟踪标准输入流的当前行号。可以通过将 InputStreamReader (将字节输入流转换为字符输入流的对象)附加到 System.in 来创建 LineNumberReader 对象以跟踪当前行号,但 System.in InputStream 类型,不能用 LineNumberReader (一种 Reader 类型,而不是 InputStream )替换它。

System.out System.err PrintStream 对象, PrintStream 已被其基于字符的等效版本 PrintWriter 所取代。通常,应避免直接创建 PrintStream 对象。

3.5 字符流操作流程

graph TD;
    M[创建读取器] --> N[打开流];
    N --> O{是否有字符可读};
    O -- 是 --> P[读取字符];
    P --> Q[处理字符];
    Q --> O;
    O -- 否 --> R[关闭读取器];
    S[创建写入器] --> T[打开流];
    U[准备字符数据] --> V[写入字符];
    V --> W[刷新流];
    W --> X[关闭写入器];

4. 总结

Java的I/O流提供了强大而灵活的机制来处理不同类型的数据。字节流适用于处理二进制数据,而字符流更适合处理文本数据。在使用流时,需要注意流的打开和关闭,以避免资源泄漏。同时,了解各种流的方法和特点,能够根据具体需求选择合适的流来完成数据的读写操作。无论是处理文件、网络数据还是标准输入输出,I/O流都能发挥重要作用。通过编写示例程序和进行相关练习,可以更好地掌握Java I/O流的使用。

5. 字节流与字符流的对比

5.1 功能对比

字节流和字符流在功能上有明显的区别和联系,具体对比如下表格所示:
| 对比项 | 字节流 | 字符流 |
| ---- | ---- | ---- |
| 处理数据类型 | 二进制数据 | 文本数据(Unicode字符) |
| 基本单位 | 字节(8位) | 字符(16位UTF - 16) |
| 主要抽象类 | InputStream OutputStream | Reader Writer |
| 读取方法返回类型 | 字节以 int 表示(0 - 255) | 字符以 int 表示(0 - 65535) |
| 写入方法参数类型 | int (实际写最低8位)、 byte[] | int (实际写最低16位)、 char[] String |

从表格中可以看出,字节流更侧重于处理二进制数据,如图片、视频等文件;而字符流则专门用于处理文本数据,能更好地支持Unicode字符。

5.2 性能对比

在性能方面,字节流和字符流也有所不同。字节流在处理二进制数据时,由于直接操作字节,没有字符编码和解码的过程,所以通常速度较快。而字符流在处理文本数据时,会进行字符编码和解码操作,这会带来一定的性能开销。但如果处理的是大量的文本数据,使用字符流可以避免手动进行字符编码和解码,提高开发效率。

5.3 适用场景对比

根据功能和性能的特点,字节流和字符流有不同的适用场景:
- 字节流适用场景
- 处理二进制文件,如图片、音频、视频等。
- 与网络进行数据交互,网络传输的数据通常是二进制形式。
- 对性能要求较高,且不需要处理字符编码的场景。
- 字符流适用场景
- 处理文本文件,如代码文件、配置文件等。
- 需要进行字符编码和解码的场景,如读取不同编码的文本文件。
- 处理标准输入输出,将字节流转换为字符流更方便处理文本信息。

6. 异常处理与资源管理

6.1 异常处理

在使用Java I/O流时, IOException 是常见的异常类型。许多 java.io 中的方法会抛出 IOException 来表示与流操作相关的异常情况。例如,在关闭的流上进行读写操作、文件不存在等情况都可能导致 IOException

为了确保程序的健壮性,需要对 IOException 进行适当的处理。通常可以使用 try - catch 块来捕获和处理异常,示例代码如下:

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

public class ExceptionHandlingExample {
    public static void main(String[] args) {
        InputStream in = null;
        try {
            in = new FileInputStream("example.txt");
            int b;
            while ((b = in.read()) != -1) {
                // 处理数据
            }
        } catch (IOException e) {
            System.err.println("发生I/O异常: " + e.getMessage());
        } finally {
            try {
                if (in != null) {
                    in.close();
                }
            } catch (IOException e) {
                System.err.println("关闭流时发生异常: " + e.getMessage());
            }
        }
    }
}

在上述代码中,使用 try - catch 块捕获 IOException ,并在 finally 块中确保流被关闭,即使在发生异常的情况下也能释放资源。

6.2 资源管理

在Java 7及以后的版本中,引入了 try - with - resources 语句来简化资源管理。实现了 AutoCloseable 接口的类(如 InputStream OutputStream Reader Writer 等)可以使用 try - with - resources 语句自动关闭资源。示例代码如下:

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

public class ResourceManagementExample {
    public static void main(String[] args) {
        try (InputStream in = new FileInputStream("example.txt")) {
            int b;
            while ((b = in.read()) != -1) {
                // 处理数据
            }
        } catch (IOException e) {
            System.err.println("发生I/O异常: " + e.getMessage());
        }
    }
}

try - with - resources 语句中,声明的资源会在 try 块执行完毕后自动关闭,无需手动在 finally 块中关闭,减少了代码量,也降低了资源泄漏的风险。

6.3 异常处理与资源管理流程

graph TD;
    A[创建流对象] --> B[使用try - catch或try - with - resources];
    B --> C{是否使用try - with - resources};
    C -- 是 --> D[自动管理资源];
    C -- 否 --> E[手动管理资源];
    D --> F{是否发生异常};
    E --> F;
    F -- 是 --> G[捕获并处理异常];
    F -- 否 --> H[正常执行操作];
    G --> I[关闭资源];
    H --> I;
    I --> J[结束操作];

7. 过滤流与装饰器模式

7.1 过滤流概述

在处理流数据时,经常需要对数据进行一些额外的操作,如加密、压缩、缓冲等。过滤流就是为了实现这些功能而设计的。过滤流是一种包装流,它可以包装其他流(如字节流或字符流),并在数据的读取或写入过程中进行额外的处理。

7.2 装饰器模式

过滤流的实现基于装饰器模式。装饰器模式是一种结构型设计模式,它允许向一个现有的对象添加新的功能,同时又不改变其结构。在Java I/O流中,过滤流类(如 FilterInputStream FilterOutputStream FilterReader FilterWriter )就是装饰器,它们可以包装其他流对象,为其添加额外的功能。

7.3 示例:缓冲流

缓冲流是过滤流的一种常见应用,它可以提高流的读写性能。 BufferedInputStream BufferedOutputStream 用于字节流的缓冲, BufferedReader BufferedWriter 用于字符流的缓冲。示例代码如下:

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

public class BufferedStreamExample {
    public static void main(String[] args) {
        try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("input.txt"));
             BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("output.txt"))) {
            int b;
            while ((b = bis.read()) != -1) {
                bos.write(b);
            }
        } catch (IOException e) {
            System.err.println("发生I/O异常: " + e.getMessage());
        }
    }
}

在上述代码中, BufferedInputStream 包装了 FileInputStream BufferedOutputStream 包装了 FileOutputStream 。缓冲流会在内存中创建一个缓冲区,减少了与磁盘的交互次数,从而提高了读写性能。

7.4 过滤流使用流程

graph TD;
    A[创建基础流对象] --> B[创建过滤流对象并包装基础流];
    B --> C[进行读写操作];
    C --> D[关闭过滤流(自动关闭基础流)];

8. 总结与展望

8.1 总结

本文详细介绍了Java的I/O流,包括字节流和字符流的基本概念、使用方法、异常处理、资源管理以及过滤流的应用。字节流适用于处理二进制数据,字符流适用于处理文本数据,它们各自有不同的抽象类和方法。在使用流时,需要注意流的打开和关闭,避免资源泄漏。异常处理和资源管理是保证程序健壮性的重要方面, try - with - resources 语句可以简化资源管理。过滤流基于装饰器模式,可以为流添加额外的功能,如缓冲、加密等。

8.2 展望

随着技术的不断发展,Java的I/O流也在不断改进和扩展。例如,Java NIO(New I/O)提供了更高效的非阻塞I/O操作,适用于处理大量并发连接的场景。在未来的开发中,可以根据具体需求选择合适的I/O方式,以提高程序的性能和可维护性。同时,对于不同类型的流和过滤流的组合使用,也可以进一步探索,以实现更复杂的功能。通过不断学习和实践,能够更好地掌握Java I/O流的技术,为开发高质量的Java应用程序打下坚实的基础。

总之,Java I/O流是Java编程中不可或缺的一部分,深入理解和掌握它的使用方法对于开发人员来说至关重要。希望本文能够帮助读者更好地理解和应用Java I/O流技术。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值