跳轉到

Java IO 與 NIO:檔案操作的基本概念與實踐

引言

在Java程式設計中,檔案操作是一個常見且重要的任務,無論是讀取配置檔案、寫入日誌、處理使用者上傳的檔案,還是管理應用程式的資料存儲,我們都需要進行檔案操作。
Java提供豐富的API來處理檔案,從傳統的java.io包到更現代的java.nio包,為開發者提供多種選擇。

Java檔案操作概述

Java提供多種方式來進行檔案操作,主要分為兩大類:傳統的I/O(java.io包)和新I/O(java.nio包,又稱NIO)。

傳統I/O(java.io)

傳統I/O主要使用以下類別進行檔案操作:

  1. File:用於表示檔案和目錄路徑。
  2. FileInputStream/FileOutputStream:用於讀寫位元資料。
  3. FileReader/FileWriter:用於讀寫字元資料。
  4. BufferedReader/BufferedWriter:為字元輸入/輸出提供緩衝,提高效能。

這些類別提供基本的檔案操作功能,如創建、刪除、重命名檔案,以及讀寫檔案內容。

新I/O(java.nio)

Java NIO(New I/O)在Java 1.4中引入,提供更高效的I/O操作。NIO主要使用以下類別:

  1. Path:表示檔案路徑。
  2. Files:提供大量靜態方法來操作檔案。
  3. FileChannel:用於檔案的讀寫操作。
  4. ByteBuffer:用於緩衝資料。

NIO.2(java.nio.file)

Java 7引入NIO.2,進一步增強檔案操作能力:

  1. 提供更簡單的檔案操作API。
  2. 支援符號連結。
  3. 提供檔案系統的遍歷和監視功能。
  4. 改進檔案屬性的訪問方式。

NIO.2使得檔案操作更加便捷和高效,同時保持與舊版API的兼容性。

使用File類別

File類別是Java I/O中最基本的類別之一,用於表示檔案系統中的檔案和目錄。雖然在Java 7之後,我們有更現代的Path和Files API,但是File類別仍然被廣泛使用,尤其是在處理舊版程式碼時。

創建File物件

創建File物件非常簡單,您可以使用檔案的路徑字串來初始化:

File file = new File("/path/to/file.txt");
File directory = new File("/path/to/directory");

檔案和目錄操作

File類別提供許多方法來操作檔案和目錄:

  1. 創建檔案和目錄:

    boolean created = file.createNewFile(); // 創建新檔案
    boolean dirCreated = directory.mkdir(); // 創建目錄
    boolean dirsCreated = directory.mkdirs(); // 創建多層目錄
    

  2. 檢查檔案是否存在:

    boolean exists = file.exists();
    

  3. 刪除檔案或目錄:

    boolean deleted = file.delete();
    

  4. 重命名檔案或目錄:

    boolean renamed = file.renameTo(new File("/new/path/newname.txt"));
    

  5. 檢查是檔案還是目錄:

    boolean isFile = file.isFile();
    boolean isDirectory = file.isDirectory();
    

獲取檔案資訊

File類別還提供許多方法來獲取檔案的資訊:

long length = file.length(); // 獲取檔案大小(位元組)
long lastModified = file.lastModified(); // 獲取最後修改時間
String name = file.getName(); // 獲取檔案名稱
String path = file.getPath(); // 獲取檔案路徑
String absolutePath = file.getAbsolutePath(); // 獲取絕對路徑

列出目錄內容

您可以使用File類別來列出目錄中的檔案和子目錄:

File[] files = directory.listFiles(); // 獲取目錄中的所有檔案和子目錄
String[] fileNames = directory.list(); // 獲取目錄中的所有檔案和子目錄名稱

注意事項

  • File類別的許多方法在操作失敗時會返回false,而不是拋出異常。這可能會導致錯誤被忽視,所以在使用這些方法時要特別小心。
  • File類別不提供複製檔案的方法,如果需要複製檔案,您需要自己實現或使用Apache Commons IO等第三方庫。
  • 在處理大量檔案或需要更高效能的場景時,考慮使用NIO.2的Path和Files API。

檔案讀寫操作

在Java中,檔案的讀寫操作是最常見的I/O任務之一。Java提供多種方式來進行檔案的讀寫,包括使用傳統的I/O類別和更現代的NIO.2 API。

使用FileInputStream和FileOutputStream

這些類別用於讀寫位元資料:

// 讀取檔案
try (FileInputStream fis = new FileInputStream("input.txt")) {
    int content;
    while ((content = fis.read()) != -1) {
        // 處理讀取的位元資料
        System.out.print((char) content);
    }
} catch (IOException e) {
    e.printStackTrace();
}

// 寫入檔案
try (FileOutputStream fos = new FileOutputStream("output.txt")) {
    String data = "Hello, World!";
    fos.write(data.getBytes());
} catch (IOException e) {
    e.printStackTrace();
}

使用FileReader和FileWriter

這些類別用於讀寫字元資料:

// 讀取檔案
try (FileReader fr = new FileReader("input.txt")) {
    int content;
    while ((content = fr.read()) != -1) {
        System.out.print((char) content);
    }
} catch (IOException e) {
    e.printStackTrace();
}

// 寫入檔案
try (FileWriter fw = new FileWriter("output.txt")) {
    fw.write("你好,世界!");
} catch (IOException e) {
    e.printStackTrace();
}

使用BufferedReader和BufferedWriter

這些類別提供緩衝功能,可以提高讀寫效能:

// 讀取檔案
try (BufferedReader br = new BufferedReader(new FileReader("input.txt"))) {
    String line;
    while ((line = br.readLine()) != null) {
        System.out.println(line);
    }
} catch (IOException e) {
    e.printStackTrace();
}

// 寫入檔案
try (BufferedWriter bw = new BufferedWriter(new FileWriter("output.txt"))) {
    bw.write("這是第一行");
    bw.newLine();
    bw.write("這是第二行");
} catch (IOException e) {
    e.printStackTrace();
}

使用NIO.2的Files類別

Java 7引入的NIO.2提供更簡潔的檔案讀寫方法:

import java.nio.file.*;

// 讀取檔案
try {
    List<String> lines = Files.readAllLines(Paths.get("input.txt"));
    for (String line : lines) {
        System.out.println(line);
    }
} catch (IOException e) {
    e.printStackTrace();
}

// 寫入檔案
try {
    List<String> lines = Arrays.asList("第一行", "第二行", "第三行");
    Files.write(Paths.get("output.txt"), lines);
} catch (IOException e) {
    e.printStackTrace();
}

大檔案處理

對於大檔案,建議使用緩衝串流或NIO的Channel和Buffer:

// 使用FileChannel讀取大檔案
try (FileChannel channel = FileChannel.open(Paths.get("bigfile.dat"), StandardOpenOption.READ)) {
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    while (channel.read(buffer) != -1) {
        buffer.flip();
        // 處理緩衝區中的資料
        buffer.clear();
    }
} catch (IOException e) {
    e.printStackTrace();
}

注意事項

  1. 始終使用try-with-resources語句來確保資源被正確關閉。
  2. 考慮檔案的編碼,特別是在處理非ASCII文字時。
  3. 對於大檔案,使用緩衝串流或NIO來提高效能。
  4. 在多執行緒環境中進行檔案操作時,要注意同步問題。

目錄操作

在Java中,目錄操作是檔案系統管理的重要部分。無論是創建新目錄、列出目錄內容,還是遍歷目錄樹,Java都提供豐富的API來處理這些任務。

使用File類別操作目錄

  1. 創建目錄:

    File dir = new File("newDirectory");
    boolean created = dir.mkdir(); // 創建單層目錄
    boolean createdMultiple = dir.mkdirs(); // 創建多層目錄
    

  2. 列出目錄內容:

    File dir = new File("myDirectory");
    String[] fileList = dir.list(); // 獲取檔案名稱列表
    File[] files = dir.listFiles(); // 獲取File物件列表
    

  3. 刪除目錄:

    boolean deleted = dir.delete(); // 只能刪除空目錄
    

使用NIO.2的Files和Path類別

  1. 創建目錄:

    Path path = Paths.get("newDirectory");
    try {
        Files.createDirectory(path);
        // 或者創建多層目錄
        Files.createDirectories(path);
    } catch (IOException e) {
        e.printStackTrace();
    }
    

  2. 列出目錄內容:

    try (DirectoryStream<Path> stream = Files.newDirectoryStream(Paths.get("myDirectory"))) {
        for (Path file: stream) {
            System.out.println(file.getFileName());
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    

  3. 遍歷目錄樹:

    Path start = Paths.get("rootDirectory");
    try {
        Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                System.out.println(file.toString());
                return FileVisitResult.CONTINUE;
            }
        });
    } catch (IOException e) {
        e.printStackTrace();
    }
    

  4. 刪除目錄:

    Path path = Paths.get("directoryToDelete");
    try {
        Files.delete(path); // 只能刪除空目錄
        // 或者使用遞迴刪除非空目錄
        Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                Files.delete(file);
                return FileVisitResult.CONTINUE;
            }
    
            @Override
            public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
                Files.delete(dir);
                return FileVisitResult.CONTINUE;
            }
        });
    } catch (IOException e) {
        e.printStackTrace();
    }
    

監視目錄變化

Java NIO.2提供監視目錄變化的功能:

Path dir = Paths.get("watchedDirectory");
try (WatchService watchService = FileSystems.getDefault().newWatchService()) {
    dir.register(watchService, StandardWatchEventKinds.ENTRY_CREATE, 
                               StandardWatchEventKinds.ENTRY_DELETE, 
                               StandardWatchEventKinds.ENTRY_MODIFY);
    while (true) {
        WatchKey key = watchService.take();
        for (WatchEvent<?> event : key.pollEvents()) {
            System.out.println("Event kind: " + event.kind() + ". File affected: " + event.context() + ".");
        }
        key.reset();
    }
} catch (IOException | InterruptedException e) {
    e.printStackTrace();
}

注意事項

  1. 在刪除目錄時要小心,特別是使用遞迴刪除時。
  2. 使用try-with-resources語句來確保資源(如DirectoryStream)被正確關閉。
  3. 在多執行緒環境中操作目錄時,要注意同步問題。
  4. 使用NIO.2的Path和Files API可以提供更好的效能和更豐富的功能。

檔案屬性操作

檔案屬性包括檔案的大小、創建時間、最後修改時間、權限等資訊。Java提供多種方式來獲取和修改這些屬性。

使用File類別

File類別提供一些基本的方法來獲取檔案屬性:

File file = new File("example.txt");

// 獲取檔案大小(位元組)
long size = file.length();

// 獲取最後修改時間
long lastModified = file.lastModified();

// 檢查檔案權限
boolean canRead = file.canRead();
boolean canWrite = file.canWrite();
boolean canExecute = file.canExecute();

// 修改檔案權限
boolean setReadable = file.setReadable(true);
boolean setWritable = file.setWritable(true);
boolean setExecutable = file.setExecutable(true);

使用NIO.2的Files類別

NIO.2提供更豐富的檔案屬性操作方法:

Path path = Paths.get("example.txt");

// 獲取基本屬性
BasicFileAttributes attr = Files.readAttributes(path, BasicFileAttributes.class);
System.out.println("創建時間: " + attr.creationTime());
System.out.println("最後訪問時間: " + attr.lastAccessTime());
System.out.println("最後修改時間: " + attr.lastModifiedTime());
System.out.println("檔案大小: " + attr.size());

// 修改檔案時間屬性
FileTime newLastModifiedTime = FileTime.fromMillis(System.currentTimeMillis());
Files.setLastModifiedTime(path, newLastModifiedTime);

// 獲取和設置POSIX檔案權限(僅在支援POSIX的系統上)
try {
    Set<PosixFilePermission> permissions = Files.getPosixFilePermissions(path);
    permissions.add(PosixFilePermission.OWNER_EXECUTE);
    Files.setPosixFilePermissions(path, permissions);
} catch (UnsupportedOperationException e) {
    System.out.println("This file system does not support POSIX file permissions");
}

// 獲取檔案擁有者
UserPrincipal owner = Files.getOwner(path);
System.out.println("檔案擁有者: " + owner.getName());

// 設置檔案擁有者(需要適當的系統權限)
UserPrincipalLookupService lookupService = FileSystems.getDefault().getUserPrincipalLookupService();
UserPrincipal newOwner = lookupService.lookupPrincipalByName("newowner");
Files.setOwner(path, newOwner);

使用FileStore獲取檔案系統資訊

Path path = Paths.get("example.txt");
FileStore store = Files.getFileStore(path);

System.out.println("檔案系統: " + store.name());
System.out.println("總空間: " + store.getTotalSpace());
System.out.println("可用空間: " + store.getUsableSpace());
System.out.println("未分配空間: " + store.getUnallocatedSpace());

檔案屬性視圖

NIO.2引入檔案屬性視圖的概念,允許訪問特定檔案系統的屬性:

Path path = Paths.get("example.txt");

// 獲取DOS屬性視圖(僅在支援DOS屬性的檔案系統上可用)
try {
    DosFileAttributes dosAttr = Files.readAttributes(path, DosFileAttributes.class);
    System.out.println("是否為隱藏檔案: " + dosAttr.isHidden());
    System.out.println("是否為系統檔案: " + dosAttr.isSystem());
} catch (UnsupportedOperationException e) {
    System.out.println("This file system does not support DOS attributes");
}

注意事項

  1. 某些檔案屬性操作可能需要特殊的系統權限。
  2. 不同的檔案系統可能支援不同的屬性集。在使用特定屬性前,應該先檢查是否支援。
  3. 修改檔案屬性可能會影響其他應用程式對該檔案的訪問,請謹慎操作。
  4. 在處理大量檔案的屬性時,考慮使用批量操作以提高效能。

NIO.2中的檔案操作

Java 7引入的NIO.2(New I/O 2)改進Java的檔案操作能力。NIO.2提供更簡潔、更強大的API,使得檔案和目錄操作變得更加容易和高效。

Path介面

Path是NIO.2中的核心概念,代表檔案系統中的路徑:

Path path = Paths.get("example.txt");
Path absolutePath = path.toAbsolutePath();
Path parentPath = path.getParent();
Path fileName = path.getFileName();

Files類別

Files類別提供大量靜態方法來操作檔案和目錄:

  1. 檔案操作:

    // 創建檔案
    Files.createFile(Paths.get("newfile.txt"));
    
    // 複製檔案
    Files.copy(Paths.get("source.txt"), Paths.get("destination.txt"));
    
    // 移動檔案
    Files.move(Paths.get("oldname.txt"), Paths.get("newname.txt"));
    
    // 刪除檔案
    Files.delete(Paths.get("tobedeleted.txt"));
    

  2. 讀寫操作:

    // 讀取所有行
    List<String> lines = Files.readAllLines(Paths.get("file.txt"));
    
    // 寫入所有行
    List<String> linesToWrite = Arrays.asList("Line 1", "Line 2", "Line 3");
    Files.write(Paths.get("output.txt"), linesToWrite);
    
    // 使用BufferedReader讀取大檔案
    try (BufferedReader reader = Files.newBufferedReader(Paths.get("largefile.txt"))) {
        String line;
        while ((line = reader.readLine()) != null) {
            // 處理每一行
        }
    }
    

  3. 目錄操作:

    // 創建目錄
    Files.createDirectory(Paths.get("newdir"));
    
    // 創建多層目錄
    Files.createDirectories(Paths.get("path/to/newdir"));
    
    // 列出目錄內容
    try (DirectoryStream<Path> stream = Files.newDirectoryStream(Paths.get("mydir"))) {
        for (Path entry: stream) {
            System.out.println(entry.getFileName());
        }
    }
    

檔案系統操作

NIO.2提供FileSystem類別來操作檔案系統:

// 獲取默認檔案系統
FileSystem fs = FileSystems.getDefault();

// 獲取所有根目錄
for (Path root : fs.getRootDirectories()) {
    System.out.println(root);
}

// 獲取檔案存儲
for (FileStore store : fs.getFileStores()) {
    System.out.println(store);
}

檔案監視服務

NIO.2引入WatchService,用於監視目錄變化:

WatchService watchService = FileSystems.getDefault().newWatchService();
Path path = Paths.get("watchedDir");
path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE, 
                            StandardWatchEventKinds.ENTRY_DELETE, 
                            StandardWatchEventKinds.ENTRY_MODIFY);

while (true) {
    WatchKey key = watchService.take();
    for (WatchEvent<?> event : key.pollEvents()) {
        System.out.println("Event kind: " + event.kind() + ". File affected: " + event.context() + ".");
    }
    key.reset();
}

SeekableByteChannel

NIO.2提供SeekableByteChannel介面,允許在檔案中隨機訪問:

try (SeekableByteChannel channel = Files.newByteChannel(Paths.get("file.txt"))) {
    ByteBuffer buffer = ByteBuffer.allocate(10);
    channel.position(100); // 移動到檔案的第100個位元組
    channel.read(buffer);
    buffer.flip();
    // 處理讀取的資料
}

優點和注意事項

  1. NIO.2提供更一致和更強大的檔案操作API。
  2. Path介面比File類別更靈活,支援不同的檔案系統。
  3. Files類別的方法通常比傳統I/O更高效。
  4. NIO.2支援符號連結和檔案屬性的高級操作。
  5. 使用try-with-resources語句來自動關閉資源。
  6. 注意處理可能拋出的IOException。

NIO.2簡化Java中的檔案操作,提供更現代、更強大的API。
對於新的專案,建議優先考慮使用NIO.2來進行檔案和目錄操作。

實踐和注意事項

在進行Java檔案操作時,遵循一些最佳實踐可以幫助您寫出更安全、更高效的程式碼。以下是一些重要的建議和注意事項:

1. 使用try-with-resources語句

始終使用try-with-resources語句來自動關閉資源,這可以防止資源洩漏:

try (BufferedReader reader = Files.newBufferedReader(Paths.get("file.txt"))) {
    // 使用reader
} catch (IOException e) {
    e.printStackTrace();
}

2. 正確處理異常

不要忽視或吞掉異常。適當地處理IOException和其他可能的異常:

try {
    Files.move(source, target);
} catch (IOException e) {
    System.err.println("無法移動檔案: " + e.getMessage());
    // 根據需要進行錯誤處理或重試
}

3. 使用NIO.2 API

對於新的專案,優先考慮使用NIO.2(java.nio.file包)而不是舊的java.io.File類別。NIO.2提供更強大和一致的API。

4. 考慮檔案系統的差異

記住不同的操作系統可能有不同的檔案系統行為。例如,檔案路徑分隔符、檔案名稱大小寫敏感性等:

String separator = File.separator; // 使用系統特定的路徑分隔符

5. 檢查檔案存在性和權限

在操作檔案之前,檢查檔案是否存在以及是否有適當的權限:

Path path = Paths.get("example.txt");
if (Files.exists(path) && Files.isReadable(path)) {
    // 進行檔案操作
}

6. 使用緩衝進行大檔案操作

對於大檔案的讀寫操作,使用緩衝串流或NIO的Channel和Buffer來提高效能:

try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("largefile.dat"))) {
    // 使用緩衝串流讀取
}

7. 安全地刪除檔案

在刪除檔案或目錄時要小心,特別是在遞迴刪除目錄時:

Files.walkFileTree(Paths.get("directory"), new SimpleFileVisitor<Path>() {
    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
        Files.delete(file);
        return FileVisitResult.CONTINUE;
    }

    @Override
    public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
        Files.delete(dir);
        return FileVisitResult.CONTINUE;
    }
});

8. 使用相對路徑

盡可能使用相對路徑而不是絕對路徑,這可以提高程式的可移植性:

Path relativePath = Paths.get("resources", "config.properties");

9. 注意檔案鎖定

在多執行緒或多行程環境中,使用檔案鎖定來防止資料損壞:

try (FileChannel channel = FileChannel.open(path, StandardOpenOption.WRITE)) {
    FileLock lock = channel.lock();
    try {
        // 執行需要鎖定的操作
    } finally {
        lock.release();
    }
}

10. 定期備份重要資料

在進行關鍵的檔案操作之前,考慮備份重要資料:

Files.copy(source, backup, StandardCopyOption.REPLACE_EXISTING);

11. 使用適當的字元編碼

在讀寫文字檔案時,明確指定字元編碼以避免編碼問題:

Charset utf8 = StandardCharsets.UTF_8;
List<String> lines = Files.readAllLines(path, utf8);

本篇文章同步刊載iThome: iThome
筆者個人的網站: JUNYI