Java IO:Reader 类

在 Java IO 中,java.io.Reader 类扮演着至关重要的基础角色,它作为 Java IO API 中所有 Reader 子类的抽象基类,定义了字符输入流的通用方法和属性。

Reader 与 InputStream 有着相似之处,却又存在着本质的区别。Reader 专注于字符层面的处理,以字符为单位进行数据读取,非常适合处理文本内容。而InputStream 则是面向字节的,主要用于读取诸如图片、音频、视频等原始字节数据。

也就是说,当我们需要处理文本(字符)信息时,Reader是理想的选择;而对于读取原始字节数据的场景,InputStream 则更为合适。

📢注意:Reader 通常连接到某个数据源,如文件、字符数组、网络套接字等。

Unicode 字符

在当今数字化时代,众多应用程序都倾向于采用 Unicode(UTF-8 或 UTF-16)编码格式来存储文本数据。因为 UTF-8 编码具有灵活性,它表示一个字符所需的字节数是可变的,可能是一个字节,也可能是多个字节。而 UTF-16 编码相对固定,每个字符都需要 2 个字节来表示。这就意味着,在读取文本数据的过程中,数据里的单个字节未必能对应上 UTF 编码中的一个完整字符。

倘若仅仅借助 InputStream 逐字节地读取 UTF-8 编码的数据,并且直接把每个字节都转换为一个字符,那么最终得到的文本内容极有可能与预期不符。

为了有效解决这一难题,Java 提供了 Reader 类。Reader 类的核心功能在于将字节数据解码为字符数据。不过,在使用 Reader 类进行解码操作时,需要明确指定要解码的字符集。这一指定操作需要在实例化 Reader 类时完成,实际上,通常是在实例化 Reader 类的某个具体子类时进行设置。

Reader 子类

在实际编程中,你一般会选用 Reader 的子类,而非直接使用 Reader 类。Java 的输入输出(IO)体系涵盖了众多 Reader 的子类。下面为你列出常见的 Java Reader 子类:

  • InputStreamReader:将字节输入流转换为字符输入流,可指定字符编码。例如,当你从文件或网络获取字节数据,但需要以字符形式处理时,就可以用它把字节流转换成字符流。

  • CharArrayReader:用于从字符数组中读取数据。当你有一个字符数组,想将其作为输入源进行字符读取操作时,可使用此类。

  • FileReader:专门用于读取文件中的字符数据。它默认使用系统的默认字符编码,是操作文件字符输入的常用类。

  • PipedReader:与 PipedWriter 配合使用,用于线程间的字符数据通信。一个线程通过 PipedWriter 写入数据,另一个线程通过 PipedReader 读取数据。

  • BufferedReader:为其他字符输入流添加缓冲功能,提高读取效率。它可以一次读取一行数据,适合处理大文本文件。

  • FilterReader:是所有过滤字符输入流的抽象基类,用于对其他字符输入流进行过滤和增强处理。

  • PushbackReader:允许将已读取的字符推回到输入流中,以便后续再次读取。在处理一些需要回溯的字符输入场景时很有用。

  • LineNumberReader:是 BufferedReader 的子类,能跟踪行号。在读取文本时,它可以记录当前读取的行号,方便定位文本位置。

  • StringReader:用于从字符串中读取字符数据。当你有一个字符串,想将其作为输入源进行字符读取操作时,可使用此类。

以下是创建 FileReader 的示例:

Reader reader = new FileReader("D:\\data.txt");

从 Reader 中读取字符

在 Java 里,Reader 类的 read() 方法会返回一个 int 类型的值,该值代表了下一个读取到的字符的字符编码。当 read() 方法返回 -1 时,这就意味着 Reader 已经没有更多的数据可供读取了,此时可以将其关闭。需要特别注意的是,这里返回的 -1 是一个 int 类型的值,并非字节或者字符的值,这两者存在明显差异!

下面展示了如何使用 Reader 读取所有字符的示例:

package com.hxstrive.java_io;

import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;

public class FileReaderExample {
    public static void main(String[] args) throws IOException {
        try(Reader reader = new FileReader("data.txt")) {
            int theCharNum = reader.read();
            while (theCharNum != -1) {
                char theChar = (char) theCharNum;
                System.out.print(theChar);
                theCharNum = reader.read();
            }
        }
        //输出:
        //你好!欢迎光临
    }
}

注意,上面示例代码首先从 Reader 中读取一个字符,然后检查字符数值是否等于-1。如果不等于,则处理该字符并继续读取,直到从 Reader 的 read() 方法返回-1。

从 Reader 读取字符数组

Reader 类也有一个 read(char[], offset, length) 方法,该方法将字符数组、起始偏移量和长度作为参数。字符数组是 read() 方法读取字符的位置。偏移参数是 read() 方法应开始读取的字符数组的位置。长度参数是 read() 方法从偏移量开始向前读入字符数组的字符数。下面是一个使用 Reader 将字符数组读入字符数组的示例:

package com.hxstrive.java_io;

import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;

public class FileReaderExample2 {
    public static void main(String[] args) throws IOException {
        try(Reader reader = new FileReader("data.txt")) {
            // 字符数组
            char[] theChars = new char[128];
            // 读取字符到字符数组,charsRead 表示实际读取的字符数,-1 表示文件结束
            int charsRead = reader.read(theChars, 0, theChars.length);
            while(charsRead != -1) {
                System.out.println(new String(theChars, 0, charsRead));
                // 再次读取
                charsRead = reader.read(theChars, 0, theChars.length);
            }
        }
    }
}

注意,上述示例 read(char[],offset,length) 方法会返回读入字符数组的字符数,如果 Reader 中没有更多字符可读取(例如 Reader 连接的文件已读完),则返回-1。

读取性能

在数据读取操作中,相较于从 Reader 逐字符读取,一次性读取一个字符数组的方式效率更高。通过读取字符数组而非逐个字符读取,性能能够轻松提升 10 倍甚至更多。

实际的速度提升幅度会受到多种因素的影响,其中包括读取字符数组的大小,以及运行代码的计算机所使用的操作系统和硬件配置等。因此,在确定具体的字符数组大小时,您需要对目标系统的硬盘缓冲区大小等相关参数进行研究。通常来说,使用 8KB 及以上的缓冲区大小能显著提升读取速度。然而,一旦字符数组的大小超出了底层操作系统和硬件所能承受的范围,继续增大字符数组的尺寸并不会带来读取速度的进一步提升。

为了找到最适合的字符数组大小,您可以尝试使用不同字节数的数组进行读取操作,并对每次操作的性能进行测量。通过这种方式,您能够精准地确定在特定系统环境下实现最优读取性能的字符数组大小。

通过 BufferedReader 实现透明缓冲

借助 BufferedReader 类,您能够为 Reader 增添透明化、自动化的读取以及字节数组缓冲功能。BufferedReader 会从底层的 Reader 一次性读取大量字符,并将这些字符存入一个字符数组中。随后,您便可以从 BufferedReader 逐个读取字符,如此一来,相较于逐个字符读取,通过读取字符数组的方式,读取速度仍能大幅提升。以下是一个使用 BufferedReader 封装 Reader 的示例:

package com.hxstrive.java_io;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;

public class BufferedReaderExample {
    public static void main(String[] args) throws IOException {
        try(Reader reader = new BufferedReader(new FileReader("data.txt"), 1024*8)) {
            int val = reader.read();
            while(val != -1) {
                System.out.print((char)val);
                val = reader.read();
            }
        }
    }
}

注意,BufferedReader 是 Reader 的子类,可以在任何可以使用 Reader 的地方使用。

看看 BufferedReader 类的源码,如下:

public class BufferedReader extends Reader {
    private Reader in; // 底层 Reader

    private char[] cb; // 缓冲字符数组
    private int nChars, nextChar;

    private static final int INVALIDATED = -2;
    private static final int UNMARKED = -1;
    private int markedChar = UNMARKED;
    private int readAheadLimit = 0; /* Valid only when markedChar > 0 */
    //...
}

跳过字符

Reader 类有一个名为 skip() 的方法,可用于跳过输入内容中不想读取的字符数。您可以将要跳过的字符数作为参数传递给 skip() 方法。例如:

long charsSkipped = reader.skip(24);

上述示例,告诉 Reader 跳过 Reader 中的下一个 24 个字符。skip() 方法返回实际跳过的字符数。大多数情况下,跳过的字符数与您请求跳过的字符数相同,但如果 Reader 中剩余的字符数少于您请求跳过的字符数,则返回的跳过字符数可能少于您请求跳过的字符数。

关闭 Reader

从 Reader 读取完字符后,记得关闭它。关闭 Reader 的方法是调用其 close() 方法。例如:

reader.close();

你还可以使用 Java 7 中引入的 try-with-resources 结构。例如:

try(Reader reader = new BufferedReader(new FileReader("data.txt"), 1024*8)) {
    int val = reader.read();
    while(val != -1) {
        System.out.print((char)val);
        val = reader.read();
    }
}

注意,上面没有任何明确的 close() 方法调用,try-with-resources 结构会帮我们关闭 Reader。

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