IO流:基礎、緩衝流、轉換流與Unicode編碼
尚硅谷JavaSE筆記-26

File類

  • 位於 java.io之下
  • File類的一個物件,代表一個實際的文件檔案或一個資料夾
  • File類只涉及檔案或資料夾的創建、刪除、改名…等方法,只能碰到外殼;讀取或寫入需要IO流才能做到
  • File類的物件通常做為參數傳到流的構造器中,作為讀取或寫入的終點

實例化

構造器的幾種參數

  • File(String filepath):一般路徑
  • File(String parentPath, String childPath):分成父目錄與子目錄,再拼接起來
  • File(FileA parentFile, String childPath):相當於以 FileA為父目錄下的子目錄

路徑

  • 不指明就是相對路徑,在IDEA中若使用 JUnit單元測試位置是 Module下(與 src資料夾同層);若 main()方法則是在 Project

  • 指明則是絕對路徑,例如 "G:\\Java\\code\\guigu\\day26IO\\src\\hi.txt"

  • 關於正反、雙斜槓等疑惑可以參考這篇:

    https://yoziming.github.io/post/211212-slash-file-name/

  • File類中有一個常量 File.separator可以根據系統自動變換分隔符

常用方法

預設以一個File類的實例物件.調用以下方法

  • String getAbsolutionPath():獲取絕對路徑
  • String getPath():獲取相對路徑
  • String getName():獲取檔案名稱
  • String getParent():獲取上層文件夾目錄路徑(需本身是絕對路徑),若無則返回null
  • Long Length():獲取檔案大小,單位位元組
  • Long LastModified():獲取最後修改時間戳
  • file1.boolean renameTo(File file2):將file1搬到file2的路徑並改名成file2,必須有file1無file2(他是一個移動+改名,無法覆蓋)
  • String[] List():獲取指定目錄下的所有資料夾名與檔案名,類似dir與ls,只適用於資料夾目錄
  • File[] ListFiles():獲取指定目錄下的所有資料夾與檔案的File數組,只適用於資料夾目錄
  • boolean isDirctory():判斷是否為文件夾
  • boolean isFile():判斷是否為檔案
  • boolean exists():判斷是否存在
  • boolean canRead():判斷是否能讀
  • boolean canWrite():判斷是否能寫
  • boolean isHidden():判斷是否隱藏

創建/刪除檔案

不是實例化,而是真正在硬碟中創建檔案

  • boolean createNewFile():創建檔案,若檔案已存在則不創建並返回 false
  • boolean mkdir():創建資料夾,若資料夾已存在或不存在上層目錄,則不創建並返回 false
  • boolean mkdirs():創建資料夾,若不存在上層目錄,一併創建
  • boolean delete():執行就馬上刪了,可不會進資源回收桶,慎用,如果是文件夾必須是空的才能刪

IO流

IO即in和out,輸入和輸出

流(Stream)是一種抽象的概念,指資料的傳遞,流有以下特性:

  • 先進先出:最先寫入輸出流的資料最先被輸入流讀取到
  • 順序存取:可以一個接一個地往流中寫入一串位元組,讀出時也將按寫入順序讀取一串位元組,不能隨機訪問中間的資料(RandomAccessFile除外)
  • 只讀或只寫:每個流只能是輸入流或輸出流的一種,不能同時具備兩個功能,輸入流只能進行讀操作,對輸出流只能進行寫操作。在一個資料傳輸通道中,如果既要寫入資料,又要讀取資料,則要分別提供兩個流

分類

台灣用法,字節=位元組;字符=字元,個人認為這邊中國翻譯完勝。

中國用位元-字節-字符,元是"基礎"能組成"字",形象的"符"比"節點"大,邏輯清晰、字數整齊且不易混淆。

台灣用位元-位元組-字元,長短不一就算了,結果又用回"元",元應該是用在基本的、最小的、固定的單位,這邊記java字元是16-bit;而C語言的字元又是8-bit,媽的char就不該翻成字元

  1. 按資料流的方向:輸入流、輸出流

  2. 按處理資料單位:字節流(8bit = 1byte)、字符流(16bit = 1char)

  3. 按功能:節點流、處理流(針對流的流)

    字節(位元組,8-bit) 字符(字元,16-bit)
    輸入 InputStream Reader
    輸出 OutputStream Writer
    適用於 非文字檔案 純文字檔案(底層自帶緩衝)

字符流

專門用於純文字

範例-字符流讀取文字檔案
@Test
public void test1() {
    // 造一個讀取的流,聲明在這是為了finally要關,所以先賦null(沒有實際造)
    FileReader reader = null;
    try {
        // 指定要作為流的起點的檔案
        File file1 = new File("hi.txt");
        // 將檔案作為形參,傳入構造器,實際造流
        reader = new FileReader(file1);
        // .read()方法按順序讀出一個一個int形式的char,直到沒東西了返回-1
        int data;
        while ((data = reader.read()) != -1) {
            System.out.print(((char) data));
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        // 永遠記得開了流就要關
        try {
            // 關閉前先判斷是否為"一開始就沒進去實際造"的null
            if (reader != null) reader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
改良-使用帶參數的 read(cbuf)方法
try {
    // 指定要作為流的起點的檔案
    File file1 = new File("hi.txt");
    // 將檔案作為形參,傳入構造器,實際造
    reader = new FileReader(file1);
    // .read(cbuf)方法按順序讀出一個一個char[]形式的數組,
    // 返回讀出的char數量,直到沒東西了返回-1
    char[] cbuf = new char[5]; // 假設一次拿5個char出來
    int len; // 用來接收拿出來的數量
    while ((len = reader.read(cbuf)) != -1) {
        for (int i = 0; i < len; i++) {
            // 注意這個for循環不能寫i < cbuf.length,否則拿到不滿5個時會出錯
            // "錯誤"不是空指針,而是會重複輸出上一輪的後幾個字
            System.out.print(cbuf[i]);
        }
        // 再改進,取代上面的for,直接用String裝
        String str = new String(cbuf, 0, len);
        System.out.print(str);
    }
}
範例-字符流輸出文字到檔案
FileWriter writer = null;
try {
    File file2 = new File("file2.txt");
    writer = new FileWriter(file2, true); // 參數append預設是false
    writer.write("something...");
} catch (IOException e) {
    e.printStackTrace();
} finally {
    try {
        assert writer != null;
        writer.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}
  • new FileWriter(file, true); 此構造器的參數 append可以選擇輸出方式,預設或不寫為 false,意思就是每次調用時覆蓋一個新的檔案;true時則表示在原有檔案追加

字節流

可以用於所有地方,但讀取文字時要注意

範例-字節流讀取文字
@Test
public void test3() throws Exception { // 偷懶先拋個異常
    File f3 = new File("hi.txt");
    FileInputStream fileInputStream = new FileInputStream(f3);
    byte[] buf = new byte[5];
    int n = 0;
    while ((n = fileInputStream.read(buf)) != -1) {
        String str = new String(buf, 0, n);
        System.out.print(str);
    }
    fileInputStream.close();
}
  • 在UTF-8編碼下用字節流讀取中文會出現切字問題,原因後述
範例-字節流複製圖片
@Test
public void test4() {
    FileInputStream inputStream = null;
    FileOutputStream outputStream = null;
    try {
        File src = new File("pic.png");
        File dest = new File("newpic.png");
        inputStream = new FileInputStream(src);
        outputStream = new FileOutputStream(dest);
        byte[] buf = new byte[5];
        int len;
        while ((len = inputStream.read(buf)) != -1) {
            outputStream.write(buf,0,len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            assert inputStream != null; // 並列寫完全OK,反正都是要關
            inputStream.close();
            assert outputStream != null;
            outputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

緩衝流

透過緩衝區提高讀寫效率

範例-使用緩衝流實現複製
@Test
public void bufferTest1() {
    FileInputStream fileInputStream = null;
    FileOutputStream fileOutputStream = null;
    BufferedInputStream bufferedInputStream = null;
    BufferedOutputStream bufferedOutputStream = null;
    try {
        File src = new File("陳菊.avi");
        File dest = new File("陳菊2.avi");

        fileInputStream = new FileInputStream(src);
        fileOutputStream = new FileOutputStream(dest);

        // 造緩衝流
        bufferedInputStream = new BufferedInputStream(fileInputStream);
        bufferedOutputStream = new BufferedOutputStream(fileOutputStream);

        // 用緩衝流操作
        byte[] buf = new byte[1024];
        int len;
        while ((len = bufferedInputStream.read(buf)) != -1) {
            bufferedOutputStream.write(buf, 0, len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        // 關閉流,穿脫衣服原則,後穿的先脫
        try {
            assert bufferedOutputStream != null;
            bufferedOutputStream.close();
            assert bufferedInputStream != null;
            bufferedInputStream.close();

            // 其實關閉外層流就把內層的也關了,所以可省略
            fileOutputStream.close();
            fileInputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}
  • 就是在原有的流上套一層緩衝流
  • 使用緩衝流讀寫速度有明顯提升,這邊懶的特別寫測試了
    • 緩衝流有flush()方法可以手動清空緩衝區,正常不需要,自動會清
  • 把input、outputStream換成Reader、Writer也能操作文字檔案,特別提下關於換行:
    • ReaderreadLine()方法
    • WriternewLine()方法

練習-用流實現加密解密

@Test
public void picLock() {
    FileInputStream fileInputStream = null;
    FileOutputStream fileOutputStream = null;
    try {
        // 在流的構造參數直接寫路徑相當於new一個File物件並包裝起來傳入,懶人福音
        fileInputStream = new FileInputStream("pic.png");
        fileOutputStream = new FileOutputStream("lockedPic.png");
        byte[] buf = new byte[10];
        int len;
        while ((len = fileInputStream.read(buf)) != -1) {
            // 進行加密修改字節,注意不是用foreach,那是讀出數據賦到一個暫時變量=白改
            for (int i = 0; i < buf.length; i++) {
                byte b = buf[i];
                b = (byte) (b ^ 5); // 異或,兩者不同為true
                buf[i] = b;
            }
            fileOutputStream.write(buf, 0, len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            fileInputStream.close();
            fileOutputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

兩個知識點:

  • 在流的構造參數直接寫路徑,省略創造File物件的步驟
  • 由於加密方法是異或,再執行一遍就是解密了

轉換流

提供字節流跟字符流之間的轉換,本身是一種處理流、字符流(看最尾詞綴)

解碼
  • 字節 -> 字符 (從一串瑣碎看不懂的轉為看得懂的,小到大)

  • InputStreamReader:將一個字節的輸入流轉換為字符的輸入流

  • new InputStreamReader(fileInputStream,"UTF-8");
    
    • 根據讀入的字節流需要指明編碼格式,沒寫會依照預設的
編碼
  • 字符 -> 字節 (把人看的轉成給機器看的,大到小)

  • OutputStreamWriter:將一個字符的輸出流轉換為字節的輸出流

  • new OutputStreamWriter(fileOutputStream,"Big5");
    
    • 一樣需要指明編碼,這邊有點不好理解,outputStreamWriter物件本身屬於字符流,調用write()方法時,根據編碼格式輸出對應的字節流

Unicode與UTF-8

  • Unicode字符集是定義字符唯一編號的集合,相當於一本超大超厚的電話簿,他使用16+1位面符來編號,所以上限不是65536個字,總之全世界的文字都能存的下
  • 考慮Unicode的"大小或長度"是沒有意義的,他只是一種規範、一張表,所謂"一個Unicode佔16bit=2char“是不嚴謹、不正確的說法
  • 真正用來傳輸、實現”編碼“的是Unicode Transformation Format,UTF
  • UTF-8就是用8bit=1byte當作最小單位,一個"字"可以由1~4個byte表示,由檔頭0或1的個數宣告此"字"有幾個byte;長度可變是為了靈活省空間
    • 理論上是1~6byte但實際只用到4,例如在MySQL中用utf8編碼會被打死,一定要用utf8mb4
  • 純英數字跟基本符號只佔1byte,與ACSII相同
  • UTF-8大部分漢字佔3byte;所以用字節流去裝漢字,很可能把字給切剩了造成亂碼

小結

  • 流的基本分類:
    • 方向:輸入(Input)、輸出(Output)
    • 單位:字節(Stream)、字符(Reader/Writer)
    • 功能:節點、處理
  • 節點流前綴:
    • 文件:File
    • 記憶體:CharArrayByteArrayString
    • 管道(進程通訊):Piped
  • 處理流:
    • 緩衝流:Buffered為前綴,可加速讀寫,套上後操作緩衝流(含關閉)
    • 轉換流:
      • InputStreamReader:將輸入的字節轉字符流(解碼)
      • OutputStreamWriter:將輸出的字符轉字節流(編碼)
    • 物件流:Object
    • 數據流:Data
    • 過濾流:Filter
    • 計數流:LineNumber
    • 打印流:Print

上次修改於 2021-12-11