掌握文件元數據讀取是文件管理、同步備份、審計日誌、安全掃描等系統的基礎能力。本文基於 java.io.File 與 java.nio.file 雙 API 對比,詳細講解文件大小、最後修改時間、讀寫執行權限、隱藏屬性、規範絕對路徑的獲取方法,並提供線程安全的日期格式化工具。
核心 API 對比表
| 元數據 | java.io.File (舊) | java.nio.file.Files (新,推薦) |
| 文件大小 | file.length() → long (bytes) | Files.size(path) → long |
| 最後修改時間 | file.lastModified() → long (ms since epoch) | Files.getLastModifiedTime(path) → FileTime |
| 可讀性 | file.canRead() | Files.isReadable(path) |
| 可寫性 | file.canWrite() | Files.isWritable(path) |
| 可執行性 | file.canExecute() (JDK 6+) | Files.isExecutable(path) |
| 隱藏屬性 | file.isHidden() | Files.isHidden(path) (JDK 11+) |
| 規範路徑 | file.getCanonicalPath() | path.toRealPath() |
| 是否目錄 | file.isDirectory() | Files.isDirectory(path) |
| 是否文件 | file.isFile() | Files.isRegularFile(path) |
| 是否符號鏈接 | 不支援 | Files.isSymbolicLink(path) |
| POSIX 權限 | 不支援 | Files.getPosixFilePermissions(path) |
| ACL 權限 | 不支援 | Files.getFileAttributeView(path, AclFileAttributeView.class) |
生產級工具類:FileMetadata
package com.example.util;
import java.io.*;
import java.nio.file.*;
import java.nio.file.attribute.*;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.*;
public class FileMetadata {
private static final DateTimeFormatter FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
.withZone(ZoneId.systemDefault());
/** 文件信息數據類 */
public static class Info {
public final String name;
public final String canonicalPath;
public final long sizeBytes;
public final String sizeHuman;
public final String lastModified;
public final boolean readable;
public final boolean writable;
public final boolean executable;
public final boolean hidden;
public final boolean isDirectory;
public final boolean isSymbolicLink;
public final Set posixPermissions; // null if not POSIX
public Info(String name, String canonicalPath, long sizeBytes, String sizeHuman,
String lastModified, boolean readable, boolean writable, boolean executable,
boolean hidden, boolean isDirectory, boolean isSymbolicLink,
Set posixPermissions) {
this.name = name;
this.canonicalPath = canonicalPath;
this.sizeBytes = sizeBytes;
this.sizeHuman = sizeHuman;
this.lastModified = lastModified;
this.readable = readable;
this.writable = writable;
this.executable = executable;
this.hidden = hidden;
this.isDirectory = isDirectory;
this.isSymbolicLink = isSymbolicLink;
this.posixPermissions = posixPermissions;
}
@Override public String toString() {
return String.format("FileInfo{name='%s', path='%s', size=%s (%d bytes), modified=%s, rwx=%s%s%s, hidden=%s, dir=%s, link=%s, posix=%s}",
name, canonicalPath, sizeHuman, sizeBytes, lastModified,
readable ? 'r' : '-', writable ? 'w' : '-', executable ? 'x' : '-',
hidden, isDirectory, isSymbolicLink, posixPermissions);
}
}
/**
* 獲取文件完整元數據
* @throws IOException 文件不存在或訪問失敗
*/
public static Info getInfo(String filePath) throws IOException {
Path path = Paths.get(filePath);
if (!Files.exists(path)) throw new FileNotFoundException(filePath);
// 基礎屬性
BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
long size = attrs.size();
String sizeHuman = humanReadableSize(size);
String modified = FMT.format(Instant.ofEpochMilli(attrs.lastModifiedTime().toMillis()));
// 權限檢查
boolean readable = Files.isReadable(path);
boolean writable = Files.isWritable(path);
boolean executable = Files.isExecutable(path);
boolean hidden = Files.isHidden(path);
// 類型
boolean isDir = attrs.isDirectory();
boolean isLink = Files.isSymbolicLink(path);
// 規範路徑
String canonical = path.toRealPath().toString();
// POSIX 權限(Linux/Unix)
Set posixPerms = null;
if (FileSystems.getDefault().supportedFileAttributeViews().contains("posix")) {
try {
posixPerms = Files.getPosixFilePermissions(path);
} catch (UnsupportedOperationException | IOException ignored) {}
}
return new Info(path.getFileName().toString(), canonical, size, sizeHuman, modified,
readable, writable, executable, hidden, isDir, isLink, posixPerms);
}
/** 字節數人性化格式化 */
public static String humanReadableSize(long bytes) {
if (bytes < 1024) return bytes + " B";
int exp = (int) (Math.log(bytes) / Math.log(1024));
String unit = "KMGTPE".charAt(exp - 1) + "iB";
return String.format("%.2f %s", bytes / Math.pow(1024, exp), unit);
}
/** 批量獲取目錄下所有文件信息 */
public static List getDirectoryInfo(String dirPath) throws IOException {
List list = new ArrayList<>();
try (Stream stream = Files.list(Paths.get(dirPath))) {
stream.forEach(p -> {
try {
list.add(getInfo(p.toString()));
} catch (IOException e) {
System.err.println("Failed to read: " + p + " - " + e.getMessage());
}
});
}
return list;
}
}
日期時間處理最佳實踐
舊代碼常見問題
// 問題:SimpleDateFormat 非線程安全,Calendar 麻煩
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm");
String s = sdf.format(new Date(file.lastModified()));
現代解決方案:java.time (JDK 8+)
// 線程安全、不可變、鏈式 API
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
.withZone(ZoneId.systemDefault()); // 或指定 ZoneId.of("Asia/Shanghai")
String formatted = fmt.format(Instant.ofEpochMilli(file.lastModified()));
// 或 FileTime
String formatted2 = fmt.format(Files.getLastModifiedTime(path).toInstant());
格式化模式對照
| 模式字母 | 含義 | 示例 |
yyyy | 年 | 2024 |
MM | 月(01-12) | 01 |
dd | 日(01-31) | 15 |
HH | 時(00-23) | 14 |
mm | 分 | 30 |
ss | 秒 | 05 |
SSS | 毫秒 | 123 |
z | 時區名 | CST |
X | ISO 8601 時區偏移 | +08:00 |
進階:POSIX 權限解析與修改
// 讀取 rwx 字符串
Set perms = Files.getPosixFilePermissions(path);
String rwx = PosixFilePermissions.toString(perms); // e.g. "rw-r--r--"
// 修改權限:添加組寫權限
Set newPerms = new HashSet<>(perms);
newPerms.add(PosixFilePermission.GROUP_WRITE);
Files.setPosixFilePermissions(path, newPerms);
// 從字符串解析
Set parsed = PosixFilePermissions.fromString("rwxr-xr-x");
Files.setPosixFilePermissions(path, parsed);
命令行測試工具
public class FileInfoTool {
public static void main(String[] args) {
if (args.length == 0) {
System.err.println("Usage: FileInfoTool [file2...]");
System.exit(1);
}
for (String arg : args) {
try {
if (Files.isDirectory(Paths.get(arg))) {
FileMetadata.getDirectoryInfo(arg).forEach(System.out::println);
} else {
System.out.println(FileMetadata.getInfo(arg));
}
} catch (IOException e) {
System.err.println(arg + ": " + e.getMessage());
}
}
}
}
常見坑點避雷
- canRead/canWrite 對超級用戶總返回 true:Linux root 進程權限檢查失效,需結合 POSIX 權限位判斷。
- lastModified 精度受文件系統限制:FAT32 2秒、ext4 奈秒、NTFS 100奈秒,跨 FS 複製會丟失精度。
- getCanonicalPath() 拋出 IOException:舊 API 必須 try-catch,NIO
toRealPath() 同理。
- Windows 隱藏屬性判斷:
Files.isHidden() JDK 11+ 才可靠,舊版需 JNI 或命令行 attrib。
- 符號鏈接循環導致 walk 死循環:使用
Files.walkFileTree(..., FOLLOW_LINKS) 時需自行檢測循環或限制深度。