java 檔案的操作(Path、Paths、Files)

小程xy發表於2024-10-14

PathPathsFiles 是 Java NIO(New I/O)檔案處理系統中的核心元件,它們提供了比傳統 java.io.File 更加靈活和高效的檔案操作方式。

1. 概述

隨著 Java 7 引入 NIO.2(即 Java New I/O 2),檔案處理得到了顯著改進。PathPathsFiles 是 NIO.2 中用於檔案和目錄操作的三個關鍵元件:

  • Path:表示檔案系統中的路徑,類似於傳統的 java.io.File,但更加靈活和功能豐富。
  • Paths:一個工具類,提供靜態方法用於建立 Path 例項。
  • Files:一個實用工具類,提供了大量靜態方法用於執行檔案和目錄的各種操作,如建立、刪除、複製、移動、讀取和寫入等。

相比傳統的 File 類,NIO.2 提供了更好的錯誤處理、更豐富的功能以及對不同檔案系統的支援。


2. Path 介面

概述

Path 是一個介面,位於 java.nio.file 包中,用於表示檔案系統中的路徑。它提供了一種平臺無關的方式來表示檔案和目錄的路徑,並支援豐富的路徑操作。

主要功能和方法

以下是 Path 介面的一些關鍵方法和功能:

路徑建立與解析

  • Path resolve(String other):將給定的路徑字串解析為當前路徑的子路徑。
  • Path resolve(Path other):將給定的 Path 解析為當前路徑的子路徑。
  • Path relativize(Path other):計算從當前路徑到給定路徑的相對路徑。

路徑資訊

  • String getFileName():返回路徑中的檔名部分。
  • Path getParent():返回路徑的父路徑。
  • Path getRoot():返回路徑的根元件。
  • int getNameCount():返回路徑中的名稱元素數。
  • Path getName(int index):返回指定索引的名稱元素。

路徑轉換

  • Path toAbsolutePath():將相對路徑轉換為絕對路徑。
  • Path normalize():規範化路徑,去除冗餘的名稱元素,如 "."".."

路徑比較

  • boolean startsWith(String other):判斷路徑是否以給定的路徑字串開頭。
  • boolean endsWith(String other):判斷路徑是否以給定的路徑字串結尾。
  • boolean equals(Object other):判斷兩個路徑是否相等。

其他方法

  • Iterator<Path> iterator():返回一個迭代器,用於遍歷路徑中的名稱元素。
  • String toString():返回路徑的字串表示。
  • String toAbsolutePath().toString():返回絕對路徑的字串表示。

示例程式碼

import java.nio.file.Path;
import java.nio.file.Paths;

public class PathExample {
    public static void main(String[] args) {
        // 建立 Path 例項
        Path path = Paths.get("src", "main", "java", "Example.java");
        
        // 獲取檔名
        System.out.println("檔名: " + path.getFileName());
        
        // 獲取父路徑
        System.out.println("父路徑: " + path.getParent());
        
        // 獲取根路徑
        System.out.println("根路徑: " + path.getRoot());
        
        // 規範化路徑
        Path normalizedPath = path.normalize();
        System.out.println("規範化路徑: " + normalizedPath);
        
        // 轉換為絕對路徑
        Path absolutePath = path.toAbsolutePath();
        System.out.println("絕對路徑: " + absolutePath);
        
        // 解析子路徑
        Path resolvedPath = path.resolve("subdir/File.txt");
        System.out.println("解析後的路徑: " + resolvedPath);
        
        // 計算相對路徑
        Path basePath = Paths.get("src/main");
        Path relativePath = basePath.relativize(path);
        System.out.println("相對路徑: " + relativePath);
        
        // 遍歷路徑中的元素
        System.out.println("路徑元素:");
        for (Path element : path) {
            System.out.println(element);
        }
    }
}

輸出示例:

檔名: Example.java
父路徑: src/main/java
根路徑: null
規範化路徑: src/main/java/Example.java
絕對路徑: /Users/username/project/src/main/java/Example.java
解析後的路徑: src/main/java/Example.java/subdir/File.txt
相對路徑: java/Example.java
路徑元素:
src
main
java
Example.java

3. Paths

概述

Paths 是一個最終類,位於 java.nio.file 包中,提供了靜態方法用於建立 Path 例項。它簡化了 Path 物件的建立過程,使程式碼更加簡潔和易讀。

建立 Path 例項

import java.nio.file.Path;
import java.nio.file.Paths;
import java.net.URI;

public class PathsExample {
    public static void main(String[] args) {
        // 使用多個字串片段建立路徑
        Path path1 = Paths.get("C:", "Users", "Public", "Documents");
        System.out.println("路徑1: " + path1);
        
        // 使用單個字串建立路徑
        Path path2 = Paths.get("/home/user/docs");
        System.out.println("路徑2: " + path2);
        
        // 使用相對路徑建立路徑
        Path path3 = Paths.get("src/main/java/Example.java");
        System.out.println("路徑3: " + path3);
          
        // 組合路徑片段
        Path basePath = Paths.get("/home/user");
        Path combinedPath = basePath.resolve("downloads/music");
        System.out.println("組合後的路徑: " + combinedPath);
    }
}

輸出示例:

路徑1: C:\Users\Public\Documents
路徑2: /home/user/docs
路徑3: src/main/java/Example.java
組合後的路徑: /home/user/downloads/music

注意事項

  • Paths.get(...) 方法會根據作業系統自動處理路徑分隔符,無需手動指定。例如,在 Windows 上會使用 \,在 Unix/Linux 上會使用 /

4. Files

概述

Files 是一個最終類,位於 java.nio.file 包中,提供了大量的靜態方法用於執行檔案和目錄的各種操作。它與 Path 介面緊密整合,提供了比 java.io.File 更加豐富和高效的功能。

主要功能和方法

Files 類的方法可以大致分為以下幾類:

  1. 檔案和目錄的建立
  2. 檔案和目錄的刪除
  3. 檔案和目錄的複製與移動
  4. 檔案內容的讀取與寫入
  5. 檔案屬性的獲取與修改
  6. 目錄的遍歷和查詢

1. 檔案和目錄的建立

  • static Path createFile(Path path, FileAttribute<?>... attrs):建立一個新檔案。
  • static Path createDirectory(Path dir, FileAttribute<?>... attrs):建立一個新目錄。
  • static Path createDirectories(Path dir, FileAttribute<?>... attrs):遞迴地建立目錄,包括不存在的父目錄。

2. 檔案和目錄的刪除

  • static void delete(Path path):刪除指定的檔案或目錄。如果路徑是目錄,則目錄必須為空。
  • static boolean deleteIfExists(Path path):刪除指定的檔案或目錄,如果存在的話。

3. 檔案和目錄的複製與移動

  • static Path copy(Path source, Path target, CopyOption... options):複製檔案或目錄。
  • static Path move(Path source, Path target, CopyOption... options):移動或重新命名檔案或目錄。

4. 檔案內容的讀取與寫入

  • static byte[] readAllBytes(Path path):讀取檔案的所有位元組。
  • static List<String> readAllLines(Path path, Charset cs):按行讀取檔案內容。
  • static Path write(Path path, byte[] bytes, OpenOption... options):將位元組陣列寫入檔案。
  • static Path write(Path path, Iterable<? extends CharSequence> lines, Charset cs, OpenOption... options):將行寫入檔案。

5. 檔案屬性的獲取與修改

  • static boolean exists(Path path, LinkOption... options):檢查路徑是否存在。
  • static boolean isDirectory(Path path, LinkOption... options):判斷路徑是否是目錄。
  • static boolean isRegularFile(Path path, LinkOption... options):判斷路徑是否是常規檔案。
  • static long size(Path path):獲取檔案的大小(以位元組為單位)。
  • static FileTime getLastModifiedTime(Path path, LinkOption... options):獲取檔案的最後修改時間。
  • static Path setLastModifiedTime(Path path, FileTime time):設定檔案的最後修改時間。

6. 目錄的遍歷和查詢

  • static DirectoryStream<Path> newDirectoryStream(Path dir, DirectoryStream.Filter<? super Path> filter):開啟一個目錄流,遍歷目錄中的檔案和子目錄。
  • static Stream<Path> walk(Path start, FileVisitOption... options):遞迴地遍歷目錄樹。
  • static Stream<Path> list(Path dir):列出目錄中的內容,不進行遞迴。

示例程式碼

以下是一些常見的 Files 類方法的示例:

建立檔案和目錄

import java.nio.file.*;
import java.io.IOException;

public class FilesCreateExample {
    public static void main(String[] args) {
        Path directory = Paths.get("exampleDir");
        Path file = directory.resolve("exampleFile.txt");
        
        try {
            // 建立目錄
            if (!Files.exists(directory)) {
                Files.createDirectory(directory);
                System.out.println("目錄已建立: " + directory);
            }
            
            // 建立檔案
            if (!Files.exists(file)) {
                Files.createFile(file);
                System.out.println("檔案已建立: " + file);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

寫入和讀取檔案內容

import java.nio.file.*;
import java.io.IOException;
import java.util.List;

public class FilesReadWriteExample {
    public static void main(String[] args) {
        Path file = Paths.get("exampleDir/exampleFile.txt");
        
        // 寫入位元組陣列到檔案
        String content = "Hello, Java NIO!";
        try {
            Files.write(file, content.getBytes(), StandardOpenOption.WRITE);
            System.out.println("資料已寫入檔案");
        } catch (IOException e) {
            e.printStackTrace();
        }
        
        // 讀取所有位元組
        try {
            byte[] data = Files.readAllBytes(file);
            System.out.println("檔案內容 (位元組): " + new String(data));
        } catch (IOException e) {
            e.printStackTrace();
        }
        
        // 按行讀取檔案內容
        try {
            List<String> lines = Files.readAllLines(file, StandardOpenOption.READ);
            System.out.println("檔案內容 (按行):");
            for (String line : lines) {
                System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

複製和移動檔案

import java.nio.file.*;
import java.io.IOException;

public class FilesCopyMoveExample {
    public static void main(String[] args) {
        Path source = Paths.get("exampleDir/exampleFile.txt");
        Path targetCopy = Paths.get("exampleDir/copyOfExampleFile.txt");
        Path targetMove = Paths.get("exampleDir/movedExampleFile.txt");
        
        try {
            // 複製檔案
            Files.copy(source, targetCopy, StandardCopyOption.REPLACE_EXISTING);
            System.out.println("檔案已複製到: " + targetCopy);
            
            // 移動檔案
            Files.move(source, targetMove, StandardCopyOption.REPLACE_EXISTING);
            System.out.println("檔案已移動到: " + targetMove);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

刪除檔案和目錄

import java.nio.file.*;
import java.io.IOException;

public class FilesDeleteExample {
    public static void main(String[] args) {
        Path file = Paths.get("exampleDir/movedExampleFile.txt");
        Path directory = Paths.get("exampleDir");
        
        try {
            // 刪除檔案
            if (Files.deleteIfExists(file)) {
                System.out.println("檔案已刪除: " + file);
            }
            
            // 刪除目錄(目錄必須為空)
            if (Files.deleteIfExists(directory)) {
                System.out.println("目錄已刪除: " + directory);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

遍歷目錄內容

import java.nio.file.*;
import java.io.IOException;

public class FilesListDirectoryExample {
    public static void main(String[] args) {
        Path directory = Paths.get("exampleDir");
        
        try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory)) {
            System.out.println("目錄中的檔案:");
            for (Path entry : stream) {
                System.out.println(entry.getFileName());
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

獲取和設定檔案屬性

import java.nio.file.*;
import java.nio.file.attribute.FileTime;
import java.io.IOException;

public class FilesAttributesExample {
    public static void main(String[] args) {
        Path file = Paths.get("exampleDir/exampleFile.txt");
        
        try {
            // 獲取檔案大小
            long size = Files.size(file);
            System.out.println("檔案大小: " + size + " 位元組");
            
            // 獲取最後修改時間
            FileTime lastModifiedTime = Files.getLastModifiedTime(file);
            System.out.println("最後修改時間: " + lastModifiedTime);
            
            // 設定最後修改時間為當前時間
            FileTime newTime = FileTime.fromMillis(System.currentTimeMillis());
            Files.setLastModifiedTime(file, newTime);
            System.out.println("最後修改時間已更新");
            
            // 檢查檔案是否存在
            boolean exists = Files.exists(file);
            System.out.println("檔案存在: " + exists);
            
            // 檢查是否為目錄
            boolean isDirectory = Files.isDirectory(file);
            System.out.println("是目錄: " + isDirectory);
            
            // 檢查是否為常規檔案
            boolean isRegularFile = Files.isRegularFile(file);
            System.out.println("是常規檔案: " + isRegularFile);
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

注意事項

  • 異常處理:大多數 Files 類的方法都會丟擲 IOException,因此在使用這些方法時需要適當的異常處理。
  • 原子操作:某些方法(如 Files.move)可以進行原子操作,確保檔案操作的完整性。
  • 效能考慮:對於大檔案或大量檔案操作,考慮使用流式處理方法(如 Files.newBufferedReaderFiles.newBufferedWriter)以提高效能和減少記憶體消耗。

5. PathPathsFiles 的協同使用

這三個元件通常一起使用,以實現對檔案和目錄的全面操作。以下是一個綜合示例,展示瞭如何使用 PathPathsFiles 完成常見的檔案操作任務。

綜合示例

import java.nio.file.*;
import java.io.IOException;
import java.util.List;
import java.nio.charset.StandardCharsets;

public class ComprehensiveFileOperations {
    public static void main(String[] args) {
        Path directory = Paths.get("comprehensiveExampleDir");
        Path file = directory.resolve("exampleFile.txt");
        Path copyFile = directory.resolve("copyOfExampleFile.txt");
        Path movedFile = directory.resolve("movedExampleFile.txt");
        
        try {
            // 1. 建立目錄
            if (!Files.exists(directory)) {
                Files.createDirectory(directory);
                System.out.println("目錄已建立: " + directory);
            }
            
            // 2. 建立檔案
            if (!Files.exists(file)) {
                Files.createFile(file);
                System.out.println("檔案已建立: " + file);
            }
            
            // 3. 寫入資料到檔案
            String content = "Hello, Comprehensive Java NIO!";
            Files.write(file, content.getBytes(StandardCharsets.UTF_8), StandardOpenOption.WRITE);
            System.out.println("資料已寫入檔案: " + file);
            
            // 4. 讀取檔案內容
            List<String> lines = Files.readAllLines(file, StandardCharsets.UTF_8);
            System.out.println("檔案內容:");
            for (String line : lines) {
                System.out.println(line);
            }
            
            // 5. 複製檔案
            Files.copy(file, copyFile, StandardCopyOption.REPLACE_EXISTING);
            System.out.println("檔案已複製到: " + copyFile);
            
            // 6. 移動檔案
            Files.move(file, movedFile, StandardCopyOption.REPLACE_EXISTING);
            System.out.println("檔案已移動到: " + movedFile);
            
            // 7. 獲取檔案屬性
            long size = Files.size(movedFile);
            FileTime lastModifiedTime = Files.getLastModifiedTime(movedFile);
            System.out.println("檔案大小: " + size + " 位元組");
            System.out.println("最後修改時間: " + lastModifiedTime);
            
            // 8. 遍歷目錄中的檔案
            System.out.println("目錄中的檔案:");
            try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory)) {
                for (Path entry : stream) {
                    System.out.println(entry.getFileName());
                }
            }
            
            // 9. 刪除檔案和目錄
            Files.deleteIfExists(copyFile);
            System.out.println("複製的檔案已刪除: " + copyFile);
            
            Files.deleteIfExists(movedFile);
            System.out.println("移動的檔案已刪除: " + movedFile);
            
            Files.deleteIfExists(directory);
            System.out.println("目錄已刪除: " + directory);
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

執行結果示例:

目錄已建立: comprehensiveExampleDir
檔案已建立: comprehensiveExampleDir/exampleFile.txt
資料已寫入檔案: comprehensiveExampleDir/exampleFile.txt
檔案內容:
Hello, Comprehensive Java NIO!
檔案已複製到: comprehensiveExampleDir/copyOfExampleFile.txt
檔案已移動到: comprehensiveExampleDir/movedExampleFile.txt
檔案大小: 31 位元組
最後修改時間: 2024-04-27T10:15:30Z
目錄中的檔案:
copyOfExampleFile.txt
movedExampleFile.txt
複製的檔案已刪除: comprehensiveExampleDir/copyOfExampleFile.txt
移動的檔案已刪除: comprehensiveExampleDir/movedExampleFile.txt
目錄已刪除: comprehensiveExampleDir

解釋

  1. 建立目錄和檔案:使用 Files.createDirectoryFiles.createFile 方法建立目錄和檔案。
  2. 寫入和讀取檔案:使用 Files.write 將字串寫入檔案,使用 Files.readAllLines 讀取檔案內容。
  3. 複製和移動檔案:使用 Files.copy 複製檔案,使用 Files.move 移動檔案。
  4. 獲取檔案屬性:使用 Files.sizeFiles.getLastModifiedTime 獲取檔案的大小和最後修改時間。
  5. 遍歷目錄:使用 Files.newDirectoryStream 遍歷目錄中的檔案。
  6. 刪除檔案和目錄:使用 Files.deleteIfExists 刪除檔案和目錄。

6. 高階功能和最佳實踐

1. 使用檔案過濾器

Files.newDirectoryStream 方法支援使用過濾器來篩選目錄中的檔案。例如,僅列出 .txt 檔案:

import java.nio.file.*;
import java.io.IOException;

public class FilesFilterExample {
    public static void main(String[] args) {
        Path directory = Paths.get("exampleDir");
        
        try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory, "*.txt")) {
            System.out.println("目錄中的 .txt 檔案:");
            for (Path entry : stream) {
                System.out.println(entry.getFileName());
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

2. 使用檔案遍歷器

對於複雜的目錄遍歷,可以使用 Files.walkFileTree 方法結合 FileVisitor 介面,實現自定義的遍歷邏輯。例如,查詢目錄中所有的 .java 檔案:

import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.io.IOException;

public class FilesWalkFileTreeExample {
    public static void main(String[] args) {
        Path startPath = Paths.get("src");
        
        try {
            Files.walkFileTree(startPath, new SimpleFileVisitor<Path>() {
                @Override
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                    if (file.toString().endsWith(".java")) {
                        System.out.println("找到 Java 檔案: " + file);
                    }
                    return FileVisitResult.CONTINUE;
                }
            });
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

3. 非同步檔案操作

雖然 Files 類主要提供同步方法,但結合 Java NIO 的非同步通道(如 AsynchronousFileChannel),可以實現非同步檔案操作,提高效能。

import java.nio.file.*;
import java.nio.channels.*;
import java.nio.ByteBuffer;
import java.io.IOException;
import java.util.concurrent.Future;

public class AsynchronousFileExample {
    public static void main(String[] args) {
        Path file = Paths.get("asyncExample.txt");
        
        try (AsynchronousFileChannel asyncChannel = AsynchronousFileChannel.open(file, StandardOpenOption.WRITE, StandardOpenOption.CREATE)) {
            String content = "Asynchronous File Writing in Java NIO.";
            ByteBuffer buffer = ByteBuffer.wrap(content.getBytes());
            
            Future<Integer> operation = asyncChannel.write(buffer, 0);
            
            while (!operation.isDone()) {
                System.out.println("正在寫入檔案...");
                Thread.sleep(100);
            }
            
            System.out.println("寫入完成,寫入位元組數: " + operation.get());
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}

4. 處理檔案系統差異

NIO.2 支援不同型別的檔案系統(如本地檔案系統、ZIP 檔案系統等)。可以使用 FileSystem 類和相關方法來處理不同的檔案系統。

import java.nio.file.*;
import java.io.IOException;

public class ZipFileSystemExample {
    public static void main(String[] args) {
        Path zipPath = Paths.get("example.zip");
        try (FileSystem zipFs = FileSystems.newFileSystem(zipPath, null)) {
            Path internalPath = zipFs.getPath("/newFile.txt");
            Files.write(internalPath, "內容寫入 ZIP 檔案".getBytes(), StandardOpenOption.CREATE);
            System.out.println("檔案已寫入 ZIP 檔案");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

5. 錯誤處理和資源管理

  • 異常處理:儘量使用具體的異常型別,如 NoSuchFileExceptionDirectoryNotEmptyException 等,以便更精確地處理錯誤。
  • 資源管理:使用 try-with-resources 語句自動關閉流和目錄流,避免資源洩漏。
import java.nio.file.*;
import java.io.IOException;

public class ResourceManagementExample {
    public static void main(String[] args) {
        Path file = Paths.get("exampleDir/exampleFile.txt");
        
        // 使用 try-with-resources 讀取檔案內容
        try (BufferedReader reader = Files.newBufferedReader(file, StandardCharsets.UTF_8)) {
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

6. 效能最佳化

  • 批次操作:儘量批次讀取或寫入資料,減少 I/O 操作的次數。
  • 緩衝流:使用緩衝流(如 BufferedReaderBufferedWriter)提高讀寫效能。
  • 並行處理:對於大規模檔案操作,可以考慮並行處理,如使用多執行緒或並行流。

7. 總結

PathPathsFiles 是 Java NIO.2 中處理檔案和目錄操作的核心元件,提供了比傳統 java.io.File 更加現代化、靈活和高效的功能。以下是它們的主要特點和最佳使用場景:

  • Path

    • 表示檔案系統中的路徑,提供豐富的路徑操作方法。
    • 不同於 String,提供平臺無關的路徑處理。
  • Paths

    • 提供靜態方法 get,簡化 Path 物件的建立過程。
    • 使程式碼更加簡潔和易讀。
  • Files

    • 提供大量靜態方法用於執行檔案和目錄的各種操作,如建立、刪除、複製、移動、讀取、寫入等。
    • Path 緊密整合,支援高階檔案操作和屬性管理。

最佳實踐

  1. 優先使用 NIO.2 的類:在新的專案中,優先使用 PathPathsFiles,而非 java.io.File,以獲得更好的效能和更多功能。
  2. 使用 try-with-resources:確保所有的流和資源在使用後被正確關閉,避免資源洩漏。
  3. 處理具體異常:儘量捕獲和處理具體的異常型別,以便更好地應對不同的錯誤情況。
  4. 最佳化效能:對於大量或大規模的檔案操作,考慮使用緩衝流、批次操作或並行處理來提高效能。
  5. 利用檔案過濾和遍歷器:使用 DirectoryStreamFileVisitor 實現高效的檔案過濾和目錄遍歷。
  6. 保持路徑的不可變性Path 物件是不可變的,這有助於執行緒安全和程式碼的健壯性。

透過充分理解和運用 PathPathsFiles,可以高效地處理 Java 應用中的各種檔案和目錄操作任務,提升程式碼的可維護性和效能。

相關文章