《Java 高階篇》六:I/O 流

ACatSmiling發表於2024-10-02

Author: ACatSmiling

Since: 2024-10-01

字元編碼

字符集 Charset:也叫編碼表。是一個系統支援的所有字元的集合,包括各國家文字、標點符號、圖形符號、數字等。

編碼表的由來:計算機只能識別二進位制資料,早期由來是電訊號。為了方便應用計算機,讓它可以識別各個國家的文字。就將各個國家的文字用數字來表示,並一一對應,形成一張表。這就是編碼表。

常見的編碼表:

  • ASCII:美國標準資訊交換碼。用一個位元組的 7 位可以表示。

  • ISO8859-1:拉丁碼錶,歐洲碼錶。用一個位元組的 8 位表示。

  • GB2312:中國的中文編碼表。最多兩個位元組編碼所有字元。

  • GBK:中國的中文編碼表升級,融合了更多的中文文字元號。最多兩個位元組編碼。

  • Unicode:國際標準碼,融合了目前人類使用的所有字元。為每個字元分配唯一的字元碼。所有的文字都用兩個位元組來表示。

  • UTF-8:變長的編碼方式,可用 1 ~ 4 個位元組來表示一個字元。

在 Unicode 出現之前,所有的字符集都是和具體編碼方案繫結在一起的,即字符集 ≈ 編碼方式,都是直接將字元和最終位元組流繫結死了。

image-20210404191338053

  • GBK 等雙位元組編碼方式,用最高位是 1 或 0 表示兩個位元組和一個位元組。

  • Unicode 不完美,這裡就有三個問題,一個是,我們已經知道,英文字母只用一個位元組表示就夠了,第二個問題是如何才能區別 Unicode 和 ASCII,計算機怎麼知道是兩個位元組表示一個符號,而不是分別表示兩個符號呢?第三個,如果和 GBK 等雙位元組編碼方式一樣,用最高位是 1 或 0 表示兩個位元組和一個位元組,就少了很多值無法用於表示字元,不夠表示所有字元。Unicode 在很長一段時間內無法推廣,直到網際網路的出現。

  • 面向傳輸的 UTF(UCS Transfer Format)標準出現了,顧名思義,UTF-8 就是每次 8 個位傳輸資料,而 UTF-16 就是每次 16 個位。這是為傳輸而設計的編碼,並使編碼無國界,這樣就可以顯示全世界上所有文化的字元了。

  • Unicode 只是定義了一個龐大的、全球通用的字符集,併為每個字元規定了唯一確定的編號,具體儲存成什麼樣的位元組流,取決於字元編碼方案。推薦的 Unicode 編碼是 UTF-8 和 UTF-16。

    image-20210401205524825

    image-20210401210058680

計算機中儲存的資訊都是用二進位制數表示的,而能在螢幕上看到的數字、英文、標點符號、漢字等字元是二進位制數轉換之後的結果。按照某種規則,將字元儲存到計算機中,稱為編碼 。反之,將儲存在計算機中的二進位制數按照某種規則解析顯示出來,稱為解碼

  • 編碼規則和解碼規則要對應,否則會導致亂碼。比如說,按照 A 規則儲存,同樣按照 A 規則解析,那麼就能顯示正確的文字符號。反之,按照 A 規則儲存,再按照 B 規則解析,就會導致亂碼現象。

  • 編碼: 字串 ---> 位元組陣列。

  • 解碼: 位元組陣列 ---> 字串。

  • 啟示:客戶端/瀏覽器端 <------> 後臺(Java,GO,Python,Node.js,php...) <------> 資料庫,要求前前後後使用的字符集要統一,都使用 UTF-8,這樣才不會亂碼。

File 類

路徑分隔符

路徑分隔符

  • 路徑中的每級目錄之間用一個路徑分隔符隔開。

  • 路徑分隔符和系統有關:

    • Windows 和 DOS 系統預設使用\來表示。
    • UNIX 和 URL 使用/來表示。
  • Java 程式支援跨平臺執行,因此路徑分隔符要慎用。為了解決這個隱患,File 類提供了一個常量public static final String separator,能夠根據作業系統,動態的提供分隔符。

  • 示例:

    File file1 = new File("d:\\test\\info.txt");
    File file2 = new File("d:/test/info.txt");
    File file3 = new File("d:" + File.separator + "test" + File.separator + "info.txt");
    

File 定義

java.io.File類:檔案和檔案目錄路徑的抽象表示形式,與平臺無關。

  • File 主要表示類似D:\\檔案目錄1D:\\檔案目錄1\\檔案.txt,前者是資料夾(directory),後者則是檔案(file),而 File 類就是操作這兩者的類。

File 能新建、刪除、重新命名檔案和目錄,但 File 不能訪問檔案內容本身。如果需要訪問檔案內容本身,則需要使用輸入/輸出流。

  • File 跟流無關,File 類不能對檔案進行讀和寫,也就是輸入和輸出。

想要在 Java 程式中表示一個真實存在的檔案或目錄,那麼必須有一個 File 物件,但是 Java 程式中的一個 File 物件,可能不對應一個真實存在的檔案或目錄。

image-20210330104549637

File 構造方法

File 常用的構造方法:

  • public File(String pathname) :以 pathname 為路徑建立 File 物件,可以是絕對路徑或者相對路徑,如果 pathname 是相對路徑,則預設的當前路徑在系統屬性 user.dir 中儲存。

    • 絕對路徑:是一個固定的路徑,從磁碟機代號開始。

    • 相對路徑:是相對於某個位置開始。

    • IDEA 中的路徑說明,main() 和 Test 中,相對路徑不一樣:

      public class Test {
          public static void main(String[] args) {
              File file = new File("hello.txt");// 相較於當前工程
              System.out.println(file.getAbsolutePath());// D:\JetBrainsWorkSpace\IDEAProjects\xisun-projects\hello.txt
          }
      
          @Test
          public void testFileReader() {
              File file = new File("hello.txt");// 相較於當前Module
              System.out.println(file.getAbsolutePath());// D:\JetBrainsWorkSpace\IDEAProjects\xisun-projects\xisun-java_base\hello.txt
          }
      }
      
  • public File(String parent, String child) :以 parent 為父路徑,child 為子路徑建立 File 物件。

  • public File(File parent, String child) :根據一個父 File 物件和子檔案路徑建立 File 物件。

File 基礎資訊

獲取 File 基礎資訊的方法:

  • public String getAbsolutePath():獲取絕對路徑。

  • public String getPath():獲取路徑。

  • public String getName():獲取名稱。

  • public String getParent():獲取上層檔案目錄路徑。若無,返回 null。

  • public long length():獲取檔案長度,即:位元組數。不能獲取目錄的長度。

  • public long lastModified():獲取最後一次的修改時間,毫秒值。

  • public String[] list():獲取指定目錄下的所有檔案或者檔案目錄的名稱陣列,如果指定目錄不存在,返回 null。

  • public File[] listFiles():獲取指定目錄下的所有檔案或者檔案目錄的 File 陣列,如果指定目錄不存在,返回 null。

  • public String[] list(FilenameFilter filter):指定檔案過濾器。

  • public File[] listFiles(FilenameFilter filter):指定檔案過濾器。

  • public File[] listFiles(FileFilter filter):指定檔案過濾器。

File 重新命名

File 重新命名的方法:

  • public boolean renameTo(File dest):把檔案重新命名為指定的檔案路徑。以file1.renameTo(file2)為例:要想保證返回 true,需要 file1 在硬碟中是存在的,且 file2 在硬碟中不能存在。

File 判斷

判斷 File 相關資訊的方法:

  • public boolean exists():判斷是否存在。

  • public boolean isDirectory():判斷是否是檔案目錄。

  • public boolean isFile():判斷是否是檔案。

  • public boolean canRead():判斷是否可讀。

  • public boolean canWrite():判斷是否可寫。

  • public boolean isHidden():判斷是否隱藏。

File 建立

建立 File 的方法:

  • public boolean createNewFile():建立檔案。若檔案不存在,則建立一個新的空檔案並返回 true;若檔案存在,則不建立檔案並返回 false。
  • public boolean mkdir():建立檔案目錄。如果此檔案目錄存在,則不建立;如果此檔案目錄的上層目錄不存在,也不建立。
  • public boolean mkdirs():建立檔案目錄。如果上層檔案目錄不存在,也一併建立。
  • 如果建立檔案或者檔案目錄時,沒有寫磁碟機代號路徑,那麼,預設在專案路徑下。

File 刪除

刪除 File 的方法:

  • public boolean delete():刪除檔案或者資料夾。
  • Java 中的刪除不走回收站。要刪除一個檔案目錄,請注意該檔案目錄內不能包含檔案或者檔案目錄,即只能刪除空的檔案目錄

File 遍歷

遞迴遍歷資料夾下所有檔案以及子檔案:

public class Test {
    public static void main(String[] args) {
        // 遞迴:檔案目錄
        /** 列印出指定目錄所有檔名稱,包括子檔案目錄中的檔案 */

        // 1. 建立目錄物件
        File dir = new File("E:\\teach\\01_javaSE\\_尚矽谷Java程式語言\\3_軟體");

        // 2. 列印目錄的子檔案
        printSubFile(dir);
    }

    // 方式一:
    public static void printSubFile(File dir) {
        // 判斷傳入的是否是目錄
        if (!dir.isDirectory()) {
            // 不是目錄直接退出
            return;
        }

        // 列印目錄的子檔案
        File[] subfiles = dir.listFiles();
        if (subfiles != null) {
            for (File f : subfiles) {
                if (f.isDirectory()) {
                    // 檔案目錄
                    printSubFile(f);
                } else {
                    // 檔案
                    System.out.println(f.getAbsolutePath());
                }
            }
        }
    }

    // 方式二:迴圈實現
    // 列出 file 目錄的下級內容,僅列出一級的話,使用 File 類的 String[] list() 比較簡單
    public void listSubFiles(File file) {
        if (file.isDirectory()) {
            String[] all = file.list();
            if (all != null) {
                for (String s : all) {
                    System.out.println(s);
                }
            }
        } else {
            System.out.println(file + "是檔案!");
        }
    }

    // 方式三:列出 file 目錄的下級,如果它的下級還是目錄,接著列出下級的下級,依次類推
    // 建議使用 File 類的 File[] listFiles()
    public void listAllSubFiles(File file) {
        if (file.isFile()) {
            System.out.println(file);
        } else {
            File[] all = file.listFiles();
            // 如果 all[i] 是檔案,直接列印
            // 如果 all[i] 是目錄,接著再獲取它的下一級
            if (all != null) {
                for (File f : all) {
                    // 遞迴呼叫:自己呼叫自己就叫遞迴
                    listAllSubFiles(f);
                }
            }
        }
    }

    // 擴充 1:計算指定目錄所在空間的大小
    // 求任意一個目錄的總大小
    public long getDirectorySize(File file) {
        // file 是檔案,那麼直接返回 file.length()
        // file 是目錄,把它的下一級的所有大小加起來就是它的總大小
        long size = 0;
        if (file.isFile()) {
            size += file.length();
        } else {
            // 獲取 file 的下一級
            File[] all = file.listFiles();
            if (all != null) {
                // 累加 all[i] 的大小
                for (File f : all) {
                    // f 的大小
                    size += getDirectorySize(f);
                }
            }
        }
        return size;
    }

    // 擴充 2:刪除指定檔案目錄及其下的所有檔案
    public void deleteDirectory(File file) {
        // 如果 file 是檔案,直接 delete
        // 如果 file 是目錄,先把它的下一級幹掉,然後刪除自己
        if (file.isDirectory()) {
            File[] all = file.listFiles();
            // 迴圈刪除的是 file 的下一級
            if (all != null) {
                // f 代表 file 的每一個下級
                for (File f : all) {
                    deleteDirectory(f);
                }
            }
        }
        // 刪除自己
        file.delete();
    }
}

NIO.2 中 Path 、Paths 、Files

Java NIO (New IO 或 Non-Blocking IO)是從 Java 1.4 版本開始引入的一套新的 I/O API,可以替代標準的 Java I/O API。NIO 與原來的 I/O 有同樣的作用和目的,但是使用的方式完全不同,NIO 支援面向緩衝區的(I/O是面向流的)、基於通道的 I/O 操作,NIO 也會以更加高效的方式進行檔案的讀寫操作。

Java API 中提供了兩套 NIO,一套是針對標準輸入輸出 NIO,另一套就是網路程式設計 NIO。

  • |----- java.nio.channels.Channel
    • |----- FileChannel:處理本地檔案。
      • |----- SocketChannel:TCP 網路程式設計的客戶端的 Channel。
      • |----- ServerSocketChannel:TCP 網路程式設計的伺服器端的 Channel。
      • |----- DatagramChannel:UDP 網路程式設計中傳送端和接收端的 Channel。

隨著 JDK 7 的釋出,Java 對 NIO 進行了極大的擴充套件,增強了對檔案處理和檔案系統特性的支援,以至於我們稱他們為 NIO.2。因為 NIO 提供的一些功能,NIO 已經成為檔案處理中越來越重要的部分。

早期的 Java 只提供了一個 File 類來訪問檔案系統,但 File 類的功能比較有限,所提供的方法效能也不高。而且,大多數方法在出錯時僅返回失敗,並不會提供異常資訊。

NIO. 2 為了彌補這種不足,引入了 Path 介面,代表一個平臺無關的平臺路徑,描述了目錄結構中檔案的位置。Path 可以看成是 File 類的升級版本,實際引用的資源也可以不存在。

在以前 I/O 操作是類似如下寫法的:

import java.io.File;

File file = new File("index.html");

但在 JDK 7 中,我們可以這樣寫:

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

Path path = Paths.get("index.html");

同時,NIO.2 在java.nio.file包下還提供了 Files、Paths 工具類,Files 包含了大量靜態的工具方法來操作檔案;Paths 則包含了兩個返回 Path 的靜態工廠方法。

Paths 類提供的獲取 Path 物件的方法:

  • static Path get(String first, String … more):用於將多個字串串連成路徑。

  • static Path get(URI uri):返回指定 uri 對應的 Path 路徑。

    public class PathTest {
        /*
        如何使用Paths例項化Path
         */
        @Test
        public void test1() {
            Path path1 = Paths.get("d:\\nio\\hello.txt");// = new File(String filepath)
            System.out.println(path1);
    
            Path path2 = Paths.get("d:\\", "nio\\hello.txt");// = new File(String parent,String filename);
            System.out.println(path2);
    
            Path path3 = Paths.get("d:\\", "nio");
            System.out.println(path3);
        }
    }
    

Path 類常用方法:

  • String toString():返回撥用 Path 物件的字串表示形式。

  • boolean startsWith(String path):判斷是否以 path 路徑開始。

  • boolean endsWith(String path):判斷是否以 path 路徑結束。

  • boolean isAbsolute():判斷是否是絕對路徑。

  • Path getParent():返回 Path 物件包含整個路徑,不包含 Path 物件指定的檔案路徑。

  • Path getRoot():返回撥用 Path 物件的根路徑。

  • Path getFileName():返回與呼叫 Path 物件關聯的檔名。

  • int getNameCount():返回 Path 根目錄後面元素的數量。

  • Path getName(int idx):返回指定索引位置 idx 的路徑名稱。

  • Path toAbsolutePath():作為絕對路徑返回撥用 Path 物件。

  • Path resolve(Path p):合併兩個路徑,返回合併後的路徑對應的 Path 物件。

  • File toFile():將 Path 轉化為 File 類的物件。File 類轉化為 Path 物件的方法是:Path toPath()

    public class PathTest {
        /*
        Path中的常用方法
         */
        @Test
        public void test2() {
            Path path1 = Paths.get("d:\\", "nio\\nio1\\nio2\\hello.txt");
            Path path2 = Paths.get("hello1.txt");// 相對當前Module的路徑
    
            // String toString():返回撥用Path物件的字串表示形式
            System.out.println(path1);// d:\nio\nio1\nio2\hello.txt
            // boolean startsWith(String path): 判斷是否以path路徑開始
            System.out.println(path1.startsWith("d:\\nio"));// true
            // boolean endsWith(String path): 判斷是否以path路徑結束
            System.out.println(path1.endsWith("hello.txt"));// true
            // boolean isAbsolute(): 判斷是否是絕對路徑
            System.out.println(path1.isAbsolute() + "~");// true~
            System.out.println(path2.isAbsolute() + "~");// false~
            // Path getParent():返回Path物件包含整個路徑,不包含Path物件指定的檔案路徑
            System.out.println(path1.getParent());// d:\nio\nio1\nio2
            System.out.println(path2.getParent());// null
            // Path getRoot():返回撥用Path物件的根路徑
            System.out.println(path1.getRoot());// d:\
            System.out.println(path2.getRoot());// null
            // Path getFileName(): 返回與呼叫Path物件關聯的檔名
            System.out.println(path1.getFileName() + "~");// hello.txt~
            System.out.println(path2.getFileName() + "~");// hello1.txt~
            // int getNameCount(): 返回Path根目錄後面元素的數量
            // Path getName(int idx): 返回指定索引位置idx的路徑名稱
            for (int i = 0; i < path1.getNameCount(); i++) {
                // nio*****nio1*****nio2*****hello.txt*****
                System.out.print(path1.getName(i) + "*****");
            }
            System.out.println();
    
            // Path toAbsolutePath(): 作為絕對路徑返回撥用Path物件
            System.out.println(path1.toAbsolutePath());// d:\nio\nio1\nio2\hello.txt
            System.out.println(path2.toAbsolutePath());// D:\xisun-projects\java_base\hello1.txt
            // Path resolve(Path p): 合併兩個路徑,返回合併後的路徑對應的Path物件
            Path path3 = Paths.get("d:\\", "nio");
            Path path4 = Paths.get("nioo\\hi.txt");
            path3 = path3.resolve(path4);
            System.out.println(path3);// d:\nio\nioo\hi.txt
    
            // File toFile(): 將Path轉化為File類的物件
            File file = path1.toFile();// Path--->File的轉換
            // Path toPath(): 將File轉化為Path類的物件
            Path newPath = file.toPath();// File--->Path的轉換
        }
    }
    

java.nio.file.Files:用於操作檔案或目錄的工具類。常用方法:

  • Path copy(Path src, Path dest, CopyOption … how):檔案的複製。

  • Path createDirectory(Path path, FileAttribute<?> … attr):建立一個目錄。

  • Path createFile(Path path, FileAttribute<?> … arr):建立一個檔案。

  • void delete(Path path):刪除一個檔案/目錄,如果不存在,執行報錯。

  • void deleteIfExists(Path path):Path 對應的檔案/目錄如果存在,執行刪除。

  • Path move(Path src, Path dest, CopyOption…how):將 src 移動到 dest 位置。

  • long size(Path path):返回 path 指定檔案的大小。

  • boolean exists(Path path, LinkOption … opts):判斷檔案是否存在。

  • boolean isDirectory(Path path, LinkOption … opts):判斷是否是目錄。

  • boolean isRegularFile(Path path, LinkOption … opts):判斷是否是檔案。

  • boolean isHidden(Path path):判斷是否是隱藏檔案。

  • boolean isReadable(Path path):判斷檔案是否可讀。

  • boolean isWritable(Path path):判斷檔案是否可寫。

  • boolean notExists(Path path, LinkOption … opts):判斷檔案是否不存在。

  • SeekableByteChannel newByteChannel(Path path, OpenOption…how):獲取與指定檔案的連線,how 指定開啟方式。

  • DirectoryStream\<Path> newDirectoryStream(Path path):開啟 path 指定的目錄。

  • InputStream newInputStream(Path path, OpenOption…how):獲取 InputStream 物件。

  • OutputStream newOutputStream(Path path, OpenOption…how):獲取 OutputStream 物件。

    public class FilesTest {
        @Test
        public void test1() throws IOException {
            Path path1 = Paths.get("d:\\nio", "hello.txt");
            Path path2 = Paths.get("atguigu.txt");
    
            // Path copy(Path src, Path dest, CopyOption … how): 檔案的複製
            // 要想複製成功,要求path1對應的物理上的檔案存在。path2 對應的檔案沒有要求。
            // Files.copy(path1, path2, StandardCopyOption.REPLACE_EXISTING);
    
            // Path createDirectory(Path path, FileAttribute<?> … attr): 建立一個目錄
            // 要想執行成功,要求path對應的物理上的檔案目錄不存在。一旦存在,丟擲異常。
            Path path3 = Paths.get("d:\\nio\\nio1");
            // Files.createDirectory(path3);
    
            // Path createFile(Path path, FileAttribute<?> … arr): 建立一個檔案
            // 要想執行成功,要求path對應的物理上的檔案不存在。一旦存在,丟擲異常。
            Path path4 = Paths.get("d:\\nio\\hi.txt");
            // Files.createFile(path4);
    
            // void delete(Path path): 刪除一個檔案/目錄,如果不存在,執行報錯
            // Files.delete(path4);
    
            // void deleteIfExists(Path path): Path對應的檔案/目錄如果存在,執行刪除。如果不存在,正常執行結束
            Files.deleteIfExists(path3);
    
            // Path move(Path src, Path dest, CopyOption…how): 將src移動到dest位置
            // 要想執行成功,src對應的物理上的檔案需要存在,dest對應的檔案沒有要求。
            // Files.move(path1, path2, StandardCopyOption.ATOMIC_MOVE);
    
            // long size(Path path): 返回path指定檔案的大小
            long size = Files.size(path2);
            System.out.println(size);
        }
    
        @Test
        public void test2() throws IOException {
            Path path1 = Paths.get("d:\\nio", "hello.txt");
            Path path2 = Paths.get("atguigu.txt");
    
            // boolean exists(Path path, LinkOption … opts): 判斷檔案是否存在
            System.out.println(Files.exists(path2, LinkOption.NOFOLLOW_LINKS));
    
            // boolean isDirectory(Path path, LinkOption … opts): 判斷是否是目錄
            // 不要求此path對應的物理檔案存在。
            System.out.println(Files.isDirectory(path1, LinkOption.NOFOLLOW_LINKS));
    
            // boolean isRegularFile(Path path, LinkOption … opts): 判斷是否是檔案
    
            // /boolean isHidden(Path path): 判斷是否是隱藏檔案
            // 要求此path對應的物理上的檔案需要存在。才可判斷是否隱藏。否則,拋異常。
            System.out.println(Files.isHidden(path1));
    
            // /boolean isReadable(Path path): 判斷檔案是否可讀
            System.out.println(Files.isReadable(path1));
            // boolean isWritable(Path path): 判斷檔案是否可寫
            System.out.println(Files.isWritable(path1));
          // boolean notExists(Path path, LinkOption … opts): 判斷檔案是否不存在
            System.out.println(Files.notExists(path1, LinkOption.NOFOLLOW_LINKS));
        }
    
        /**
         * StandardOpenOption.READ: 表示對應的Channel是可讀的。
         * StandardOpenOption.WRITE:表示對應的Channel是可寫的。
         * StandardOpenOption.CREATE:如果要寫出的檔案不存在,則建立。如果存在,忽略
         * StandardOpenOption.CREATE_NEW:如果要寫出的檔案不存在,則建立。如果存在,拋異常
         *
         * @throws IOException
         */
        @Test
        public void test3() throws IOException {
            Path path1 = Paths.get("d:\\nio", "hello.txt");
    
            // InputStream newInputStream(Path path, OpenOption…how): 獲取InputStream物件
            InputStream inputStream = Files.newInputStream(path1, StandardOpenOption.READ);
    
            // OutputStream newOutputStream(Path path, OpenOption…how): 獲取OutputStream物件
            OutputStream outputStream = Files.newOutputStream(path1, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
    
            // SeekableByteChannel newByteChannel(Path path, OpenOption…how): 獲取與指定檔案的連線,how指定開啟方式
            SeekableByteChannel channel = Files.newByteChannel(path1, StandardOpenOption.READ,
                    StandardOpenOption.WRITE, StandardOpenOption.CREATE);
    
            // DirectoryStream<Path>  newDirectoryStream(Path path): 開啟path指定的目錄
            Path path2 = Paths.get("e:\\teach");
            DirectoryStream<Path> directoryStream = Files.newDirectoryStream(path2);
            Iterator<Path> iterator = directoryStream.iterator();
            while (iterator.hasNext()) {
                System.out.println(iterator.next());
            }
        }
    }
    

FileUtils 工具類

Maven 引入依賴:

<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.7</version>
</dependency>

複製功能:

public class FileUtilsTest {
    public static void main(String[] args) {
        File srcFile = new File("day10\\愛情與友情.jpg");
        File destFile = new File("day10\\愛情與友情2.jpg");

        try {
            FileUtils.copyFile(srcFile, destFile);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

遍歷資料夾和檔案的每一行:

public class FileUtilsMethod {
    /**
     * 常規方法:若檔案路徑內的檔案比較少,可以採用此方法
     *
     * @param filePath 檔案路徑
     */
    public static void common(String filePath) {
        File file = new File(filePath);
        if (file.exists()) {
            // 獲取子資料夾內所有檔案,放到檔案陣列裡,如果含有大量檔案,會建立一個很大的陣列,佔用空間
            File[] fileList = file.listFiles();
            for (File currentFile : fileList) { 
                // 當前檔案是普通檔案(排除資料夾),且不是隱藏檔案
                if (currentFile.isFile() && !currentFile.isHidden()) {
                    // 當前檔案的完整路徑,含檔名
                    String currentFilePath = currentFile.getPath();
                    if (currentFilePath.endsWith("xml") || currentFilePath.endsWith("XML")) {
                        // 當前檔案的檔名,含字尾
                        String fileName = currentFile.getName();
                        System.out.println("檔名:" + fileName);
                    }
                }
            }
            System.out.println("=======================================");
            // list方法返回的是檔名的String陣列
            String[] fileNameList = file.list();
            for (String fileName : fileNameList) {
                System.out.println("檔名:" + fileName);
            }
        }
    }

    /**
     * 根據檔案路徑,迭代獲取該路徑下指定檔案字尾型別的檔案:若檔案路徑內含有大量檔案,建議採用此方法
     *
     * @param filePath 檔案路徑
     */
    public static void iterateFiles(String filePath) {
        File file = FileUtils.getFile(filePath);

        if (file.isDirectory()) {
            Iterator<File> fileIterator = FileUtils.iterateFiles(file, new String[]{"xml", "XML"}, false);

            while (fileIterator.hasNext()) {
                File currentFile = fileIterator.next();

                if (currentFile.isFile() && !currentFile.isHidden()) {
                    // 絕對路徑
                    String currentFilePath = currentFile.getAbsolutePath();
                    System.out.println("絕對路徑:" + currentFilePath);
                    // 檔名,含檔案字尾
                    String fileName = currentFilePath.substring(currentFilePath.lastIndexOf("\\") + 1);
                    System.out.println("含字尾檔名:" + fileName);
                    // 檔名,不含檔案字尾
                    fileName = fileName.substring(0, fileName.lastIndexOf("."));
                    System.out.println("不含字尾檔名:" + fileName);
                }
            }
        }
    }

    /**
     * 讀取目標檔案每一行資料,返回List:若檔案內容較少,可以採用此方法
     *
     * @param filePath 檔案路徑
     * @throws IOException
     */
    public static void readLinesForList(String filePath) throws IOException {
        List<String> linesList = FileUtils.readLines(new File(filePath), "utf-8");
        for (String line : linesList) {
            System.out.println(line);
        }
    }

    /**
     * 讀取目標檔案每一行資料,返回迭代器:若檔案內容較多,建議採用此方法
     *
     * @param filePath 檔案路徑
     * @throws IOException
     */
    public static void readLinesForIterator(String filePath) throws IOException {
        LineIterator lineIterator = FileUtils.lineIterator(new File(filePath), "utf-8");
        while (lineIterator.hasNext()) {
            System.out.println(lineIterator.next());
        }
    }
}

I/O 流

I/O 的原理

I/O是 Input/Output 的縮寫, I/O 技術是非常實用的技術,用於處理裝置之間的資料傳輸。如讀/寫檔案,網路通訊等。

  • Java 程式中,對於資料的輸入/輸出操作以流 (stream)的方式進行。
  • java.io包下提供了各種 "流" 類和介面,用以獲取不同種類的資料,並透過標準的方法輸入或輸出資料。

輸入 input:讀取外部資料(磁碟、光碟等儲存裝置的資料)到程式(記憶體)中。

輸出 output:將程式(記憶體)資料輸出到磁碟、光碟等外部儲存裝置中。

I/O 的分類

image-20210330213653653

按操作資料單位不同分為:位元組流(8 bit),字元流(16 bit)。

  • 位元組流:以位元組為單位,讀寫資料的流。
  • 字元流:以字元為單位,讀寫資料的流。

按資料流的流向不同分為:輸入流,輸出流。

  • 輸入流:把資料從其他裝置上讀取到記憶體中的流。
  • 輸出流:把資料從記憶體中寫出到其他裝置上的流。

按流的角色的不同分為:節點流,處理流。

  • 節點流:直接從資料來源或目的地讀寫資料。也叫檔案流。

    image-20210330215602183

  • 處理流:不直接連線到資料來源或目的地,而是連線在已存在的流(節點流或處理流)之上,透過對資料的處理為程式提供更為強大的讀寫功能。

    image-20210330220211998

Java 的 I/O 流共涉及 40 多個類,實際上非常規則,都是從如下四個抽象基類派生的。同時,由這四個類派生出來的子類名稱都是以其父類名作為子類名字尾:

image-20210330213507408

I/O 流體系:

image-20210330214731163

I/O 的四個抽象基類

InputStream & Reader

InputStream 和 Reader 是所有輸入流的基類。

  • InputStream 的典型實現:FileInputStream。
    • FileInputStream:用於讀取非文字資料的原始位元組流。
  • Reader 的典型實現:FileReader。
    • FileReader:用於讀取文字資料的字元流。
InputStream

InputStream 的方法:

  • int read():從輸入流中讀取資料的下一個位元組。返回 0 到 255 範圍內的 int 位元組值。如果因為已經到達流末尾而沒有可用的位元組,則返回值 -1。
  • int read(byte[] b):從輸入流中將最多b.length()個位元組的資料讀入一個 byte 陣列中。以整數形式返回實際讀取的位元組數。如果因為已經到達流末尾而沒有可用的位元組,則返回值 -1。
  • int read(byte[] b, int off,int len):將輸入流中最多 len 個資料位元組讀入 byte 陣列。嘗試讀取 len 個位元組,但讀取的位元組也可能小於該值。以整數形式返回實際讀取的位元組數。如果因為已經到達流末尾而沒有可用的位元組,則返回值 -1。
  • public void close() throws IOException:關閉輸入流並釋放與該流關聯的所有系統資源。
Reader

Reader 的方法:

  • int read():讀取單個字元。作為整數讀取的字元,範圍在 0 到 65535 之間(0x00-0xffff)(2 個位元組的 Unicode 碼),如果已到達流的末尾,則返回 -1。
  • int read(char[] cbuf):將字元讀入陣列。如果已到達流的末尾,則返回 -1。否則返回本次讀取的字元數。
  • int read(char[] cbuf,int off,int len):將字元讀入陣列的某一部分。存到陣列 cbuf 中,從 off 處開始儲存,最多讀 len 個字元。如果已到達流的末尾,則返回 -1。否則返回本次讀取的字元數。
  • public void close() throws IOException:關閉此輸入流並釋放與該流關聯的所有系統資源。

OutputStream & Writer

OutputStream 和 Writer 是所有輸出流的基類。

  • OutputStream 的典型實現:FileOutStream。
    • FileOutputStream:用於寫出非文字資料的原始位元組流。
  • Writer 的典型實現:FileWriter。
    • FileWriter:用於寫出文字資料的字元流。
OutputStream

OutputStream 的方法:

  • void write(int b):將指定的位元組寫入此輸出流。write 的常規協定是:向輸出流寫入一個位元組。要寫入的位元組是引數 b 的八個低位。b 的 24 個高位將被忽略,即寫入 0 ~ 255 範圍的。
  • void write(byte[] b):將 b.length() 個位元組從指定的 byte 陣列寫入此輸出流。write(b) 的常規協定是:應該與呼叫 write(b, 0, b.length) 的效果完全相同。
  • void write(byte[] b,int off,int len):將指定 byte 陣列中從偏移量 off 開始的 len 個位元組寫入此輸出流。
  • public void flush() throws IOException:重新整理此輸出流並強制寫出所有緩衝的輸出位元組,呼叫此方法指示應將這些位元組立即寫入它們預期的目標。
  • public void close() throws IOException:關閉此輸出流並釋放與該流關聯的所有系統資源。
Writer

Writer 的方法:

  • void write(int c):寫入單個字元。要寫入的字元包含在給定整數值的 16 個低位中,16 高位被忽略。 即寫入 0 到 65535 之間的 Unicode 碼。
  • void write(char[] cbuf):寫入字元陣列。
  • void write(char[] cbuf,int off,int len):寫入字元陣列的某一部分。從 off 開始,寫入 len 個字元。
  • void write(String str):寫入字串。
  • void write(String str,int off,int len):寫入字串的某一部分。
  • public void flush() throws IOException:重新整理該流的緩衝,則立即將它們寫入預期目標。
  • public void close() throws IOException:關閉此輸出流並釋放與該流關聯的所有系統資源。

節點流(或檔案流)

讀取檔案流程:

  • 例項化 File 類的物件,指明要操作的檔案。
  • 提供具體的流物件。
  • 資料的讀入。
  • 流的關閉操作。

寫入檔案流程:

  • 例項化 File 類的物件,指明寫出到的檔案。
  • 提供具體的流物件。
  • 資料的寫入。
  • 流的關閉操作。

注意事項:

  • 定義檔案路徑時,可以用 / 或者 \。
  • 在讀取檔案時,必須保證該檔案已存在,否則報異常。
  • 對於非文字檔案(.jpg,.mp3,.mp4,.avi,.rmvb,.doc,.ppt 等),使用位元組流處理。如果使用位元組流操作文字檔案,在輸出到控制檯時,可能會出現亂碼。
  • 對於文字檔案(.txt,.java,.c,.cpp 等),使用字元流處理。

FileInputStream 和 FileOutputStream

在寫入一個檔案時,如果使用構造器FileOutputStream(file),則目錄下有同名檔案將被覆蓋。如果使用構造器FileOutputStream(file,true),則目錄下的同名檔案不會被覆蓋,而是在檔案內容末尾追加內容。

/**
 * 測試FileInputStream和FileOutputStream的使用
 *
 * 結論:
 * 1. 對於文字檔案(.txt,.java,.c,.cpp),使用字元流處理
 * 2. 對於非文字檔案(.jpg,.mp3,.mp4,.avi,.doc,.ppt,...),使用位元組流處理
 */
public class FileInputOutputStreamTest {
    /*
    使用位元組流FileInputStream處理文字檔案,可能出現亂碼。
     */
    @Test
    public void testFileInputStream() {
        FileInputStream fis = null;
        try {
            // 1. 造檔案
            File file = new File("hello.txt");

            // 2.造流
            fis = new FileInputStream(file);

            // 3.讀資料
            byte[] buffer = new byte[5];
            // 記錄每次讀取的位元組的個數
            int len;
            while ((len = fis.read(buffer)) != -1) {
                String str = new String(buffer, 0, len);
                System.out.print(str);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fis != null) {
                // 4.關閉資源
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /*
    實現對圖片的複製操作
     */
    @Test
    public void testFileInputOutputStream() {
        FileInputStream fis = null;
        FileOutputStream fos = null;
        try {
            // 1.獲取檔案
            File srcFile = new File("愛情與友情.jpg");
            File destFile = new File("愛情與友情2.jpg");

            // 2.獲取流
            fis = new FileInputStream(srcFile);
            fos = new FileOutputStream(destFile);

            // 3.複製的過程
            byte[] buffer = new byte[5];
            int len;
            while ((len = fis.read(buffer)) != -1) {
                fos.write(buffer, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 4.關閉流
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /*
    指定路徑下檔案的複製
     */
    public void copyFile(String srcPath, String destPath) {
        FileInputStream fis = null;
        FileOutputStream fos = null;
        try {
            // 1.獲取檔案
            File srcFile = new File(srcPath);
            File destFile = new File(destPath);

            // 2.獲取流
            fis = new FileInputStream(srcFile);
            fos = new FileOutputStream(destFile);

            // 3.複製的過程
            byte[] buffer = new byte[1024];
            int len;
            while ((len = fis.read(buffer)) != -1) {
                fos.write(buffer, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 4.關閉流
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Test
    public void testCopyFile() {
        long start = System.currentTimeMillis();

        String srcPath = "C:\\Users\\Administrator\\Desktop\\01-影片.avi";
        String destPath = "C:\\Users\\Administrator\\Desktop\\02-影片.avi";

        /*String srcPath = "hello.txt";
        String destPath = "hello3.txt";*/

        copyFile(srcPath, destPath);

        long end = System.currentTimeMillis();

        System.out.println("複製操作花費的時間為:" + (end - start));// 618
    }
}

FileReader 和 FileWriter

/**
 * 一、流的分類:
 * 1.運算元據單位:位元組流、字元流
 * 2.資料的流向:輸入流、輸出流
 * 3.流的角色:節點流、處理流
 *
 * 二、流的體系結構
 * 抽象基類       節點流(或檔案流)                                緩衝流(處理流的一種)
 * InputStream   FileInputStream   (read(byte[] buffer))        BufferedInputStream (read(byte[] buffer))
 * OutputStream  FileOutputStream  (write(byte[] buffer,0,len)  BufferedOutputStream (write(byte[] buffer,0,len)/flush()
 * Reader        FileReader (read(char[] cbuf))                 BufferedReader (read(char[] cbuf)/readLine())
 * Writer        FileWriter (write(char[] cbuf,0,len)           BufferedWriter (write(char[] cbuf,0,len)/flush()
 */
public class FileReaderWriterTest {
    /*
    將當前Module下的hello.txt檔案內容讀入程式中,並輸出到控制檯

    說明點:
    1. read()的理解:返回讀入的一個字元。如果達到檔案末尾,返回-1
    2. 異常的處理:為了保證流資源一定可以執行關閉操作。需要使用try-catch-finally處理
    3. 讀入的檔案一定要存在,否則就會報FileNotFoundException。
     */

    // read(): 返回讀入的一個字元。如果達到檔案末尾,返回-1
    @Test
    public void testFileReader() {
        FileReader fr = null;
        try {
            // 1.例項化File類的物件,指明要操作的檔案
            File file = new File("hello.txt");// 相較於當前Module

            // 2.提供具體的流
            fr = new FileReader(file);

            // 3.資料的讀入
            // 方式一:
            /*int data = fr.read();
            while (data != -1) {
                System.out.print((char) data);
                data = fr.read();
            }*/
            // 方式二:語法上針對於方式一的修改
            int data;
            while ((data = fr.read()) != -1) {
                System.out.print((char) data);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 4.流的關閉操作
            // 方式一:
            /*try {
                if (fr != null)
                    fr.close();
            } catch (IOException e) {
                e.printStackTrace();
            }*/
            // 方式二:
            if (fr != null) {
                try {
                    fr.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    // 對read()操作升級:使用read的過載方法read(char[] cbuf)
    @Test
    public void testFileReader1() {
        FileReader fr = null;
        try {
            // 1.File類的例項化
            File file = new File("hello.txt");

            // 2.FileReader流的例項化
            fr = new FileReader(file);

            // 3.讀入的操作
            // read(char[] cbuf):返回每次讀入cbuf陣列中的字元的個數。如果達到檔案末尾,返回-1
            char[] cbuf = new char[5];
            int len;
            while ((len = fr.read(cbuf)) != -1) {
                // 方式一:
                // 錯誤的寫法,如果以cubf的length為基準,可能會造成多輸出內容
                /*for (int i = 0; i < cbuf.length; i++) {
                    System.out.print(cbuf[i]);
                }*/
                // 正確的寫法
                /*for (int i = 0; i < len; i++) {
                    System.out.print(cbuf[i]);
                }*/
                //方式二:
                // 錯誤的寫法,對應著方式一的錯誤的寫法
                /*String str = new String(cbuf);
                System.out.print(str);*/
                // 正確的寫法,對應著方式一的正確的寫法
                String str = new String(cbuf, 0, len);
                System.out.print(str);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fr != null) {
                // 4.資源的關閉
                try {
                    fr.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /*
    從記憶體中寫出資料到硬碟的檔案裡。

    說明:
    1. 輸出操作,對應的File可以不存在的。並不會報異常
    2.
         File對應的硬碟中的檔案如果不存在,在輸出的過程中,會自動建立此檔案。
         File對應的硬碟中的檔案如果存在:
                如果流使用的構造器是:FileWriter(file,false) / FileWriter(file)--->對原有檔案的覆蓋
                如果流使用的構造器是:FileWriter(file,true)--->不會對原有檔案覆蓋,而是在原有檔案基礎上追加內容
     */
    @Test
    public void testFileWriter() {
        FileWriter fw = null;
        try {
            // 1.提供File類的物件,指明寫出到的檔案
            File file = new File("hello1.txt");

            // 2.提供FileWriter的物件,用於資料的寫出
            fw = new FileWriter(file, false);

            // 3.寫出的操作
            fw.write("I have a dream!\n");
            fw.write("you need to have a dream!");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 4.流資源的關閉
            if (fw != null) {
                try {
                    fw.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /*
    實現對已存在檔案的複製
     */
    @Test
    public void testFileReaderFileWriter() {
        FileReader fr = null;
        FileWriter fw = null;
        try {
            // 1.建立File類的物件,指明讀入和寫出的檔案
            File srcFile = new File("hello.txt");
            File destFile = new File("hello2.txt");

            // 不能使用字元流來處理圖片等位元組資料
            /*File srcFile = new File("愛情與友情.jpg");
            File destFile = new File("愛情與友情1.jpg");*/

            // 2.建立輸入流和輸出流的物件
            fr = new FileReader(srcFile);
            fw = new FileWriter(destFile);

            // 3.資料的讀入和寫出操作
            char[] cbuf = new char[5];
            // 記錄每次讀入到cbuf陣列中的字元的個數
            int len;
            while ((len = fr.read(cbuf)) != -1) {
                // 每次寫出len個字元
                fw.write(cbuf, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 4.關閉流資源
            // 方式一:
            /*try {
                if (fw != null)
                    fw.close();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    if (fr != null)
                        fr.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }*/
            // 方式二:
            try {
                if (fw != null)
                    fw.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

            try {
                if (fr != null)
                    fr.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

處理流

緩衝流

為了提高資料讀寫的速度,Java API 提供了帶緩衝功能的流類,在使用這些流類時,會建立一個內部緩衝區陣列,預設使用 8192 個位元組 (8Kb) 的緩衝區

public class BufferedInputStream extends FilterInputStream {
    private static int DEFAULT_BUFFER_SIZE = 8192;
}
public class BufferedReader extends Reader {
    private static int defaultCharBufferSize = 8192;
}
public class BufferedWriter extends Writer {
    private static int defaultCharBufferSize = 8192;
}

緩衝流要 "套接" 在相應的節點流之上,根據資料操作單位可以把緩衝流分為:

  • BufferedInputStreamBufferedOutputStream
    • public BufferedInputStream(InputStream in) :建立一個新的緩衝輸入流,注意引數型別為 InputStream
    • public BufferedOutputStream(OutputStream out): 建立一個新的緩衝輸出流,注意引數型別為 OutputStream
  • BufferedReaderBufferedWriter
    • public BufferedReader(Reader in) :建立一個新的緩衝輸入流,注意引數型別為 Reader
    • public BufferedWriter(Writer out): 建立一個新的緩衝輸出流,注意引數型別為 Writer

當讀取資料時,資料按塊讀入緩衝區,其後的讀操作則直接訪問緩衝區。

當使用 BufferedInputStream 讀取位元組檔案時,BufferedInputStream 會一次性從檔案中讀取 8192 個位元組(8Kb)存在緩衝區中,直到緩衝區裝滿了,才重新從檔案中讀取下一個 8192 個位元組陣列。

向流中寫入位元組時,不會直接寫到檔案,先寫到緩衝區中直到緩衝區寫滿,BufferedOutputStream 才會把緩衝區中的資料一次性寫到檔案裡。使用flush()可以強制將緩衝區的內容全部寫入輸出流。

  • flush()的使用:手動將 buffer 中內容寫入檔案。
  • 如果使用帶緩衝區的流物件的close(),不但會關閉流,還會在關閉流之前重新整理緩衝區,但關閉流後不能再寫出。

關閉流的順序和開啟流的順序相反。一般只需關閉最外層流即可,關閉最外層流也會相應關閉內層節點流。

流程示意圖:

image-20210401141017522

實現非文字檔案及文字檔案的複製:

/**
 * 處理流之一:緩衝流的使用
 *
 * 1.緩衝流:
 * BufferedInputStream
 * BufferedOutputStream
 * BufferedReader
 * BufferedWriter
 *
 * 2.作用:提高流的讀取、寫入的速度
 *   提高讀寫速度的原因:內部提供了一個緩衝區
 *
 * 3. 處理流,就是"套接"在已有的流的基礎上。(不一定必須是套接在節點流之上)
 */
public class BufferedStreamTest {
    /*
    使用BufferedInputStream和BufferedOutputStream實現非文字檔案的複製
     */
    @Test
    public void BufferedStreamTest() throws FileNotFoundException {
        BufferedInputStream bis = null;
        BufferedOutputStream bos = null;
        try {
            // 1.造檔案
            File srcFile = new File("愛情與友情.jpg");
            File destFile = new File("愛情與友情3.jpg");

            // 2.造流
            // 2.1 造節點流
            FileInputStream fis = new FileInputStream((srcFile));
            FileOutputStream fos = new FileOutputStream(destFile);
            // 2.2 造緩衝流
            bis = new BufferedInputStream(fis);
            bos = new BufferedOutputStream(fos);

            // 3.複製的細節:讀取、寫入
            byte[] buffer = new byte[10];
            int len;
            while ((len = bis.read(buffer)) != -1) {
                bos.write(buffer, 0, len);
                // bos.flush();// 顯示的重新整理緩衝區,一般不需要
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 4.資源關閉
            // 要求:先關閉外層的流,再關閉內層的流
            if (bos != null) {
                try {
                    bos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (bis != null) {
                try {
                    bis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            // 說明:關閉外層流的同時,內層流也會自動的進行關閉。關於內層流的關閉,我們可以省略.
            // fos.close();
            // fis.close();
        }
    }

    /*
    使用BufferedInputStream和BufferedOutputStream實現檔案複製的方法
     */
    public void copyFileWithBuffered(String srcPath, String destPath) {
        BufferedInputStream bis = null;
        BufferedOutputStream bos = null;

        try {
            // 1.造檔案
            File srcFile = new File(srcPath);
            File destFile = new File(destPath);

            // 2.造流
            // 2.1 造節點流
            FileInputStream fis = new FileInputStream((srcFile));
            FileOutputStream fos = new FileOutputStream(destFile);
            // 2.2 造緩衝流
            bis = new BufferedInputStream(fis);
            bos = new BufferedOutputStream(fos);

            // 3.複製的細節:讀取、寫入
            byte[] buffer = new byte[1024];
            int len;
            while ((len = bis.read(buffer)) != -1) {
                bos.write(buffer, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 4.資源關閉
            // 要求:先關閉外層的流,再關閉內層的流
            if (bos != null) {
                try {
                    bos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (bis != null) {
                try {
                    bis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            // 說明:關閉外層流的同時,內層流也會自動的進行關閉。關於內層流的關閉,我們可以省略.
            // fos.close();
            // fis.close();
        }
    }

    @Test
    public void testCopyFileWithBuffered() {
        long start = System.currentTimeMillis();

        String srcPath = "C:\\Users\\Administrator\\Desktop\\01-影片.avi";
        String destPath = "C:\\Users\\Administrator\\Desktop\\03-影片.avi";

        copyFileWithBuffered(srcPath, destPath);

        long end = System.currentTimeMillis();

        System.out.println("複製操作花費的時間為:" + (end - start));//618 - 176
    }

    /*
    使用BufferedReader和BufferedWriter實現文字檔案的複製
     */
    @Test
    public void testBufferedReaderBufferedWriter() {
        BufferedReader br = null;
        BufferedWriter bw = null;
        try {
            // 1.建立檔案和相應的流
            br = new BufferedReader(new FileReader(new File("dbcp.txt")));
            bw = new BufferedWriter(new FileWriter(new File("dbcp1.txt")));

            // 2.讀寫操作
            // 方式一:使用char[]陣列
            /*char[] cbuf = new char[1024];
            int len;
            while ((len = br.read(cbuf)) != -1) {// 讀到檔案末尾時返回-1
                bw.write(cbuf, 0, len);
                // bw.flush();
            }*/

            // 方式二:使用String
            String data;
            while ((data = br.readLine()) != null) {// 讀到檔案末尾時返回null
                // 方法一:
                // bw.write(data + "\n");// data中不包含換行符
                // 方法二:
                bw.write(data);// data中不包含換行符
                bw.newLine();// 提供換行的操作
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 3.關閉資源
            if (bw != null) {
                try {
                    bw.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

實現圖片加密:

public class ImageEncryption {
    /*
    圖片的加密
     */
    @Test
    public void test1() {
        FileInputStream fis = null;
        FileOutputStream fos = null;
        try {
            fis = new FileInputStream("愛情與友情.jpg");
            fos = new FileOutputStream("愛情與友情secret.jpg");

            byte[] buffer = new byte[20];
            int len;
            while ((len = fis.read(buffer)) != -1) {
                // 加密:對位元組陣列進行修改,異或操作
                // 錯誤的寫法,buffer陣列中的資料沒有改變,只是重新複製給了變數b
                /*for (byte b : buffer) {
                    b = (byte) (b ^ 5);
                }*/
                // 正確的寫法
                for (int i = 0; i < len; i++) {
                    buffer[i] = (byte) (buffer[i] ^ 5);
                }
                fos.write(buffer, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }


    /*
    圖片的解密
     */
    @Test
    public void test2() {
        FileInputStream fis = null;
        FileOutputStream fos = null;
        try {
            fis = new FileInputStream("愛情與友情secret.jpg");
            fos = new FileOutputStream("愛情與友情4.jpg");

            byte[] buffer = new byte[20];
            int len;
            while ((len = fis.read(buffer)) != -1) {
                // 解密:對位元組陣列進行修改,異或操作之後再異或,返回的是自己本身
                // 錯誤的寫法
                /*for (byte b : buffer) {
                    b = (byte) (b ^ 5);
                }*/
                // 正確的寫法
                for (int i = 0; i < len; i++) {
                    buffer[i] = (byte) (buffer[i] ^ 5);
                }
                fos.write(buffer, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

獲取文字上每個字元出現的次數:

public class WordCount {
    /*
    說明:如果使用單元測試,檔案相對路徑為當前module
          如果使用main()測試,檔案相對路徑為當前工程
     */
    @Test
    public void testWordCount() {
        FileReader fr = null;
        BufferedWriter bw = null;
        try {
            // 1.建立Map集合
            Map<Character, Integer> map = new HashMap<Character, Integer>();

            // 2.遍歷每一個字元,每一個字元出現的次數放到map中
            fr = new FileReader("dbcp.txt");
            int c;
            while ((c = fr.read()) != -1) {
                // int 還原 char
                char ch = (char) c;
                // 判斷char是否在map中第一次出現
                if (map.get(ch) == null) {
                    map.put(ch, 1);
                } else {
                    map.put(ch, map.get(ch) + 1);
                }
            }

            // 3.把map中資料存在檔案count.txt
            // 3.1 建立Writer
            bw = new BufferedWriter(new FileWriter("wordcount.txt"));

            // 3.2 遍歷map,再寫入資料
            Set<Map.Entry<Character, Integer>> entrySet = map.entrySet();
            for (Map.Entry<Character, Integer> entry : entrySet) {
                switch (entry.getKey()) {
                    case ' ':
                        bw.write("空格 = " + entry.getValue());
                        break;
                    case '\t'://\t表示tab 鍵字元
                        bw.write("tab鍵 = " + entry.getValue());
                        break;
                    case '\r'://
                        bw.write("回車 = " + entry.getValue());
                        break;
                    case '\n'://
                        bw.write("換行 = " + entry.getValue());
                        break;
                    default:
                        bw.write(entry.getKey() + " = " + entry.getValue());
                        break;
                }
                bw.newLine();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 4.關閉流
            if (fr != null) {
                try {
                    fr.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (bw != null) {
                try {
                    bw.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

轉換流

轉換流提供了在位元組流和字元流之間的轉換。

  • 位元組流中的資料都是字元時,轉成字元流操作更高效。
  • 很多時候我們使用轉換流來處理檔案亂碼問題,實現編碼和解碼的功能。

Java API 提供了兩個轉換流:

  • InputStreamReader:將 InputStream 轉換為 Reader。
    • InputStreamReader(InputStream in):建立一個使用預設字符集的字元流。
    • InputStreamReader(InputStream in, String charsetName):建立一個指定字符集的字元流。
  • OutputStreamWriter:將 Writer 轉換為 OutputStream。
    • OutputStreamWriter(OutputStream in):建立一個使用預設字符集的字元流。
    • OutputStreamWriter(OutputStream in, String charsetName):建立一個指定字符集的字元流。

InputStreamReader:

  • 實現將位元組的輸入流按指定字符集轉換為字元的輸入流。
  • 需要和 InputStream 套接。
  • 構造器
    • public InputStreamReader(InputStream in)
    • public InputSreamReader(InputStream in,String charsetName)
      • 比如:Reader isr = new InputStreamReader(System.in,"gbk");,指定字符集為 gbk。

OutputStreamWriter:

  • 實現將字元的輸出流按指定字符集轉換為位元組的輸出流。
  • 需要和 OutputStream 套接。
  • 構造器
    • public OutputStreamWriter(OutputStream out)
    • public OutputSreamWriter(OutputStream out,String charsetName)

使用 InputStreamReader 解碼時,使用的字符集取決於 OutputStreamWriter 編碼時使用的字符集。

流程示意圖:

image-20210401155051887

image-20210403214550709

轉換流的編碼應用:

  • 可以將字元按指定編碼格式儲存。
  • 可以對文字資料按指定編碼格式來解讀。
  • 指定編碼表的動作由構造器完成。

為了達到最高效率,可以考慮在 BufferedReader 內包裝 InputStreamReader:

BufferedReader in = new BufferedReader(new InputStreamReader(System.in));

示例:

/**
 * 處理流之二:轉換流的使用
 * 1.轉換流:屬於字元流
 *   InputStreamReader:將一個位元組的輸入流轉換為字元的輸入流
 *   OutputStreamWriter:將一個字元的輸出流轉換為位元組的輸出流
 *
 * 2.作用:提供位元組流與字元流之間的轉換
 *
 * 3. 解碼:位元組、位元組陣列  --->字元陣列、字串
 *    編碼:字元陣列、字串 ---> 位元組、位元組陣列
 *
 *
 * 4.字符集
 * ASCII:美國標準資訊交換碼。
 *   用一個位元組的7位可以表示。
 * ISO8859-1:拉丁碼錶。歐洲碼錶
 *   用一個位元組的8位表示。
 * GB2312:中國的中文編碼表。最多兩個位元組編碼所有字元
 * GBK:中國的中文編碼表升級,融合了更多的中文文字元號。最多兩個位元組編碼
 * Unicode:國際標準碼,融合了目前人類使用的所有字元。為每個字元分配唯一的字元碼。所有的文字都用兩個位元組來表示。
 * UTF-8:變長的編碼方式,可用1-4個位元組來表示一個字元。
 */
public class InputStreamReaderTest {
    /*
    此時處理異常的話,仍然應該使用try-catch-finally
    InputStreamReader的使用,實現位元組的輸入流到字元的輸入流的轉換
     */
    @Test
    public void test1() {
        InputStreamReader isr = null;
        try {
            FileInputStream fis = new FileInputStream("dbcp.txt");
            // InputStreamReader isr = new InputStreamReader(fis);// 使用系統預設的字符集,如果在IDEA中,就是看IDEA設定的預設字符集
            // 引數2指明瞭字符集,具體使用哪個字符集,取決於檔案dbcp.txt儲存時使用的字符集
            isr = new InputStreamReader(fis, StandardCharsets.UTF_8);// 指定字符集

            char[] cbuf = new char[20];
            int len;
            while ((len = isr.read(cbuf)) != -1) {
                String str = new String(cbuf, 0, len);
                System.out.print(str);
            }
        } catch (IOException exception) {
            exception.printStackTrace();
        } finally {
            if (isr != null) {
                try {
                    isr.close();
                } catch (IOException exception) {
                    exception.printStackTrace();
                }
            }
        }
    }

    /*
    此時處理異常的話,仍然應該使用try-catch-finally
    綜合使用InputStreamReader和OutputStreamWriter
     */
    @Test
    public void test2() {
        InputStreamReader isr = null;
        OutputStreamWriter osw = null;
        try {
            // 1.造檔案、造流
            File file1 = new File("dbcp.txt");
            File file2 = new File("dbcp_gbk.txt");

            FileInputStream fis = new FileInputStream(file1);
            FileOutputStream fos = new FileOutputStream(file2);

            isr = new InputStreamReader(fis, StandardCharsets.UTF_8);
            osw = new OutputStreamWriter(fos, "gbk");

            // 2.讀寫過程
            char[] cbuf = new char[20];
            int len;
            while ((len = isr.read(cbuf)) != -1) {
                osw.write(cbuf, 0, len);
            }
        } catch (IOException exception) {
            exception.printStackTrace();
        } finally {
            // 3.關閉資源
            if (isr != null) {
                try {
                    isr.close();
                } catch (IOException exception) {
                    exception.printStackTrace();
                }
            }
            if (osw != null) {
                try {
                    osw.close();
                } catch (IOException exception) {
                    exception.printStackTrace();
                }
            }
        }
    }
}

標準輸入、輸出流

System.inSystem.out分別代表了系統標準的輸入和輸出裝置。

  • 預設輸入裝置是:鍵盤,輸出裝置是:顯示器。

  • System.in的型別是 InputStream。

  • System.out的型別是 PrintStream,其是 OutputStream 的子類 FilterOutputStream 的子類。

重定向:透過 System 類的setIn()setOut()對預設裝置進行改變。

  • public static void setIn(InputStream in)
  • public static void setOut(PrintStream out)

示例:

public class OtherStreamTest {
    /*
     1.標準的輸入、輸出流
     1.1
     System.in: 標準的輸入流,預設從鍵盤輸入
     System.out: 標準的輸出流,預設從控制檯輸出
     1.2
     System類的setIn(InputStream is) / setOut(PrintStream ps)方式重新指定輸入和輸出的流。

     1.3練習:
     從鍵盤輸入字串,要求將讀取到的整行字串轉成大寫輸出。然後繼續進行輸入操作,
     直至當輸入“e”或者“exit”時,退出程式。

     方法一:使用Scanner實現,呼叫next()返回一個字串
     方法二:使用System.in實現。System.in  --->  轉換流 ---> BufferedReader的readLine()
      */
    // IDEA的單元測試不支援從鍵盤輸入,更改為main()
    public static void main(String[] args) {
        BufferedReader br = null;
        try {
            InputStreamReader isr = new InputStreamReader(System.in);
            br = new BufferedReader(isr);

            while (true) {
                System.out.println("請輸入字串:");
                String data = br.readLine();
                if ("e".equalsIgnoreCase(data) || "exit".equalsIgnoreCase(data)) {
                    System.out.println("程式結束");
                    break;
                }

                String upperCase = data.toUpperCase();
                System.out.println(upperCase);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

模擬 Scanner:

/**
 * MyInput.java: Contain the methods for reading int, double, float, boolean, short, byte and
 * string values from the keyboard
 */
public class MyInput {
    // Read a string from the keyboard
    public static String readString() {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        // Declare and initialize the string
        String string = "";

        // Get the string from the keyboard
        try {
            string = br.readLine();
        } catch (IOException ex) {
            System.out.println(ex);
        }

        // Return the string obtained from the keyboard
        return string;
    }

    // Read an int value from the keyboard
    public static int readInt() {
        return Integer.parseInt(readString());
    }

    // Read a double value from the keyboard
    public static double readDouble() {
        return Double.parseDouble(readString());
    }

    // Read a byte value from the keyboard
    public static double readByte() {
        return Byte.parseByte(readString());
    }

    // Read a short value from the keyboard
    public static double readShort() {
        return Short.parseShort(readString());
    }

    // Read a long value from the keyboard
    public static double readLong() {
        return Long.parseLong(readString());
    }

    // Read a float value from the keyboard
    public static double readFloat() {
        return Float.parseFloat(readString());
    }

    public static void main(String[] args) {
        int i = readInt();
        System.out.println("輸出的數為:" + i);
    }
}

列印流

實現將基本資料型別的資料格式轉化為字串輸出。

列印流:PrintStreamPrintWriter

  • 提供了一系列過載的print()println(),用於多種資料型別的輸出。
  • PrintStream 和 PrintWriter 的輸出不會丟擲 IOException 異常。
  • PrintStream 和 PrintWriter 有自動 flush 功能。
  • PrintStream 列印的所有字元都使用平臺的預設字元編碼轉換為位元組。在需要寫入字元而不是寫入位元組的情況下,應該使用 PrintWriter 類。
  • System.out 返回的是 PrintStream 的例項。

把標準輸出流(控制檯輸出)改成檔案:

public class OtherStreamTest {
    /*
    2. 列印流:PrintStream 和PrintWriter
    2.1 提供了一系列過載的print()和println()
    2.2 練習:將ASCII字元輸出到自定義的外部檔案
     */
    @Test
    public void test2() {
        PrintStream ps = null;
        try {
            FileOutputStream fos = new FileOutputStream(new File("D:\\text.txt"));
            // 建立列印輸出流,設定為自動重新整理模式(寫入換行符或位元組 '\n' 時都會重新整理輸出緩衝區)
            ps = new PrintStream(fos, true);
            // 把標準輸出流(控制檯輸出)改成輸出到本地檔案
            if (ps != null) {
                // 如果不設定,下面的迴圈輸出是在控制檯
                // 設定之後,控制檯不再輸出,而是輸出到D:\text.txt
                System.setOut(ps);
            }

            // 開始輸出ASCII字元
            for (int i = 0; i <= 255; i++) {
                System.out.print((char) i);
                if (i % 50 == 0) {// 每50個資料一行
                    System.out.println();// 換行
                }
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } finally {
            if (ps != null) {
                ps.close();
            }
        }
    }
}

資料流

為了方便地操作 Java 語言的基本資料型別和 String 型別的資料,可以使用資料流。(不能操作記憶體中的物件)

資料流有兩個類:分別用於讀取和寫出基本資料型別、String類的資料。

  • DataInputStreamDataOutputStream
  • 分別套接在 InputStream 和 和 OutputStream 子類的流上。
  • 用 DataOutputStream 輸出的檔案需要用 DataInputStream 來讀取。
  • DataInputStream 讀取不同型別的資料的順序,要與當初 DataOutputStream 寫入檔案時,儲存的資料的順序一致。

DataInputStream 中的方法:

  • boolean readBoolean()byte readByte()
  • char readChar()float readFloat()
  • double readDouble()short readShort()
  • long readLong()int readInt()
  • String readUTF()void readFully(byte[] b)

DataOutputStream 中的方法:

  • 將上述的方法的 read 改為相應的 write 即可。

將記憶體中的字串、基本資料型別的變數寫出到檔案中,再讀取到記憶體中:

public class OtherStreamTest {
    /*
    3. 資料流
    3.1 DataInputStream 和 DataOutputStream
    3.2 作用:用於讀取或寫出基本資料型別的變數或字串
    練習:將記憶體中的字串、基本資料型別的變數寫出到檔案中。
    注意:處理異常的話,仍然應該使用try-catch-finally。
     */
    @Test
    public void test3() {
        DataOutputStream dos = null;
        try {
            // 1.造流
            dos = new DataOutputStream(new FileOutputStream("data.txt"));
            
            // 2.寫入操作
            dos.writeUTF("劉建辰");// 寫入String
            dos.flush();// 重新整理操作,將記憶體中的資料立即寫入檔案,也可以在關閉流時自動重新整理
            dos.writeInt(23);// 寫入int
            dos.flush();
            dos.writeBoolean(true);// 寫入boolean
            dos.flush();
        } catch (IOException exception) {
            exception.printStackTrace();
        } finally {
            // 3.關閉流
            if (dos != null) {
                try {
                    dos.close();
                } catch (IOException exception) {
                    exception.printStackTrace();
                }
            }
        }
    }

    /*
    將檔案中儲存的基本資料型別變數和字串讀取到記憶體中,儲存在變數中。
    注意點:讀取不同型別的資料的順序要與當初寫入檔案時,儲存的資料的順序一致!
     */
    @Test
    public void test4() {
        DataInputStream dis = null;
        try {
            // 1.造流
            dis = new DataInputStream(new FileInputStream("data.txt"));
            
            // 2.讀取操作
            String name = dis.readUTF();// 讀取String
            int age = dis.readInt();// 讀取int
            boolean isMale = dis.readBoolean();// 讀取boolean
            System.out.println("name = " + name);
            System.out.println("age = " + age);
            System.out.println("isMale = " + isMale);
        } catch (IOException exception) {
            exception.printStackTrace();
        } finally {
            // 3.關閉流
            if (dis!=null) {
                try {
                    dis.close();
                } catch (IOException exception) {
                    exception.printStackTrace();
                }
            }
        }
    }
}

物件流

ObjectInputStreamOjbectOutputSteam:用於儲存和讀取基本資料型別資料或物件的處理流。它的強大之處就是可以把 Java 中的物件寫入到資料來源中,也能把物件從資料來源中還原回來。

  • 一般情況下,會把物件轉換為 Json 字串,然後進行序列化和反序列化操作,而不是直接操作物件。

序列化:用 ObjectOutputStream 類儲存基本型別資料或物件的機制。

反序列化:用 ObjectInputStream 類讀取基本型別資料或物件的機制。

ObjectOutputStream 和 ObjectInputStream 不能序列化 static 和 transient 修飾的成員變數

  • 在序列化一個類的物件時,如果類中含有 static 和 transient 修飾的成員變數,則在反序列化時,這些成員變數的值會變成預設值,而不是序列化時這個物件賦予的值。比如,Person 類含有一個 static 修飾的 String name 屬性,序列化時,物件把 name 賦值為張三,在反序列化時,name 會變為 null。

    image-20210402151454756

物件序列化機制允許把記憶體中的 Java 物件轉換成平臺無關的二進位制流(序列化操作),從而允許把這種二進位制流持久地儲存在磁碟上,或透過網路將這種二進位制流傳輸到另一個網路節點。當其它程式獲取了這種二進位制流,就可以恢復成原來的 Java 物件(反序列化操作)。

  • 序列化的好處在於可將任何實現了 Serializable 介面的物件轉化為位元組資料,使其在儲存和傳輸時可被還原。

  • 序列化是 RMI(Remote Method Invoke – 遠端方法呼叫)過程的引數和返回值都必須實現的機制,而 RMI 是 JavaEE 的基礎,因此序列化機制是 JavaEE 平臺的基礎。

  • 如果需要讓某個物件支援序列化機制,則必須讓物件所屬的類及其屬性是可序列化的,為了讓某個類是可序列化的,該類必須實現如下兩個介面之一。否則,會丟擲 NotSerializableException 異常。

    • Serializable
    • Externalizable
  • 凡是實現 Serializable 介面的類都有一個表示序列化版本識別符號的靜態變數:

    • private static final long serialVersionUID;
    • serialVersionUID 用來表明類的不同版本間的相容性。 簡言之,其目的是以序列化物件進行版本控制,有關各版本反序列化時是否相容。
    • 如果類沒有顯示定義這個靜態常量,它的值是 Java 執行時環境根據類的內部細節自動生成的。此時,若類的例項變數做了修改,serialVersionUID 可能發生變化,則再對修改之前被序列化的類進行反序列化操作時,會操作失敗。因此,建議顯式宣告 serialVersionUID。
      • 在某些場合,希望類的不同版本對序列化相容,因此需要確保類的不同版本具有相同的 serialVersionUID;在某些場合,不希望類的不同版本對序列化相容,因此需要確保類的不同版本具有不同的 serialVersionUID。
      • 當序列化了一個類例項後,後續可能更改一個欄位或新增一個欄位。如果不設定 serialVersionUID,所做的任何更改都將導致無法反序化舊有例項,並在反序列化時丟擲一個異常;如果你新增了 serialVersionUID,在反序列舊有例項時,新新增或更改的欄位值將設為初始化值(物件為 null,基本型別為相應的初始預設值),欄位被刪除將不設定。

簡單來說,Java 的序列化機制是透過在執行時判斷類的 serialVersionUID 來驗證版本一致性的。在進行反序列化時,JVM 會把傳來的位元組流中的 serialVersionUID 與本地相應實體類的 serialVersionUID 進行比較,如果相同就認為是一致的,可以進行反序列化,否則就會出現序列化版本不一致的異常,即 InvalidCastException。

若某個類實現了 Serializable 介面,該類的物件就是可序列化的:

  • 建立一個 ObjectOutputStream。
    • public ObjectOutputStream(OutputStream out): 建立一個指定 OutputStream 的 ObjectOutputStream。
  • 呼叫 ObjectOutputStream 物件的writeObject(Object obj)輸出可序列化物件。
  • 注意寫出一次,操作flush()一次。

反序列化:

  • 建立一個 ObjectInputStream。
    • public ObjectInputStream(InputStream in): 建立一個指定 InputStream 的 ObjectInputStream。
  • 呼叫readObject()讀取流中的物件。

強調:如果某個類的屬性不是基本資料型別或 String 型別,而是另一個引用型別,那麼這個引用型別必須是可序列化的,否則擁有該型別的 Field 的類也不能序列化。

  • 預設情況下,基本資料型別是可序列化的。String 實現了 Serializable 介面。

流程示意圖:

image-20210403215448069

示例:

/**
 * Person需要滿足如下的要求,方可序列化
 * 1.需要實現介面:Serializable
 * 2.當前類提供一個全域性常量:serialVersionUID
 * 3.除了當前Person類需要實現Serializable介面之外,還必須保證其內部所有屬性
 *   也必須是可序列化的。(預設情況下,基本資料型別可序列化)
 *
 * 補充:ObjectOutputStream和ObjectInputStream不能序列化static和transient修飾的成員變數
 */
public class Person implements Serializable {

    public static final long serialVersionUID = 475463534532L;

    private String name;
    private int age;
    private int id;
    private Account acct;

    public Person() {

    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public Person(String name, int age, int id) {
        this.name = name;
        this.age = age;
        this.id = id;
    }

    public Person(String name, int age, int id, Account acct) {
        this.name = name;
        this.age = age;
        this.id = id;
        this.acct = acct;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", id=" + id +
                ", acct=" + acct +
                '}';
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

class Account implements Serializable {
    public static final long serialVersionUID = 4754534532L;

    private double balance;

    public Account(double balance) {
        this.balance = balance;
    }

    @Override
    public String toString() {
        return "Account{" +
                "balance=" + balance +
                '}';
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }
}
/**
 * 物件流的使用
 * 1.ObjectInputStream 和 ObjectOutputStream
 * 2.作用:用於儲存和讀取基本資料型別資料或物件的處理流。它的強大之處就是可以把Java中的物件寫入到資料來源中,也能把物件從資料來源中還原回來。
 *
 * 3.要想一個java物件是可序列化的,需要滿足相應的要求。見Person.java
 *
 * 4.序列化機制:
 * 物件序列化機制允許把記憶體中的Java物件轉換成平臺無關的二進位制流,從而允許把這種
 * 二進位制流持久地儲存在磁碟上,或透過網路將這種二進位制流傳輸到另一個網路節點。
 * 當其它程式獲取了這種二進位制流,就可以恢復成原來的Java物件。
 */
public class ObjectInputOutputStreamTest {
    /*
    序列化過程:將記憶體中的Java物件儲存到磁碟中或透過網路傳輸出去
    使用ObjectOutputStream實現
     */
    @Test
    public void testObjectOutputStream() {
        ObjectOutputStream oos = null;

        try {
            // 1.造流
            oos = new ObjectOutputStream(new FileOutputStream("object.dat"));

            // 2.序列化:寫操作
            oos.writeObject(new String("我愛北京天安門"));
            oos.flush();// 重新整理操作

            oos.writeObject(new Person("王銘", 23));
            oos.flush();

            oos.writeObject(new Person("張學", 23, 1001, new Account(5000)));
            oos.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (oos != null) {
                // 3.關閉流
                try {
                    oos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /*
    反序列化:將磁碟檔案中的物件還原為記憶體中的一個Java物件
    使用ObjectInputStream來實現
     */
    @Test
    public void testObjectInputStream() {
        ObjectInputStream ois = null;
        try {
            // 1.造流
            ois = new ObjectInputStream(new FileInputStream("object.dat"));

            // 2.反序列化:讀操作
            // 檔案中儲存的是不同型別的物件,反序列化時,需要與序列化時的順序一致
            Object obj = ois.readObject();
            String str = (String) obj;
            System.out.println(str);

            Person p = (Person) ois.readObject();
            System.out.println(p);

            Person p1 = (Person) ois.readObject();
            System.out.println(p1);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            if (ois != null) {
                try {
                    ois.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

面試題:談談你對java.io.Serializable介面的理解,我們知道它用於序列化,是空方法介面,還有其它認識嗎?

  • 實現了 Serializable 介面的物件,可將它們轉換成一系列位元組,並可在以後完全恢復回原來的樣子。 這一過程亦可透過網路進行。這意味著序列化機制能自動補償作業系統間的差異。換句話說,可以先在 Windows 機器上建立一個物件,對其序列化,然後透過網路發給一臺 Unix 機器,然後在那裡準確無誤地重新 "裝配"。不必關心資料在不同機器上如何表示,也不必關心位元組的順序或者其他任何細節。
  • 由於大部分作為引數的類如 String 、Integer 等都實現了java.io.Serializable介面,也可以利用多型的性質,作為引數使介面更靈活。

隨機存取檔案流

RandomAccessFile宣告在java.io包下,但直接繼承於java.lang.Object類。並且它實現了 DataInput、DataOutput 這兩個介面,也就意味著這個類既可以讀也可以寫。

RandomAccessFile 類支援隨機訪問的方式,程式可以直接跳到檔案的任意地方來讀、寫檔案。

  • 支援只訪問檔案的部分內容。
  • 可以向已存在的檔案後追加內容。

RandomAccessFile 物件包含一個記錄指標,用以標示當前讀寫處的位置。RandomAccessFile 類物件可以自由移動記錄指標:

  • long getFilePointer():獲取檔案記錄指標的當前位置。
  • void seek(long pos):將檔案記錄指標定位到 pos 位置。

構造器:

  • public RandomAccessFile(File file, String mode)
  • public RandomAccessFile(String name, String mode)

建立 RandomAccessFile 類例項需要指定一個 mode 引數,該引數指定 RandomAccessFile 的訪問模式:

  • r:以只讀方式開啟。

  • rw:開啟以便讀取和寫入。

  • rwd:開啟以便讀取和寫入;同步檔案內容的更新。

  • rws:開啟以便讀取和寫入;同步檔案內容和後設資料的更新。

  • JDK 1.6 上面寫的每次 write 資料時,rw 模式,資料不會立即寫到硬碟中,而 rwd 模式,資料會被立即寫入硬碟。如果寫資料過程發生異常,rwd 模式中已被 write 的資料會被儲存到硬碟,而 rw 模式的資料會全部丟失。

  • 如果模式為只讀 r,則不會建立檔案,而是會去讀取一個已經存在的檔案,如果讀取的檔案不存在則會出現異常。 如果模式為讀寫 rw,如果檔案不存在則會去建立檔案,如果存在則不會建立。

RandomAccessFile 的應用:我們可以用 RandomAccessFile 這個類,來實現一個多執行緒斷點下載的功能,用過下載工具的朋友們都知道,下載前都會建立兩個臨時檔案,一個是與被下載檔案大小相同的空檔案,另一個是記錄檔案指標的位置檔案,每次暫停的時候,都會儲存上一次的指標,然後斷點下載的時候,會繼續從上一次的地方下載,從而實現斷點下載或上傳的功能,有興趣的朋友們可以自己實現下。

示例:

/**
 * RandomAccessFile的使用
 * 1.RandomAccessFile直接繼承於java.lang.Object類,實現了DataInput和DataOutput介面
 * 2.RandomAccessFile既可以作為一個輸入流,又可以作為一個輸出流
 *
 * 3.如果RandomAccessFile作為輸出流時,寫出到的檔案如果不存在,則在執行過程中自動建立。
 *   如果寫出到的檔案存在,則會對原有檔案內容進行覆蓋。(預設情況下,從頭覆蓋)
 *
 * 4. 可以透過相關的操作,實現RandomAccessFile“插入”資料的效果
 */
public class RandomAccessFileTest {
    /*
    使用RandomAccessFile實現檔案的複製
     */
    @Test
    public void test1() {
        RandomAccessFile raf1 = null;
        RandomAccessFile raf2 = null;
        try {
            // 1.造流
            raf1 = new RandomAccessFile(new File("愛情與友情.jpg"), "r");
            raf2 = new RandomAccessFile(new File("愛情與友情1.jpg"), "rw");

            // 2.讀寫操作
            byte[] buffer = new byte[1024];
            int len;
            while ((len = raf1.read(buffer)) != -1) {
                raf2.write(buffer, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 3.關閉流
            if (raf1 != null) {
                try {
                    raf1.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (raf2 != null) {
                try {
                    raf2.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /*
    使用RandomAccessFile實現檔案內容的覆蓋和追加
     */
    @Test
    public void test2() {
        RandomAccessFile raf1 = null;
        try {
            // hello.txt內容為:abcdefghijklmn
            File file = new File("hello.txt");
            raf1 = new RandomAccessFile(file, "rw");

            raf1.write("123".getBytes());// 從頭開始覆蓋:123defghijklmn
            raf1.seek(5);// 將指標調到角標為5的位置,角標從0開始
            raf1.write("456".getBytes());// 從角標為5處開始覆蓋:123de456ijklmn
            raf1.seek(file.length());// 將指標調到檔案末尾
            raf1.write("789".getBytes());// 在檔案末尾追加:123de456ijklmn789
        } catch (IOException exception) {
            exception.printStackTrace();
        } finally {
            if (raf1 != null) {
                try {
                    raf1.close();
                } catch (IOException exception) {
                    exception.printStackTrace();
                }
            }
        }
    }

    /*
    使用RandomAccessFile實現資料的插入效果
     */
    @Test
    public void test3() {
        RandomAccessFile raf1 = null;
        try {
            // hello.txt內容為:abcdefghijklmn
            File file = new File("hello.txt");
            raf1 = new RandomAccessFile(file, "rw");

            // 將指標調到角標為3的位置,從此處開始讀入檔案的資料
            raf1.seek(3);

            // 方法一:儲存指標3後面的所有資料到StringBuilder中
            /*StringBuilder builder = new StringBuilder((int) file.length());
            byte[] buffer = new byte[20];
            int len;
            while ((len = raf1.read(buffer)) != -1) {
                builder.append(new String(buffer, 0, len));
            }*/

            // 方法二:儲存指標3後面的所有資料到ByteArrayOutputStream中
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            byte[] buffer = new byte[10];
            int len;
            while ((len = raf1.read(buffer)) != -1) {
                baos.write(buffer, 0, len);
            }

            // 經過上面的讀操作後,指標位置移到了檔案的末尾處
            // 調回指標,寫入"123",實際上是覆蓋原檔案內容
            raf1.seek(3);
            raf1.write("123".getBytes());// abc123ghijklmn

            // 經過上面的寫入操作,指標位置已到了123後,緊接著:
            // 方法一:將StringBuilder中的資料寫入到檔案中,實際上是覆蓋123後的內容
            // raf1.write(builder.toString().getBytes());// abc123defghijklmn
            // 方法二:將ByteArrayOutputStream中的資料寫入到檔案中
            raf1.write(baos.toString().getBytes());// abc123defghijklmn
        } catch (IOException exception) {
            exception.printStackTrace();
        } finally {
            if (raf1 != null) {
                try {
                    raf1.close();
                } catch (IOException exception) {
                    exception.printStackTrace();
                }
            }
        }
    }
}

I/O 流的關閉

一個流繫結了一個檔案控制代碼(或網路埠),如果流不關閉,該檔案(或埠)將始終處於被鎖定(不能讀取、寫入、刪除和重新命名)狀態,導致佔用大量系統資源卻沒有釋放,因此,必須要關閉流。

同時,在程式中開啟的檔案 I/O 資源不屬於記憶體裡的資源,垃圾回收機制無法回收該資源,所以,需要顯式關閉檔案 I/O 資源。

try-catch-finally

語法格式:

try {
     // 宣告並初始化資源
     ResourceType resource1 = createResource1();
     ResourceType resource2 = createResource2()
 } catch (ExceptionType1 e1) {
     // 處理異常型別 1
 } catch (ExceptionType2 e2) {
     // 處理異常型別 2
 } finally {
    // 在 finally 中關閉流
    if (resource1!= null) {
         try {
             resource1.close();
         } catch (IOException e) {
             // 處理關閉檔案流時的異常
         }
     }
    
    if (resource2!= null) {
         try {
             resource2.close();
         } catch (IOException e) {
             // 處理關閉檔案流時的異常
         }
     }
}
  • 流一定要在 finally 語句中關閉,防止出現異常,導致無法關閉。
  • 流關閉時,需要注意關閉的順序,先宣告的流後關閉

try-catch-resources

try-catch-resources:是 Java 7 中引入的一種異常處理機制,用於自動關閉實現了java.lang.AutoCloseable介面(在 Java 7 中)或java.io.Closeable介面(在 Java 7 之前就存在,AutoCloseable 是 Closeable 的子介面)的資源。這些資源通常包括檔案流(如 FileInputStream、FileOutputStream)、資料庫連線(如 Connection)、網路連線等。

傳統的資源管理在異常處理時可能會比較複雜,需要在 finally 塊中手動關閉資源,以確保資源被正確釋放,防止資源洩漏。而 try-catch-resources 簡化了這個過程,它能夠自動關閉資源,即使在 try 塊中發生了異常。

語法格式:

 try (
     // 在這裡宣告並初始化需要自動關閉的資源,可以宣告多個資源,以 ; 隔開
     ResourceType resource1 = createResource1();
     ResourceType resource2 = createResource2()) {
     // 使用資源進行操作,可能會丟擲異常的程式碼塊
 } catch (ExceptionType1 e1) {
     // 處理異常型別 1
 } catch (ExceptionType2 e2) {
     // 處理異常型別 2
 }

原文連結

https://github.com/ACatSmiling/zero-to-zero/blob/main/JavaLanguage/java-advanced.md

相關文章