Java 文件管理详解


都说 Linux 下万物皆文件,而 Java 经过数十年的发展,为文件操作管理也提供了丰富的 API,特别在 NIO 的引入之后,易用程度更是增加了不是一点半点。

本文将以记录为视角,分享 Java 中你不可错过的文件操作姿势。

一、文件目录

1. 文件操作

Java 中提供了 File 用于文件资源管理,常用方法参考下表:

方法 作用
isFile() 判断是否为文件,返回 boolean。
exists() 判断文件是否存在,返回 boolean。
createNewFile() 根据传入路径创建同名文件。
getPath() 获取文件路径。
getParentFile() 获取文件的父级目录。
getAbsolutePath() 获取文件的绝对路径。
public void fileDemo() throws IOException {
    File sourceFile = new File("src\\main\\resources\\info.txt");
    // 获取文件路径
    String filePath = sourceFile.getPath();    
    // 获取绝对路径
    String absolutePath = sourceFile.getAbsolutePath();
    // 文件是否存在
    boolean fileExist = sourceFile.exists();  
    // 是否为文件
    boolean isFile = sourceFile.isFile();
    // 新建文件
    sourceFile.createNewFile();
}

2. 目录操作

同理目录常用操作方法如下:

方法 作用
exists() 判断目录是否存在,返回 boolean。
mkdirs() 新建目录,返回 boolean。
isDirectoryisDirectory() 判断是否为目录,返回 boolean。
public void dirDemo(){
    File sourceFile = new File("src\\main\\resources\\info.txt");
    // 获取父级路径
    File parentPath = sourceFile.getParentFile();
    // 目录是否存在
    boolean direExist = parentPath.exists();    
    // 是否为目录
    boolean isDire = parentPath.isDirectory();
    // 新建目录
    parentPath.mkdirs();
}

3. 工具操作

File 类中提供了一些静态变量定义了文件中常用的变量,如目录分隔符等。

我们都知道 WindowsLinux 的文件目录系统使用的分隔符是不同的,如 Windows 使用的是 \ ,而 Linux 中使用的是 / ,显然在工程中维护两套代码成本是很高的。因此 File 提供了静态变量可根据系统获取分隔符。

如下述代码在 WindowsLinux 运行将会分别输出 \/

public void demo() {
    String separator = File.separator;
    System.out.println("Separator: " + separator);
}

4. 文件递归

通过递归调用实现遍历目录下所有文件。

private void traverse(File file) {
    if (file.isDirectory()) {
        File[] files = file.listFiles();
        if (files != null) {
            for (File subFile : files) {
                traverse(subFile);
            }
        }
    } else {
        // file -> do something
    }
}

二、文件解析

java.util.jar 包下提供了 ZipFileJarFile 用便捷的文件读取,可实现压缩文件内的文件读取。

1. ZipFile

ZipFile 可实现在不解压文件的前提下读取压缩包中的文件,其常用方法参考下表:

方法 作用
entries() 获取压缩文件内所有层级的文件,返回集合。
hasMoreElements() 判断文件游标是否到达末端。
nextElement() 通过游标访问集合中的下一文件元素。
isDirectory() 判断当前游标指向的元素是否文目录。
getName() 获取当前游标指定文件的文件名称。
public void demo1() throws IOException {
    String path = "E:\\Workspace\\Driver\\temporary.zip";
    try (ZipFile zipFile = new ZipFile(path)) {
        Enumeration<? extends ZipEntry> entries = zipFile.entries();
        while (entries.hasMoreElements()) {
            // visit next element
            ZipEntry zipEntry = entries.nextElement();
            // estimate is directory
            if (zipEntry.isDirectory()) {
                System.out.println(zipEntry.getName() + " is directory");
                continue;
            }

            // get file name
            // example: dir-b/info.log
            String jarName = zipEntry.getName();
            System.out.println("File path: " + zipEntry.getName());
        }
    } catch (Exception e) {
        throw new IOException(e);
    }
}

2. JarFile

JarFileZipFile 更细致的分类,可以用于读取 JAR 压缩包中的文件,其方式使用与 ZipFile 类似,这里不重复介绍。

下面是一个 JarFile 使用示例,读取 JAR 压缩文件中所有 .class 文件。

public Set<String> getClassNamesFromJarFile(File file) throws IOException {
    Set<String> classNames = new HashSet<>();
    try (JarFile jarFile = new JarFile(file)) {
        Enumeration<JarEntry> entries = jarFile.entries();
        while (entries.hasMoreElements()) {
            JarEntry jarEntry = entries.nextElement();
            if (jarEntry.isDirectory()) {
                continue;
            }

            String jarName = jarEntry.getName();
            if (jarName.endsWith(".class")) {
                String className = jarName.replace("/", ".");
                classNames.add(className);
            }
        }
        return classNames;
    }
}

3. 字节读取

在许多应用场景下,我们不仅需要读取压缩文件中的文件信息,有时需要读取压缩文件中某个文件内容。

如下则为从压缩文件中以字节数组 byte 的形式读取 info.txt 文件的应用示例。

public byte[] demo() throws IOException {
    String path = "E:\\Workspace\\Driver\\temporary.zip";
    try (ZipFile zipFile = new ZipFile(path)) {
        Enumeration<? extends ZipEntry> entries = zipFile.entries();
        while (entries.hasMoreElements()) {
            // visit next element
            ZipEntry zipEntry = entries.nextElement();
            if (zipEntry.isDirectory()) {
                continue;
            }

            // get element name
            String jarName = zipEntry.getName();
            if (jarName.equals("info.txt")) {
                try (
                        // get io from element
                        InputStream in = zipFile.getInputStream(zipEntry);
                        ByteArrayOutputStream bos = new ByteArrayOutputStream()
                ) {
                    // 0xFFFF: 65535
                    byte[] buffer = new byte[0xFFFF];
                    for (int len; (len = in.read(buffer)) != -1; ) {
                        bos.write(buffer, 0, len);
                    }
                    bos.flush();
                    return bos.toByteArray();
                } catch (IOException e) {
                    throw new IOException(e);
                }
            }
        }
        throw new NullPointerException("File not found.");
    }
}

三、资源拆合

1. 文件拆分

针对大文件的处理,通常都会涉及到拆分与合并从而降低系统资源占用与提高服务性能。

对于文件的拆分,首先确定拆分的思路:即先按照文件的大小等分确定拆分块的大小,如 95MB 的文件限制单个块大小不超过 10MB,则应该拆分为 10 个块,最后一个块不足 10MB 仍单独为一个文件块。确认文件块后即可开始按序读取文件内容,当读取的内容值到达文件块限制时则创建一个新文件。为了获取更高的性能,针对单个文件快仍采用类似的拆分写入,即分批不断添加数据至文件末端。

将上述的逻辑转化为相应的代码如下,这里采用 RandomAccessFile 进行文件的读取,并将读取的内容拆分为多个文件,拆分后每个文件名形式如:split.0,split.1。同时这里 BufferedOutputStream 最终输出为单个文件块,若需要上传至 OSS 系统可替换该部分将 RandomAccessFile 读取数据转为 InputStream

public class FileSplitTest {

    private static final String fileLocation = "src\\main\\resources\\chunk\\img1.jpg";

    private static final String splitDir = "src\\main\\resources\\chunk\\split";

    private static final String splitPrefix = "split.";

    @Test
    public void splitDemo() throws Exception {
        long numSplits = 10; // the split piece num
        int maxReadBufferSize = 2 * 1024 * 1024; // buffer size, 2MB

        try (RandomAccessFile raf = new RandomAccessFile(fileLocation, "r")) {
            long sourceSize = raf.length();
            long bytesPerSplit = sourceSize / numSplits;
            long remainingBytes = sourceSize % numSplits;

            // 按照块划分读取数据并拆分写入
            for (int destIx = 1; destIx <= numSplits; destIx++) {
                String location = splitDir + File.separator + splitPrefix + destIx;
                try (
                        OutputStream out = new FileOutputStream(location);
                        BufferedOutputStream bos = new BufferedOutputStream(out)
                ) {
                    if (bytesPerSplit > maxReadBufferSize) {
                        long numReads = bytesPerSplit / maxReadBufferSize;
                        long numRemainingRead = bytesPerSplit % maxReadBufferSize;
                        // 单个块仍采用分批写入提高性能
                        for (int i = 0; i < numReads; i++) {
                            readAndWrite(raf, bos, maxReadBufferSize);
                        }

                        // write the remain data that size less than a buffer
                        if (numRemainingRead > 0) {
                            readAndWrite(raf, bos, numRemainingRead);
                        }
                    } else {
                        // split less than buffer size then direct write
                        readAndWrite(raf, bos, bytesPerSplit);
                    }
                }
            }

            // write the remain data that size less than a buffer
            if (remainingBytes > 0) {
                String location = splitDir + File.separator + splitPrefix + (numSplits + 1);
                try (
                        OutputStream out = new FileOutputStream(location);
                        BufferedOutputStream bw = new BufferedOutputStream(out)
                ) {
                    readAndWrite(raf, bw, remainingBytes);
                }
            }
        }

        System.out.println("Work done.");
    }

    public void readAndWrite(RandomAccessFile raf, BufferedOutputStream bos, long numBytes) throws IOException {
        byte[] buf = new byte[(int) numBytes];
        int val = raf.read(buf);
        if (val != -1) {
            bos.write(buf);
        }
    }
}

2. 文件合并

对于拆分后的文件块的合并相对较为简单,即先根据先前拆分顺序回填即可。

文件块顺序在文件名中已经体现,这里通过 Comparator 实现了一个便捷的数据排序,完成后按照顺序利用 apache.commons.ioIOUtils.copy() 方法复制文件至文件末端实现数据的合并。

public class FileMergeTest {

    private static final String splitDir = "src\\main\\resources\\chunk\\split";

    private static final String mergeDir = "src\\main\\resources\\chunk\\merge";

    private static final String fileName = "img1-merge.jpg";

    @Test
    public void mergeDemo() throws Exception {
        File[] files = new File(splitDir).listFiles();
        String location = mergeDir + File.separator + fileName;
        assert files != null;
        // sort split file by name
        Arrays.sort(files, new FilerComparator());
        // merge split to one file
        joinFiles(new File(location), files);
        System.out.println("Work done.");
    }

    static class FilerComparator implements Comparator<File> {
        /**
         * @return (-1, 0, 1) = (greater, equal, less)
         */
        @Override
        public int compare(File file1, File file2) {
            int number1 = extractNumber(file1.getName());
            int number2 = extractNumber(file2.getName());
            return Integer.compare(number1, number2);
        }

        private int extractNumber(String fileName) {
            int lastIndex = fileName.lastIndexOf('.');
            if (lastIndex >= 0 && lastIndex < fileName.length() - 1) {
                return Integer.parseInt(fileName.substring(lastIndex + 1));
            }
            return 0;
        }
    }

    private void joinFiles(File destination, File[] sources) throws IOException {
        try (
                FileOutputStream fos = new FileOutputStream(destination, true);
                BufferedOutputStream bos = new BufferedOutputStream(fos);
        ) {
            for (File file : sources) {
                try (
                        FileInputStream fis = new FileInputStream(file);
                        BufferedInputStream bis = new BufferedInputStream(fis)
                ) {
                    IOUtils.copy(bis, bos);
                }
            }
        }
    }
}

参考文档

  1. Read file and split into multiple files
  2. Merging multiple files in java

文章作者: 烽火戏诸诸诸侯
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 烽火戏诸诸诸侯 !
  目录