文件处理

文件写入

BufferedWriter

public void whenWriteStringUsingBufferedWritter_thenCorrect()
  throws IOException {
    String str = "Hello";
    BufferedWriter writer = new BufferedWriter(new FileWriter(fileName));
    writer.write(str);
    writer.close();
}
// 追加写入
@Test
public void whenAppendStringUsingBufferedWritter_thenOldContentShouldExistToo()
  throws IOException {
    String str = "World";
    BufferedWriter writer = new BufferedWriter(new FileWriter(fileName, true));
    writer.append(' ');
    writer.append(str);
    writer.close();
}

PrintWriter

PrintWriter 则是支持以格式化方式写入:

@Test
public void givenWritingStringToFile_whenUsingPrintWriter_thenCorrect()
  throws IOException {
    FileWriter fileWriter = new FileWriter(fileName);
    PrintWriter printWriter = new PrintWriter(fileWriter);
    printWriter.print("Some String");
    printWriter.printf("Product name is %s and its price is %d $", "iPhone", 1000);
    printWriter.close();
}
// Some String
// Product name is iPhone and its price is 1000$

FileOutputStream

FileOutputStream/DataOutputStream 能够用于写入二进制数据:

@Test
public void givenWritingStringToFile_whenUsingFileOutputStream_thenCorrect()
  throws IOException {
    String str = "Hello";
    FileOutputStream outputStream = new FileOutputStream(fileName);
    byte[] strToBytes = str.getBytes();
    outputStream.write(strToBytes);
    outputStream.close();
}
@Test
public void givenWritingToFile_whenUsingDataOutputStream_thenCorrect()
  throws IOException {
    String value = "Hello";
    FileOutputStream fos = new FileOutputStream(fileName);
    DataOutputStream outStream = new DataOutputStream(new BufferedOutputStream(fos));
    outStream.writeUTF(value);
    outStream.close();
    // verify the results
    String result;
    FileInputStream fis = new FileInputStream(fileName);
    DataInputStream reader = new DataInputStream(fis);
    result = reader.readUTF();
    reader.close();
    assertEquals(value, result);
}

RandomAccessFile

现在,让我们说明如何在现有文件中进行写入和编辑-而不是仅写入一个全新的文件或追加到现有文件中。简而言之,我们需要随机访问。RandomAccessFile 使我们能够在文件的特定位置进行写操作,给定从文件开头的偏移量(以字节为单位)。以下代码写入一个从文件开头开始偏移的整数值:

private void writeToPosition(String filename, int data, long position)
  throws IOException {
    RandomAccessFile writer = new RandomAccessFile(filename, "rw");
    writer.seek(position);
    writer.writeInt(data);
    writer.close();
}

FileChannel

@Test
public void givenWritingToFile_whenUsingFileChannel_thenCorrect()
  throws IOException {
    RandomAccessFile stream = new RandomAccessFile(fileName, "rw");
    FileChannel channel = stream.getChannel();
    String value = "Hello";
    byte[] strBytes = value.getBytes();
    ByteBuffer buffer = ByteBuffer.allocate(strBytes.length);
    buffer.put(strBytes);
    buffer.flip();
    channel.write(buffer);
    stream.close();
    channel.close();
    // verify
    RandomAccessFile reader = new RandomAccessFile(fileName, "r");
    assertEquals(value, reader.readLine());
    reader.close();
}

Temporary

@Test
public void whenWriteToTmpFile_thenCorrect() throws IOException {
    String toWrite = "Hello";
    File tmpFile = File.createTempFile("test", ".tmp");
    FileWriter writer = new FileWriter(tmpFile);
    writer.write(toWrite);
    writer.close();
    BufferedReader reader = new BufferedReader(new FileReader(tmpFile));
    assertEquals(toWrite, reader.readLine());
    reader.close();
}

文件加锁

最后,在写入文件时,有时需要确保没有其他人同时写入该文件。基本上-您需要能够在写入时锁定该文件。让我们利用 FileChannel 尝试在写入文件之前将其锁定:

@Test
public void whenTryToLockFile_thenItShouldBeLocked()
  throws IOException {
    RandomAccessFile stream = new RandomAccessFile(fileName, "rw");
    FileChannel channel = stream.getChannel();
    FileLock lock = null;
    try {
        lock = channel.tryLock();
    } catch (final OverlappingFileLockException e) {
        stream.close();
        channel.close();
    }
    stream.writeChars("test lock");
    lock.release();
    stream.close();
    channel.close();
}

大文件读写

如果我们直接使用普通的方式来读取大文件

@Test
public void givenUsingGuava_whenIteratingAFile_thenWorks() throws IOException {
    String path = ...
    Files.readLines(new File(path), Charsets.UTF_8);
}

有可能会出现内存不够的情况:

[main] INFO  org.baeldung.java.CoreJavaIoUnitTest - Total Memory: 128 Mb
[main] INFO  org.baeldung.java.CoreJavaIoUnitTest - Free Memory: 116 Mb
[main] INFO  org.baeldung.java.CoreJavaIoUnitTest - Total Memory: 2666 Mb
[main] INFO  org.baeldung.java.CoreJavaIoUnitTest - Free Memory: 490 Mb

至此,显而易见的是,将文件内容保留在内存中将迅速耗尽可用内存,而不管实际有多少内存。

流式读取

FileInputStream inputStream = null;
Scanner sc = null;
try {
    inputStream = new FileInputStream(path);
    sc = new Scanner(inputStream, "UTF-8");
    while (sc.hasNextLine()) {
        String line = sc.nextLine();
        // System.out.println(line);
    }
    // note that Scanner suppresses exceptions
    if (sc.ioException() != null) {
        throw sc.ioException();
    }
} finally {
    if (inputStream != null) {
        inputStream.close();
    }
    if (sc != null) {
        sc.close();
    }
}

该解决方案将遍历文件中的所有行-允许处理每一行-无需保留对其的引用-并最终不将其保留在内存中:(消耗约 150 Mb)

[main] INFO  org.baeldung.java.CoreJavaIoUnitTest - Total Memory: 763 Mb
[main] INFO  org.baeldung.java.CoreJavaIoUnitTest - Free Memory: 605 Mb

我们也可以使用 Apache Commons IO 来进行流式读取:

LineIterator it = FileUtils.lineIterator(theFile, "UTF-8");
try {
    while (it.hasNext()) {
        String line = it.nextLine();
        // do something with line
    }
} finally {
    LineIterator.closeQuietly(it);
}

文件读取

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);
}
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();
}

我们也可以指定不同的编码:

@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);
}

从 Classpath 中读取文件

@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 = getClass().getClassLoader();
InputStream inputStream = classLoader.getResourceAsStream("fileTest.txt");
String data = readFromInputStream(inputStream);

我们使用 getClass().getClassLoader(). 获得当前类的 classLoader。主要区别在于,在 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();
        }
    }
}

我们也能够读取某个 Jar 包中的文件

@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));
}

commons-io

@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());
}
@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());
}

在这里,我们将 File 对象传递给 FileUtils 类的 readFileToString() 方法。该实用程序类无需写入任何样板代码即可创建内容,而无需编写任何代码来创建 InputStream 实例和读取数据。

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 类很有用。

StreamTokenizer

@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();
}

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);
}

Files

在 JDK7 之后 NIO 也是得到了极大的更新,我们可以使用 Files 的 readAllBytes 方法来读取文件

Path path = Paths.get(getClass().getClassLoader()
    .getResource("fileTest.txt").toURI());
byte[] fileBytes = Files.readAllBytes(path);
String data = new String(fileBytes);
// Use Stream
Stream<String> lines = Files.lines(path);
String data = lines.collect(Collectors.joining("\n"));
lines.close();
// 按行读取
@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);
}
// 读取大文件
@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);
}

Java 11

@Test
public void readFileIntoString() throws IOException {
    String content = Files.readString(Paths.get("[FILE_NAME]"));
    assertNotNull(content);
}

随机读取

private int readFromPosition(String filename, long position)
  throws IOException {
    int result = 0;
    RandomAccessFile reader = new RandomAccessFile(filename, "r");
    reader.seek(position);
    result = reader.readInt();
    reader.close();
    return result;
}

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();
}

Temporary

@Test
public void whenWriteToTmpFile_thenCorrect() throws IOException {
    String toWrite = "Hello";
    File tmpFile = File.createTempFile("test", ".tmp");
    FileWriter writer = new FileWriter(tmpFile);
    writer.write(toWrite);
    writer.close();
    BufferedReader reader = new BufferedReader(new FileReader(tmpFile));
    assertEquals(toWrite, reader.readLine());
    reader.close();
}Copy to clipboardErrorCopied

文件加锁

最后,在写入文件时,有时需要确保没有其他人同时写入该文件。基本上-您需要能够在写入时锁定该文件。让我们利用 FileChannel 尝试在写入文件之前将其锁定:

@Test
public void whenTryToLockFile_thenItShouldBeLocked()
  throws IOException {
    RandomAccessFile stream = new RandomAccessFile(fileName, "rw");
    FileChannel channel = stream.getChannel();
    FileLock lock = null;
    try {
        lock = channel.tryLock();
    } catch (final OverlappingFileLockException e) {
        stream.close();
        channel.close();
    }
    stream.writeChars("test lock");
    lock.release();
    stream.close();
    channel.close();
}Copy to clipboardErrorCopied

大文件读写

如果我们直接使用普通的方式来读取大文件

@Test
public void givenUsingGuava_whenIteratingAFile_thenWorks() throws IOException {
    String path = ...
    Files.readLines(new File(path), Charsets.UTF_8);
}Copy to clipboardErrorCopied

有可能会出现内存不够的情况:

[main] INFO  org.baeldung.java.CoreJavaIoUnitTest - Total Memory: 128 Mb
[main] INFO  org.baeldung.java.CoreJavaIoUnitTest - Free Memory: 116 Mb
[main] INFO  org.baeldung.java.CoreJavaIoUnitTest - Total Memory: 2666 Mb
[main] INFO  org.baeldung.java.CoreJavaIoUnitTest - Free Memory: 490 MbCopy to clipboardErrorCopied

至此,显而易见的是,将文件内容保留在内存中将迅速耗尽可用内存,而不管实际有多少内存。

流式读取

FileInputStream inputStream = null;
Scanner sc = null;
try {
    inputStream = new FileInputStream(path);
    sc = new Scanner(inputStream, "UTF-8");
    while (sc.hasNextLine()) {
        String line = sc.nextLine();
        // System.out.println(line);
    }
    // note that Scanner suppresses exceptions
    if (sc.ioException() != null) {
        throw sc.ioException();
    }
} finally {
    if (inputStream != null) {
        inputStream.close();
    }
    if (sc != null) {
        sc.close();
    }
}Copy to clipboardErrorCopied

该解决方案将遍历文件中的所有行-允许处理每一行-无需保留对其的引用-并最终不将其保留在内存中:(消耗约 150 Mb)

[main] INFO  org.baeldung.java.CoreJavaIoUnitTest - Total Memory: 763 Mb
[main] INFO  org.baeldung.java.CoreJavaIoUnitTest - Free Memory: 605 MbCopy to clipboardErrorCopied

我们也可以使用 Apache Commons IO 来进行流式读取:

LineIterator it = FileUtils.lineIterator(theFile, "UTF-8");
try {
    while (it.hasNext()) {
        String line = it.nextLine();
        // do something with line
    }
} finally {
    LineIterator.closeQuietly(it);
}
下一节:Java.io 包几乎包含了所有操作输入、输出需要的类。所有这些流类代表了输入源和输出目标;Java.io 包中的流支持很多种格式,比如:基本类型、对象、本地化字符集等等。一个流可以理解为一个数据的序列。输入流表示从一个源读取数据,输出流表示向一个目标写数据。Java 为 IO 提供了强大的而灵活的支持,使其更广泛地应用到文件传输和网络编程中。