1. 概述

在这个教程中,我们将学习如何在Java中分割大文件。首先,我们将比较内存中读取文件与使用流读取文件的方法。之后,我们将学习如何根据文件大小和数量来分割文件。

2. 内存读取文件 vs. 流式读取文件

当我们用内存读取文件时,JVM会将所有行都存储在内存中。对于小文件,这是一个不错的选择。但对于大文件,这常常会导致OutOfMemoryException

流式读取文件是另一种方式,有许多方法可以流式处理和读取大型文件。由于整个文件不会全部加载到内存中,它消耗更少的内存,并且在处理大文件时不会抛出异常。

在我们的示例中,我们将使用流来读取大文件。

3. 根据文件大小分割文件

虽然我们已经学会了读取大文件,但有时我们需要将它们分割成更小的文件或以较小的尺寸在网络上传输。首先,我们将开始将大文件分割成每个特定大小的小文件。在我们的例子中,我们将从项目src/main/resource文件夹中的一个4.3MB文件largeFile.txt开始,将其分割成每1MB一个文件,并将它们存储在/target/split目录下。首先,我们获取大文件并为其打开一个输入流

File largeFile = new File("LARGE_FILE_PATH");
InputStream inputstream = Files.newInputStream(largeFile.toPath());

这里,我们只加载文件元数据,大文件的内容尚未加载到内存中。

对于我们的示例,我们有一个固定的固定大小。在实际应用中,这个maxSizeOfSplitFiles值可以根据应用程序需求动态读取和更改。

现在,让我们创建一个方法,该方法接受一个largeFile对象和一个定义的maxSizeOfSplitFiles,用于分割文件

public List<File> splitByFileSize(File largeFile, int maxSizeOfSplitFiles, String splitFileDirPath) 
  throws IOException {
    // ...
}

接下来,我们创建一个名为SplitLargeFile的类和splitByFileSize()方法:

class SplitLargeFile {

    public List<File> splitByFileSize(File largeFile, int maxSizeOfSplitFiles, String splitFileDirPath) 
      throws IOException {

        List<File> listOfSplitFiles = new ArrayList<>();
        try (InputStream in = Files.newInputStream(largeFile.toPath())) {
            final byte[] buffer = new byte[maxSizeOfSplitFiles];
            int dataRead = in.read(buffer);
            while (dataRead > -1) {
                File splitFile = getSplitFile(FilenameUtils.removeExtension(largeFile.getName()),
                  buffer, dataRead, splitFileDirPath);
                listOfSplitFiles.add(splitFile);
                dataRead = in.read(buffer);
            }
        }
        return listOfSplitFiles;
    }

    private File getSplitFile(String largeFileName, byte[] buffer, int length, String splitFileDirPath) 
      throws IOException {

        File splitFile = File.createTempFile(largeFileName + "-", "-split", new File(splitFileDirPath));
        try (FileOutputStream fos = new FileOutputStream(splitFile)) {
            fos.write(buffer, 0, length);
        }
        return splitFile;
    }
}

使用maxSizeOfSplitFiles,我们可以指定每个小文件的最大大小。**maxSizeOfSplitFiles大小的数据将被加载到内存中,处理后形成一个小文件,然后丢弃。我们再读取下一组maxSizeOfSplitFiles数据。这样可以确保不会抛出OutOfMemoryException。**最后,该方法返回一个存储在splitFileDirPath下的分割文件列表。我们可以将分割文件存储在任何临时目录或自定义目录中。现在,让我们测试一下:

public class SplitLargeFileUnitTest {

    @BeforeClass
    static void prepareData() throws IOException {
        Files.createDirectories(Paths.get("target/split"));
    }

    private String splitFileDirPath() throws Exception {
        return Paths.get("target").toString() + "/split";
    }

    private Path largeFilePath() throws Exception {
        return Paths.get(this.getClass().getClassLoader().getResource("largeFile.txt").toURI());
    }

    @Test
    void givenLargeFile_whenSplitLargeFile_thenSplitBySize() throws Exception {
        File input = largeFilePath().toFile();
        SplitLargeFile slf = new SplitLargeFile();
        slf.splitByFileSize(input, 1024_000, splitFileDirPath());
    }
}

最后,测试后我们可以看到,程序将大文件分割成四个1MB的文件和一个240KB的文件,并将它们放在项目target/split目录下。

4. 根据文件数量分割文件

现在,让我们将给定的大文件分割成指定数量的小文件。为此,我们首先需要检查按照文件数量划分的小文件大小是否合适。同时,我们将在内部使用之前splitByFileSize()方法来进行实际的分割。

让我们创建一个splitByNumberOfFiles()方法:

class SplitLargeFile {

    public List<File> splitByNumberOfFiles(File largeFile, int noOfFiles, String splitFileDirPath)
      throws IOException {
        return splitByFileSize(largeFile, getSizeInBytes(largeFile.length(), noOfFiles), splitFileDirPath);
    }

    private int getSizeInBytes(long largefileSizeInBytes, int numberOfFilesforSplit) {
        if (largefileSizeInBytes % numberOfFilesforSplit != 0) {
            largefileSizeInBytes = ((largefileSizeInBytes / numberOfFilesforSplit) + 1) * numberOfFilesforSplit;
        }
        long x = largefileSizeInBytes / numberOfFilesforSplit;
        if (x > Integer.MAX_VALUE) {
            throw new NumberFormatException("size too large");
        }
        return (int) x;
    }
}

现在,让我们测试一下:

@Test
void givenLargeFile_whenSplitLargeFile_thenSplitByNumberOfFiles() throws Exception { 
    File input = largeFilePath().toFile(); 
    SplitLargeFile slf = new SplitLargeFile(); 
    slf.splitByNumberOfFiles(input, 3, splitFileDirPath()); 
}

最后,测试后我们可以看到,程序将大文件分割成3个1.4MB的文件,并将其放在项目target/split目录下。

5. 总结

在这篇文章中,我们了解了内存读取和流式读取文件的区别,这有助于我们在任何场景中选择合适的方法。接着,我们讨论了如何将大文件分割成小文件。然后,我们学习了按大小分割和按文件数量分割的方法。

如往常一样,本文中的示例代码可以在GitHub上找到。