1. 概述
本教程将深入探讨在 Java 中读取文件的多种方式。
首先,我们会学习如何使用标准 Java 类从类路径、URL 或 JAR 文件中加载文件。
其次,我们将研究如何使用 BufferedReader、Scanner、StreamTokenizer、DataInputStream、SequenceInputStream 和 FileChannel 读取内容。还会讨论如何读取 UTF-8 编码的文件。
最后,我们将探索 Java 7 和 Java 8 中加载和读取文件的新技术。
本文是 Baeldung 上《Java – Back to Basic》系列的一部分。
2. 环境准备
2.1. 输入文件
本文大部分示例使用名为 fileTest.txt 的文本文件,其内容仅一行:
Hello, world!
少数示例会使用不同文件,届时会明确说明文件名及其内容。
2.2. 辅助方法
我们将使用纯核心 Java 类进行测试,并在测试中使用 Hamcrest 匹配器进行断言。
测试共用一个 readFromInputStream 方法,用于将 InputStream 转换为 String 以便断言:
private String readFromInputStream(InputStream inputStream)
throws IOException {
StringBuilder resultStringBuilder = new StringBuilder();
try (BufferedReader br
= new BufferedReader(new InputStreamReader(inputStream))) {
String line;
while ((line = br.readLine()) != null) {
resultStringBuilder.append(line).append("\n");
}
}
return resultStringBuilder.toString();
}
⚠️ 注意:实现此功能还有其他方式,可参考这篇文章了解替代方案。
3. 从类路径读取文件
3.1. 使用标准 Java
本节说明如何读取类路径中的文件。我们将读取位于 src/main/resources 下的 "fileTest.txt":
@Test
public void givenFileNameAsAbsolutePath_whenUsingClasspath_thenFileData() {
String expectedData = "Hello, world!";
Class clazz = FileOperationsTest.class;
InputStream inputStream = clazz.getResourceAsStream("/fileTest.txt");
String data = readFromInputStream(inputStream);
Assert.assertThat(data, containsString(expectedData));
}
上述代码中,我们使用当前类的 getResourceAsStream 方法加载文件,并传入文件的绝对路径。
ClassLoader 实例也提供相同方法:
ClassLoader classLoader = getClass().getClassLoader();
InputStream inputStream = classLoader.getResourceAsStream("fileTest.txt");
String data = readFromInputStream(inputStream);
通过 getClass().getClassLoader() 获取当前类的类加载器。
主要区别在于:
- 使用 ClassLoader 的 getResourceAsStream 时,路径被视为从类路径根开始的绝对路径
- 使用 Class 实例时,路径可以是相对于包的路径,或以斜杠开头的绝对路径
实际开发中务必记得关闭打开的流,例如示例中的 InputStream:
InputStream inputStream = null;
try {
File file = new File(classLoader.getResource("fileTest.txt").getFile());
inputStream = new FileInputStream(file);
//...
}
finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
3.2. 使用 commons-io 库
另一种常用方案是使用 commons-io 包的 FileUtils 类:
@Test
public void givenFileName_whenUsingFileUtils_thenFileData() {
String expectedData = "Hello, world!";
ClassLoader classLoader = getClass().getClassLoader();
File file = new File(classLoader.getResource("fileTest.txt").getFile());
String data = FileUtils.readFileToString(file, "UTF-8");
assertEquals(expectedData, data.trim());
}
这里我们将 File 对象传递给 FileUtils 的 readFileToString() 方法。该工具类无需编写创建 InputStream 实例和读取数据的样板代码。
该库还提供 IOUtils 类:
@Test
public void givenFileName_whenUsingIOUtils_thenFileData() {
String expectedData = "Hello, world!";
FileInputStream fis = new FileInputStream("src/test/resources/fileTest.txt");
String data = IOUtils.toString(fis, "UTF-8");
assertEquals(expectedData, data.trim());
}
这里我们将 FileInputStream 对象传递给 IOUtils 的 toString() 方法。该工具类与前一个类似,用于创建 InputStream 实例并读取数据。
4. 使用 BufferedReader 读取
现在我们聚焦解析文件内容的不同方式。
先来看使用 BufferedReader 读取文件的简单方式:*
@Test
public void whenReadWithBufferedReader_thenCorrect()
throws IOException {
String expected_value = "Hello, world!";
String file ="src/test/resources/fileTest.txt";
BufferedReader reader = new BufferedReader(new FileReader(file));
String currentLine = reader.readLine();
reader.close();
assertEquals(expected_value, currentLine);
}
⚠️ 注意:当到达文件末尾时,readLine() 将返回 null。
5. 使用 Java NIO 读取文件
JDK7 中 NIO 包得到重大更新。
让我们看一个使用 Files 类和 readAllLines 方法的示例。readAllLines 方法接受一个 Path 参数。
Path 类可视为 java.io.File 的升级版,增加了额外操作。
5.1. 读取小文件
以下代码展示如何使用新的 Files 类读取小文件:
@Test
public void whenReadSmallFileJava7_thenCorrect()
throws IOException {
String expected_value = "Hello, world!";
Path path = Paths.get("src/test/resources/fileTest.txt");
String read = Files.readAllLines(path).get(0);
assertEquals(expected_value, read);
}
如果需要二进制数据,也可以使用 readAllBytes() 方法。
5.2. 读取大文件
若要用 Files 类读取大文件,可以使用 BufferedReader。
以下代码使用新的 Files 类和 BufferedReader 读取文件:
@Test
public void whenReadLargeFileJava7_thenCorrect()
throws IOException {
String expected_value = "Hello, world!";
Path path = Paths.get("src/test/resources/fileTest.txt");
BufferedReader reader = Files.newBufferedReader(path);
String line = reader.readLine();
assertEquals(expected_value, line);
}
5.3. 使用 Files.lines() 读取
*JDK8 在 Files 类中提供了 lines() 方法。它返回一个 String 元素的 Stream。*
让我们看一个如何将数据读取为字节并使用 UTF-8 字符集解码的示例。
以下代码使用新的 Files.lines() 读取文件:
@Test
public void givenFilePath_whenUsingFilesLines_thenFileData() {
String expectedData = "Hello, world!";
Path path = Paths.get(getClass().getClassLoader()
.getResource("fileTest.txt").toURI());
Stream<String> lines = Files.lines(path);
String data = lines.collect(Collectors.joining("\n"));
lines.close();
Assert.assertEquals(expectedData, data.trim());
}
✅ 使用 Stream 处理文件操作等 IO 通道时,必须通过 close() 方法显式关闭流。
可见,Files API 提供了另一种将文件内容读取为 String 的便捷方式。
接下来,我们将探讨其他不太常用但在某些场景下适用的读取方法。
6. 使用 Scanner 读取
接下来使用 Scanner 从文件读取。这里我们用空格作为分隔符:
@Test
public void whenReadWithScanner_thenCorrect()
throws IOException {
String file = "src/test/resources/fileTest.txt";
Scanner scanner = new Scanner(new File(file));
scanner.useDelimiter(" ");
assertTrue(scanner.hasNext());
assertEquals("Hello,", scanner.next());
assertEquals("world!", scanner.next());
scanner.close();
}
默认分隔符是空格,但 Scanner 可使用多个分隔符。
Scanner 类在从控制台读取或内容包含已知分隔符(如空格分隔的整数列表)的原始值时特别有用。
7. 使用 StreamTokenizer 读取
现在使用 StreamTokenizer 将文本文件读取为标记。
标记器通过判断下一个标记是 String 还是数字来工作。我们查看 tokenizer.ttype 字段实现。
然后根据类型读取实际标记:
- tokenizer.nval – 如果类型是数字
- tokenizer.sval – 如果类型是 String
本例使用不同的输入文件,仅包含:
Hello 1
以下代码从文件读取字符串和数字:
@Test
public void whenReadWithStreamTokenizer_thenCorrectTokens()
throws IOException {
String file = "src/test/resources/fileTestTokenizer.txt";
FileReader reader = new FileReader(file);
StreamTokenizer tokenizer = new StreamTokenizer(reader);
// token 1
tokenizer.nextToken();
assertEquals(StreamTokenizer.TT_WORD, tokenizer.ttype);
assertEquals("Hello", tokenizer.sval);
// token 2
tokenizer.nextToken();
assertEquals(StreamTokenizer.TT_NUMBER, tokenizer.ttype);
assertEquals(1, tokenizer.nval, 0.0000001);
// token 3
tokenizer.nextToken();
assertEquals(StreamTokenizer.TT_EOF, tokenizer.ttype);
reader.close();
}
注意文件结束标记的使用方式。
这种方法适用于将输入流解析为标记。
8. 使用 DataInputStream 读取
我们可以使用 DataInputStream 从文件读取二进制或原始数据类型。
以下测试使用 DataInputStream 读取文件:
@Test
public void whenReadWithDataInputStream_thenCorrect() throws IOException {
String expectedValue = "Hello, world!";
String file ="src/test/resources/fileTest.txt";
String result = null;
DataInputStream reader = new DataInputStream(new FileInputStream(file));
int nBytesToRead = reader.available();
if(nBytesToRead > 0) {
byte[] bytes = new byte[nBytesToRead];
reader.read(bytes);
result = new String(bytes);
}
assertEquals(expectedValue, result);
}
9. 使用 FileChannel 读取
读取大文件时,FileChannel 可能比标准 IO 更快。
以下代码使用 FileChannel 和 RandomAccessFile 从文件读取数据字节:
@Test
public void whenReadWithFileChannel_thenCorrect()
throws IOException {
String expected_value = "Hello, world!";
String file = "src/test/resources/fileTest.txt";
RandomAccessFile reader = new RandomAccessFile(file, "r");
FileChannel channel = reader.getChannel();
int bufferSize = 1024;
if (bufferSize > channel.size()) {
bufferSize = (int) channel.size();
}
ByteBuffer buff = ByteBuffer.allocate(bufferSize);
channel.read(buff);
buff.flip();
assertEquals(expected_value, new String(buff.array()));
channel.close();
reader.close();
}
10. 读取 UTF-8 编码文件
现在看如何使用 BufferedReader 读取 UTF-8 编码文件。本例读取包含中文字符的文件:
@Test
public void whenReadUTFEncodedFile_thenCorrect()
throws IOException {
String expected_value = "青空";
String file = "src/test/resources/fileTestUtf8.txt";
BufferedReader reader = new BufferedReader
(new InputStreamReader(new FileInputStream(file), "UTF-8"));
String currentLine = reader.readLine();
reader.close();
assertEquals(expected_value, currentLine);
}
11. 从 URL 读取内容
要从 URL 读取内容,示例中使用“*https://baeldung.com/*” URL:
@Test
public void givenURLName_whenUsingURL_thenFileData() {
String expectedData = "Baeldung";
URL urlObject = new URL("https://baeldung.com/");
URLConnection urlConnection = urlObject.openConnection();
InputStream inputStream = urlConnection.getInputStream();
String data = readFromInputStream(inputStream);
Assert.assertThat(data, containsString(expectedData));
}
连接 URL 还有其他方式。这里使用了标准 SDK 中的 URL 和 URLConnection 类。
12. 从 JAR 中读取文件
要读取 JAR 文件内的文件,我们需要一个包含文件的 JAR。示例中读取“hamcrest-library-1.3.jar”中的“LICENSE.txt”:
@Test
public void givenFileName_whenUsingJarFile_thenFileData() {
String expectedData = "BSD License";
Class clazz = Matchers.class;
InputStream inputStream = clazz.getResourceAsStream("/LICENSE.txt");
String data = readFromInputStream(inputStream);
Assert.assertThat(data, containsString(expectedData));
}
这里要加载 Hamcrest 库中的 LICENSE.txt,因此使用 Matcher 类帮助获取资源。同样文件也可通过类加载器加载。
13. 总结
可见,使用纯 Java 加载文件和读取数据的方式多种多样。
我们可以从类路径、URL 或 JAR 文件等多种位置加载文件。
然后可以使用:
- BufferedReader 逐行读取
- Scanner 使用不同分隔符读取
- StreamTokenizer 将文件读取为标记
- DataInputStream 读取二进制和原始数据类型
- SequenceInputStream 将多个文件链接为一个流
- FileChannel 更快地读取大文件
本文源代码可在以下 GitHub 仓库 找到。