Contents

BufferedReader指南

1. 概述

BufferedReader 是一个简化从字符输入流中读取文本的类。它缓冲字符以便能够有效地读取文本数据。 在本教程中,我们将了解如何使用 BufferedReader类。

2. 何时使用BufferedReader

一般来说, 如果我们想从任何类型的输入源读取文本,无论是文件、套接字还是其他东西, BufferedReader都会派上用场。

简而言之,**它使我们能够通过读取字符块并将它们存储在内部缓冲区中来最小化 I/O 操作的数量。**当缓冲区有数据时,阅读器将从它而不是直接从底层流中读取。

2.1. 缓冲另一个阅读器

与大多数 Java I/O 类一样,BufferedReader实现了装饰器模式,这意味着它的构造函数中需要一个Reader。**通过这种方式,它使我们能够灵活地扩展 具有缓冲功能的Reader实现的实例:

BufferedReader reader = 
  new BufferedReader(new FileReader("src/main/resources/input.txt"));

但是,如果缓冲对我们来说并不重要,我们可以直接使用FileReader

FileReader reader = 
  new FileReader("src/main/resources/input.txt");

除了缓冲,BufferedReader还提供了一些不错的帮助函数来逐行读取文件。因此,尽管直接使用FileReader看起来更简单,但BufferedReader可以提供很大帮助。

2.2. 缓冲流

一般来说,我们可以配置 BufferedReader以将任何类型的输入流作为底层源。我们可以使用InputStreamReader并将其包装在构造函数中:

BufferedReader reader = 
  new BufferedReader(new InputStreamReader(System.in));

在上面的例子中,我们从System.in中读取,它通常对应于键盘的输入。类似地,我们可以传递一个输入流来读取套接字、文件或任何可以想象的文本输入类型。唯一的先决条件是它有合适的InputStream实现。

2.3. BufferedReader vs Scanner

作为替代方案,我们可以使用Scanner类来实现与BufferedReader相同的功能。

但是,这两个类之间存在显着差异,这可能使它们对我们来说或多或少方便,具体取决于我们的用例:

  • BufferedReader是同步的(线程安全的),而 Scanner 不是
  • Scanner可以使用正则表达式解析原始类型和字符串
  • BufferedReader允许更改缓冲区的大小,而 Scanner 具有固定的缓冲区大小
  • BufferedReader具有较大的默认缓冲区大小
  • Scanner隐藏IOException,而BufferedReader强制我们处理它
  • BufferedReader通常比Scanner快,因为它只读取数据而不解析它

考虑到这些,如果我们解析文件中的单个标记,那么Scanner会比BufferedReader感觉更自然一点。但是,一次只读取一行是 BufferedReader的亮点。

如果需要,我们还提供有关scanner 的指南。

3. 使用BufferedReader读取文本

让我们来看看正确构建、使用和销毁BufferReader以从文本文件中读取的整个过程。

3.1. 初始化BufferedReader

首先,让我们使用其BufferedReader(Reader) 构造函数创建一个BufferedReader

BufferedReader reader = 
  new BufferedReader(new FileReader("src/main/resources/input.txt"));

像这样包装FileReader是一种向其他阅读器添加缓冲作为方面的好方法。

默认情况下,这将使用 8 KB 的缓冲区。但是,如果我们想缓冲更小或更大的块,我们可以使用 BufferedReader(Reader, int) 构造函数:

BufferedReader reader = 
  new BufferedReader(new FileReader("src/main/resources/input.txt")), 16384);

这会将缓冲区大小设置为 16384 字节 (16 KB)。

最佳缓冲区大小取决于输入流的类型和运行代码的硬件等因素。为此,要达到理想的缓冲区大小,我们必须自己通过实验来寻找。

最好使用 2 的幂作为缓冲区大小,因为大多数硬件设备都有 2 的幂作为块大小。

最后,还有一种更方便的方法可以使用java.nio API中的Files 帮助器类来创建BufferedReader:**

BufferedReader reader = 
  Files.newBufferedReader(Paths.get("src/main/resources/input.txt"))

如果我们想读取文件,这样创建它是一种很好的缓冲方式,因为我们不必先手动创建FileReader然后包装它。

3.2. 逐行阅读

接下来,让我们使用readLine方法读取文件的内容:

public String readAllLines(BufferedReader reader) throws IOException {
    StringBuilder content = new StringBuilder();
    String line;
    
    while ((line = reader.readLine()) != null) {
        content.append(line);
        content.append(System.lineSeparator());
    }
    return content.toString();
}

我们可以更简单地使用Java 8 中引入的lines方法来做与上面相同的事情:

public String readAllLinesWithStream(BufferedReader reader) {
    return reader.lines()
      .collect(Collectors.joining(System.lineSeparator()));
}

3.3. 关闭流

使用BufferedReader之后,我们必须调用它的close() 方法来释放与之关联的任何系统资源。如果我们使用try-with-resources块,这会自动完成:

try (BufferedReader reader = 
       new BufferedReader(new FileReader("src/main/resources/input.txt"))) {
    return readAllLines(reader);
}

4. 其他有用的方法

现在让我们关注 BufferedReader 中可用的各种有用方法。

4.1. 读取单个字符

我们可以使用*read()*方法来读取单个字符。让我们逐个字符地读取整个内容,直到流结束:

public String readAllCharsOneByOne(BufferedReader reader) throws IOException {
    StringBuilder content = new StringBuilder();

    int value;
    while ((value = reader.read()) != -1) {
        content.append((char) value);
    }

    return content.toString();
}

这将读取字符(作为 ASCII 值返回),将它们转换为char并将它们附加到结果中。我们重复此操作直到流结束,这由*read()*方法的响应值 -1 指示。

4.2. 读取多个字符

如果我们想一次读取多个字符,我们可以使用方法 read(char[] cbuf, int off, int len)

public String readMultipleChars(BufferedReader reader) throws IOException {
    int length;
    char[] chars = new char[length];
    int charsRead = reader.read(chars, 0, length);
    String result;
    if (charsRead != -1) {
        result = new String(chars, 0, charsRead);
    } else {
        result = "";
    }
    return result;
}

在上面的代码示例中,我们将最多读取 5 个字符到一个 char 数组中并从中构造一个字符串。如果在我们的读取尝试中没有读取任何字符(即我们已经到达流的末尾),我们将简单地返回一个空字符串。

4.3. 跳过字符

我们还可以通过调用*skip(long n)*方法跳过给定数量的字符:

@Test
public void givenBufferedReader_whensSkipChars_thenOk() throws IOException {
    StringBuilder result = new StringBuilder();
    try (BufferedReader reader = 
           new BufferedReader(new StringReader("1__2__3__4__5"))) {
        int value;
        while ((value = reader.read()) != -1) {
            result.append((char) value);
            reader.skip(2L);
        }
    }
    assertEquals("12345", result);
}

在上面的示例中,我们从一个输入字符串中读取,该字符串包含由两个下划线分隔的数字。为了构造一个只包含数字的字符串,我们通过调用skip方法来跳过下划线。

4.4. markreset

我们可以使用 mark(int readAheadLimit)reset()方法来标记流中的某个位置并稍后返回。作为一个有些人为的例子,让我们使用mark()reset() 来忽略流开头的所有空格:

@Test
public void givenBufferedReader_whenSkipsWhitespacesAtBeginning_thenOk() 
  throws IOException {
    String result;
    try (BufferedReader reader = 
           new BufferedReader(new StringReader("    Lorem ipsum dolor sit amet."))) {
        do {
            reader.mark(1);
        } while(Character.isWhitespace(reader.read()))
        reader.reset();
        result = reader.readLine();
    }
    assertEquals("Lorem ipsum dolor sit amet.", result);
}

在上面的例子中,我们使用mark()方法来标记我们刚刚读取的位置。给它一个值 1 意味着只有代码会记住前一个字符的标记。**这在这里很方便,因为一旦我们看到第一个非空白字符,我们就可以返回并重新读取该字符,而无需重新处理整个流。如果没有标记,我们会 在最后一个字符串中丢失L。**

请注意,因为 mark()可以抛出 UnsupportedOperationException,所以将markSupported()与调用 mark() 的代码相关联是很常见的。虽然,我们实际上并不需要它。**那是因为 markSupported() 总是为BufferedReader返回 true。**

当然,我们也许可以通过其他方式更优雅地完成上述操作,并且确实markreset不是非常典型的方法。 但是,当需要展望未来时,它们肯定会派上用场