都说 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
类中提供了一些静态变量定义了文件中常用的变量,如目录分隔符等。
我们都知道 Windows
和 Linux
的文件目录系统使用的分隔符是不同的,如 Windows
使用的是 \
,而 Linux
中使用的是 /
,显然在工程中维护两套代码成本是很高的。因此 File
提供了静态变量可根据系统获取分隔符。
如下述代码在 Windows
和 Linux
运行将会分别输出 \
和 /
。
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
包下提供了 ZipFile
与 JarFile
用便捷的文件读取,可实现压缩文件内的文件读取。
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
JarFile
是 ZipFile
更细致的分类,可以用于读取 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.io
的 IOUtils.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);
}
}
}
}
}
参考文档