天天看點

Java基礎(五)——異常機制、IO類的使用與原理前言異常機制IO流彩蛋

版本 說明 釋出日期
1.0 釋出文章第一版 2020-12-04
1.1 【死纏爛打的finally!】小節中新增一個更複雜的例子 2021-02-03
新增小節【關于read的傳回值為什麼是int】
【檔案通路流】的常用方法中新增以File為參數的構造方法

文章目錄

  • 前言
  • 異常機制
    • 基本概念
    • 異常的分類
    • 死纏爛打的finally!
      • 先來個簡單的
      • 再來個國小二年級難度的
    • 自定義異常類
  • IO流
    • IO流的親戚——File類
      • 常用方法
    • IO流基本概念
    • 基本分類
    • IO大家族
    • 檔案通路流
      • FileWriter
        • 常用方法
      • FileReader
        • 常用方法
        • 關于read的傳回值為什麼是int
      • FileOutputStream、FileInputStream
        • 檔案拷貝案例
          • 第一種方式
          • 第二種方式
          • 第三種方式
    • 緩沖流
      • BufferedOutputStream
      • BufferedInputStream
      • 用緩沖流拷貝檔案
      • BufferedWriter、BufferedReader
    • 列印流——PrintStream、PrintWriter
    • 轉換流——OutputStreanWriter、InputStreamReader
      • 舉栗子:模拟聊天日志功能
    • 資料流——DataOutputStream、DataInputStream
    • 對象流——ObjectOutputStream、ObjectInputStream
      • 說到對象流,就得說一說序列化
        • what is 序列化?
        • 序列化版本号
        • transient
      • 啃一塊栗子
    • RandomAccessFile
      • 基本概念
      • 常用方法
      • 栗子↓
  • 彩蛋

前言

  • 這篇文章是我個人的學習筆記,可能無法做到面面俱到,也可能會有各種纰漏。如果任何疑惑的地方,歡迎一起讨論~
  • 如果想完整閱讀這個系列的文章,歡迎關注我的專欄《Java基礎系列文章》~
  • 哦對了!請不要吝啬->點贊、關注、收藏~

異常機制

基本概念

  • 異常機制可以說是大家日常接觸最多的概念之一了,是以就隻說重點了。
  • 所有異常類的超類時Exception,位于java.lang.Exception。
  • 異常類Exception繼承自java.lang.Throwable,同樣繼承Throwable的還有Error。
    • Error通常描述JVM無法解決的嚴重錯誤,無法通過編碼解決。例如JVM崩潰了。
    • Exception可以通過編碼解決。
  • 異常的三種處理方式:
    • 通過條件語句,避免異常的發生;
    • try/catch/finally語句處理發生的異常;
    • throw抛出異常。

異常的分類

  • java.lang.Exception的子類:
    • RuntimeException:運作時異常,也叫非檢測異常。編譯階段無法檢測出來是否會發生此類異常。主要子類:
      • ArithmeticException:算術異常
      • ArrayIndexOutOfBoundsException:數組下标越界異常
      • NullPointerException:空指針異常
      • ClassCastException:類型轉換異常
      • NumberFormatException:數字格式異常
    • IOException和其他異常:檢測性異常,編譯階段能夠被編譯器(或者說現在通常都能被IDE檢測)檢測出來的異常。

死纏爛打的finally!

先來個簡單的

  • 大家都知道finally是無論有沒有異常都會執行的,但是你真的知道這家夥有多麼死纏爛打麼?比如下面這個例子,小夥伴們覺得結果是什麼?
public class ExceptionTest {
    public static void main(String[] args) {
        System.out.println(finallyTest());
    }

    private static String finallyTest(){
        String a = "aAa";
        try{
            System.out.println(1);
            int b = 1/0;
            System.out.println(2);
        }catch (ArithmeticException e){
            System.out.println(3);
            return a = a.toUpperCase();
        }finally {
            System.out.println(4);
            return a = a.substring(1);
        }
    }
}
           
  • 結果如下。怎麼樣,有沒有感受到finally的死纏爛打?
    • catch中return準備傳回了,甚至

      a = a.toUpperCase()

      都執行完了,然後finally突然吼了一嗓子:橋豆麻袋!
    • 然後finally裡面正常執行,甚至還return了。
    • 隻留下catch裡面的return,在原地傻了~
1
3
4
AA
           

再來個國小二年級難度的

  • 下面這個執行結果是多少?
public class ExceptionTest {
    public static void main(String[] args) {
        System.out.println(finallyTest());
    }

    private static int finallyTest(){
        int x = 1;
        try {
            return ++x;
        } catch (Exception e) {
        } finally {
            ++x;
        }
        return x;
    }
}
           
  • 答案是2。我知道大多數人都答錯了哈哈哈。下面來解釋一下。
    1. 執行

      return ++x;

      ,++x已經執行完了此時x為2,但是發現有finally,于是跑去執行finally;
    2. 執行finally,x變為3。因為finally并沒有傳回,是以執行完之後,又跑回去執行

      return ++x;

    3. 注意了,return傳回的是表達式的運算結果,而不是x的值。而

      return ++x;

      的結果在步驟1已經算出來了,是2!是以傳回的結果是2,而不是3。
    4. 可能還是有小夥伴十臉懵逼。我覺得可以如下了解。也就是說,return的表達式的值算出來了,固定了,不會再改變了。
    public class ExceptionTest {
        public static void main(String[] args) {
            System.out.println(finallyTest());
        }
    
        private static int finallyTest(){
            int x = 1, y;
            try {
                return y = ++x;
            } catch (Exception e) {
            } finally {
                ++x;
            }
            return x;
        }
    }
               

自定義異常類

  • 對于Java官方沒有提供的異常類,但業務場景需要對此類異常進行區分,就需要小夥伴們自己編寫。
  • 自定義異常很簡單,隻需要以下步驟:
    • 繼承Exception類或其子類;
    • 編寫一個無參構造和帶有String的有參構造。其中String是用來記錄錯誤描述的。
    • 兩個構造方法體中隻需要調用父類的對應構造方法即可。
    • 定義序列化版本号serialVersionUID。至于這玩意兒是什麼東西,等講IO的時候再說~
  • 舉例如下:
public class CustomException extends Exception {
    static final long serialVersionUID = -3387516993124229948L;

    public CustomException(){
        super();
    }

    public CustomException(String message){
        super(message);
    }

    public static void main(String[] args) {
        try{
            throw new CustomException("抛出去啦~");
        } catch (CustomException e) {
            e.printStackTrace();
        }
    }
}
           
  • 運作結果如下。
com.UsefulNativeClass.Exception.CustomException: 抛出去啦~
	at com.UsefulNativeClass.Exception.CustomException.main(CustomException.java:16)
           

IO流

IO流的親戚——File類

  • 位于java.io.File。主要用于描述檔案或目錄路徑的特征資訊, 如:大小、檔案名等。
  • 流相關的類其實在執行個體化的時候都是用到了File類的。

常用方法

方法聲明 功能
File(String pathname) 根據指定的路徑構造檔案或者目錄對象
File(String parent, String child) 根據指定的父路徑和子路徑構造檔案或者目錄對象
File(File parent, String child) 根據指定的父檔案路徑和子路徑構造檔案或者目錄對象
boolean exists() 判斷此路徑表示的檔案或目錄是否存在
String getName() 擷取檔案或者目錄的名稱
long length() 傳回檔案的長度,機關是byte
long lastModified() 擷取檔案的最後一次修改時間。是long類型的時間戳
String getAbsolutePath() 擷取絕對路徑
boolean delete() 删除檔案或目錄,當删除目錄時要求是空目錄。删除成功則傳回true。
boolean createNewFile() 建立新的空檔案。建立成功則傳回true。
boolean mkdir() 用于建立單級目錄。當建立d:/A/B時,如果A不存在,則建立失敗。
boolean mkdirs() 用于建立多級目錄當建立d:/A/B時,如果A不存在,則A和B一起建立。
File[] listFiles() 擷取該目錄下的所有檔案和目錄。如果調用對象是檔案,則會傳回null。
boolean isFile() 判斷是否為檔案。是檔案則傳回true。
boolean isDirectory() 判斷是否為目錄。是目錄則傳回true。
File[] listFiles(FileFilter filter) 擷取目錄下滿足篩選器的所有内容。FileFilter是一個接口類,定義了抽象方法accept,該方法要求想要的檔案傳回true,想被過濾的檔案傳回false。
  • 這些方法的使用還是說得比較清楚的,下面就實作一個周遊某目錄下所有目錄和java檔案的例子吧:
public class FileTest {
    //實作過濾器,隻尋找目錄和.java檔案
    private final FileFilter fileFilter = pathname -> {
        if(pathname.isFile()){
            return pathname.getName().endsWith(".java");
        }
        return true;
    };

    public static void main(String[] args) {
        FileTest fileTest = new FileTest();
        fileTest.listAllFile(new File("C:\\Users\\米\\Desktop\\子產品四 Java核心類庫(下)\\01任務一 異常機制和File類"));
    }

    private void listAllFile(File file){
        if(file.isFile()){
            return;
        }
        
        File[] listFile = file.listFiles(fileFilter);
        for (File f:listFile){
            if(f.isFile()){
                System.out.println(f.getName());
            }else if(f.isDirectory()){
                System.out.println("[" + f.getName() + "]");
                listAllFile(f);
            }
        }
    }
}
           
  • 運作結果如下。當然每個人電腦裡面的檔案不一樣,是以執行結果也沒啥參考價值。
[01_課件]
[02_圖檔]
[04_代碼]
AgeException.java
ExceptionCatchTest.java
ExceptionFinallyTest.java
ExceptionMethod.java
ExceptionPreventTest.java
ExceptionTest.java
ExceptionThrowsTest.java
FileTest.java
Person.java
PersonTest.java
SubExceptionMethod.java
           

IO流基本概念

  • 上面以迅雷不及掩耳盜鈴之勢講完了異常機制,然後順道還擦邊講了個File。接下來才是這篇文章的主角——IO stream。
  • IO相關類都位于java.io包。
  • 為什麼叫IO流呢?首先IO是in和out的意思,這個大家應該都知道,至于stream,可能java的coder們覺得資料能有幾多愁,恰似一江春水向東流吧~
  • 不開玩笑了,其實流執行個體化的過程,就像是給水桶中插了根水管;對流進行讀入、寫出的過程,就像是給水桶抽水、加水的過程;重新整理流,就像是把水管中殘留的水分給清幹淨;關閉流,就像是把水管從水桶中抽出來。

基本分類

  • 按照讀寫資料的基本機關不同,分為位元組流和字元流。
    • 位元組流:主要指以位元組為機關進行資料讀寫的流,可以讀寫任意類型的檔案。
    • 字元流:主要指以字元(2個位元組)為機關進行資料讀寫的流,隻能讀寫文本(字元)檔案。
  • 按照讀寫資料的方向不同,分為輸入流和輸出流。
    • 輸入流:主要指從檔案中讀取資料内容輸入到程式中,也就是讀檔案。
    • 輸出流:主要指将程式中的資料内容輸出到檔案中,也就是寫檔案。
  • 按照流的角色不同分為節點流和處理流。
    • 節點流:主要指和輸入輸出源直接對接的流。
    • 處理流:主要指建立在節點流的基礎之上的流。處理流與檔案是間接關聯的。

IO大家族

分類 位元組輸入流 位元組輸岀流 字元輸入流 字元輸岀流
抽象基類 InputStream OutputStream Reader Writer
通路檔案 FileInputStream FileOutputStream FileReader FileWriter
通路數組 ByteArrayInputStream ByteArrayOutpuStream CharArrayReader CharArrayWriter
通路管道 PipedlnputStream PipedOutputStream PipedReader PipedWriter
通路字元串 StringReader StringWriter
緩沖流 BufferedlnputStream BufferedOutputStream BufferedReader BufferedWriter
轉換流 InputStreamReader OutputStreamWriter
對象流 ObjectInputStream ObjectOutputStream
過濾流 FilterInputStream FiIterOutputStream FilterReader FilterWriter
列印流 PrintStream PrintWriter
推回輸入流 PushbackinputStream PushbackReader
特殊流 DatalnputStream DataOutputStream

檔案通路流

FileWriter

  • 主要用于對檔案進行資料寫入。

常用方法

  • 如果構造流對象的時候,檔案不存在,則會自動建立對應檔案。
方法聲明 功能
FileWriter(String fileName) 根據參數指定的路徑構造對象。執行個體化之後,會清空檔案原有資料。
FileWriter(File file) 根據參數指定的檔案構造對象。執行個體化之後,會清空檔案原有資料。
FileWriter(String fileName, boolean append) 根據參數指定的路徑來構造對象。傳入true時,表示不清空原有資料,寫出資料追加在末尾。
void write(int c) 寫入單個字元。如果傳入的是整數,則代表的是Unicode值。
void write(char[] cbuf, int off, int len) 将指定字元數組,從下标off開始的,len個字元寫入檔案。
void write(char[] cbuf) 将指定字元數組寫入檔案。
void flush() 重新整理流。因為各種機制的原因,write操作并不一定會立即寫入資料。而重新整理的作用就是将目前打算寫入,但還未寫入的資料,全部立即寫入。
void close() 關閉流對象并釋放有關的資源。關閉流自帶重新整理流的功能。
  • 老規矩,栗子來了:
public class FileWriterTest {
    public static void main(String[] args) {
        FileWriterTest test = new FileWriterTest();
        test.baseUse();
    }

    private void baseUse(){
        FileWriter fileWriter = null;
        try {
            fileWriter = new FileWriter("D:\\WorkSpace\\Work\\java-practice\\JavaPractice\\src\\com\\UsefulNativeClass\\IO\\1.txt");
            fileWriter.write('a');
            String str = "啊哦一";
            fileWriter.write(str, 1, 2);
            System.out.println("執行成功");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(fileWriter != null){
                try {
                    fileWriter.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        try {
            fileWriter = new FileWriter("D:\\WorkSpace\\Work\\java-practice\\JavaPractice\\src\\com\\UsefulNativeClass\\IO\\1.txt", true);
            fileWriter.write('a');
            String str = "啊哦一";
            fileWriter.write(str, 1, 2);
            System.out.println("執行成功");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(fileWriter != null){
                try {
                    fileWriter.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
           
  • 檔案内容如下。為了友善示範,建立了兩次流操作。可以看到,不管代碼執行多少次,檔案裡面的内容始終都是一樣的。因為第一次的流操作,會清空檔案資料。
a哦一a哦一
           

FileReader

  • 主要用于對檔案内容進行讀操作。

常用方法

方法聲明 功能
FileReader(String fileName) 根據參數指定路徑構造對象。
FileReader(File file) 根據參數指定檔案構造對象。
int read() 讀取單個字元的資料并傳回,傳回-1表示一個字元都沒讀到。
int read(char[] cbuf, int offset, int length) 将最多length個字元的資料讀入一個字元數組中,從下标位置offset開始放(注意不要下标越界)。傳回讀取到的字元個數,傳回-1表示一個字元都沒讀到。
int read(char[] cbuf) 将最多cbuf.length個字元的資料讀入字元數組中。傳回讀取到的字元個數,傳回-1表示一個字元都沒讀到。
void close() 關閉流對象并釋放有關的資源
  • 栗子來啦:
public class FileReaderTest {
    public static void main(String[] args) {
        FileReaderTest test = new FileReaderTest();
        test.baseUse();
    }

    private void baseUse(){
        FileReader fr = null;
        try {
            fr = new FileReader("D:\\WorkSpace\\Work\\java-practice\\JavaPractice\\src\\com\\UsefulNativeClass\\IO\\1.txt");
            int res;
            while((res = fr.read()) != -1){
                System.out.println("讀取的内容是:" + (char)res);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(null != fr){
                try {
                    fr.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        try {
            fr = new FileReader("D:\\WorkSpace\\Work\\java-practice\\JavaPractice\\src\\com\\UsefulNativeClass\\IO\\1.txt");
            int res;
            char[] chars = new char[3];
            res = fr.read(chars, 1, 2);
            System.out.println("讀取到了:"+ res + "個字元,讀取結果是:" + Arrays.toString(chars));
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(null != fr){
                try {
                    fr.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
           
  • 結果如下。如果第二段栗子中,讀取的個數改為3,則會有下标越界異常。
讀取的内容是:a
讀取的内容是:哦
讀取的内容是:一
讀取的内容是:a
讀取的内容是:哦
讀取的内容是:一
讀取到了:2個字元,讀取結果是:[ , a, 哦]
           

關于read的傳回值為什麼是int

  • 我們知道,Java中一個char固定占2個位元組,因為其用Unicode來編碼的。但為什麼傳回值要用int呢?int可是得用4個位元組呢。這就涉及到一個設計思想的問題了。
    • 假設我們隻用char來作為傳回值(2個位元組),那麼我們怎麼表示沒有讀到任何資料呢?沒辦法表示吧?
    • 是以Java就用int來作為傳回值,用-1表示沒有讀取到任何值。而正常讀取到的字元對應的二進制,隻會用到低16位,并不會和-1(1111 1111 1111 1111)沖突。
    • 下面要講的位元組流,也是同樣的道理~是不是覺得Java的設計師們很聰明呢?

FileOutputStream、FileInputStream

  • 上面介紹的是字元流,操作文本(字元)檔案妥妥的。但如果操作的不是文本檔案,那麼用字元流就會導緻檔案不正常或者損壞。這個時候就需要我們的位元組流上場了。
  • 常用方法和字元流幾乎一樣,隻是之前的char[]變成了byte[]。
  • FileInputStream多了一個比較常用的方法:

    int available()

    。用于擷取檔案的大小(機關為位元組)。

檔案拷貝案例

  • 既然都說了位元組流和字元流的使用基本一樣,那我就不費口舌來介紹怎麼使用位元組流了。我們直接來玩一個有意思的東西——檔案拷貝。
第一種方式
public class FileCopy {
    public static void main(String[] args) {
        FileCopy test = new FileCopy();
        test.fileCopyFirst("D:\\WorkSpace\\Work\\java-practice\\JavaPractice\\src\\com\\UsefulNativeClass\\IO\\FileIO\\background.jpg",
                "D:\\WorkSpace\\Work\\java-practice\\JavaPractice\\src\\com\\UsefulNativeClass\\IO\\FileIO\\background-back.jpg");
    }

    private void fileCopyFirst(String srcPath, String destPath) {
        OutputStream out = null;
        InputStream in = null;
        try {
            out = new FileOutputStream(destPath);
            in = new FileInputStream(srcPath);

            System.out.println("檔案大小:" + in.available());

            //記個時
            long beginTime = System.currentTimeMillis();
            System.out.println("開始copy");

            int result;
            while (-1 != (result = in.read())) {
                out.write(result);
            }

            long endTime = System.currentTimeMillis();
            System.out.println("結束copy");
            System.out.println("耗時:" + (endTime - beginTime));
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (null != out) {
                    out.close();
                }
                if (null != in) {
                    in.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
           
  • 這種方式運作結果如下。大家可以感受到一個問題。對于一個2MB的圖檔,拷貝過程需要将近10秒,這是一件非常誇張的事情。
檔案大小:2153023
開始copy
結束copy
耗時:16107
           
  • 為什麼會這樣呢?因為每一次執行read或者write的時候,其實JVM是與計算機底層硬體做了很多互動的,這個過程開支比較大。如果一個位元組就進行一次read和write,總共需要多少次?amazing!
  • 這個感覺就像什麼呢?老媽讓你去買50個雞蛋,然後你去菜市場,每次就買1個雞蛋回家。然後來回跑了50次!amazing!
第二種方式
  • 你吸取教訓了,這次打算一次性把所有需要的雞蛋給買回去,于是代碼變成了這樣:
public class FileCopy {
    public static void main(String[] args) {
        FileCopy test = new FileCopy();
        test.fileCopyFirst("D:\\WorkSpace\\Work\\java-practice\\JavaPractice\\src\\com\\UsefulNativeClass\\IO\\FileIO\\background.jpg",
                "D:\\WorkSpace\\Work\\java-practice\\JavaPractice\\src\\com\\UsefulNativeClass\\IO\\FileIO\\background-back.jpg");
    }

    private void fileCopySecond(String srcPath, String destPath) {
        OutputStream out = null;
        InputStream in = null;
        try {
            out = new FileOutputStream(destPath);
            in = new FileInputStream(srcPath);

            //記個時
            long beginTime = System.currentTimeMillis();

            //讀取資料至緩存
            System.out.println("開始讀資料");
            byte[] buffer = new byte[in.available()];
            int result = in.read(buffer);
            System.out.println("實際讀取的位元組數:" + result);

            //從緩存寫出資料
            System.out.println("開始寫資料");
            out.write(buffer);

            long endTime = System.currentTimeMillis();
            System.out.println("耗時:" + (endTime - beginTime));
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (null != out) {
                    out.close();
                }
                if (null != in) {
                    in.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
           
  • 運作結果如下。我去,這也太快了。然後你又覺得你自己行了,于是決定:以後不管多少個雞蛋,我就一次性撈回去完事兒~
開始讀資料
實際讀取的位元組數:2153023
開始寫資料
耗時:30
           
  • 結果小區裡面的大爺大媽都覺得你行了,于是都找你幫忙買雞蛋。于是,你一天需要買一萬個雞蛋。然後你試着一次性買回去一萬個雞蛋…你又發現你不行了。
第三種方式
  • 你痛并思痛,想到為什麼我不能每次就買50個呢?雖然需要多跑幾次,但總比被直接壓死強是吧!于是代碼變成了這樣:
public class FileCopy {
    public static void main(String[] args) {
        FileCopy test = new FileCopy();
        test.fileCopyThird("D:\\WorkSpace\\Work\\java-practice\\JavaPractice\\src\\com\\UsefulNativeClass\\IO\\FileIO\\background.jpg",
                "D:\\WorkSpace\\Work\\java-practice\\JavaPractice\\src\\com\\UsefulNativeClass\\IO\\FileIO\\background-back.jpg");
    }

    private void fileCopyThird(String srcPath, String destPath) {
        OutputStream out = null;
        InputStream in = null;
        try {
            out = new FileOutputStream(destPath);
            in = new FileInputStream(srcPath);

            //記個時
            long beginTime = System.currentTimeMillis();

            //讀取資料至緩存
            System.out.println("開始拷貝資料");
            byte[] buffer = new byte[102400];
            int result;
            //隻要還沒有讀到檔案末尾,就一直循環
            while((result = in.read(buffer)) != -1){
                out.write(buffer, 0, result);//注意,因為最後一次寫入,可能并沒有放滿buffer,是以,每一次放的時候,應該是buffer有多少,就放多少
                System.out.println("實際讀取的位元組數:" + result);
            }

            long endTime = System.currentTimeMillis();
            System.out.println("耗時:" + (endTime - beginTime));
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (null != out) {
                    out.close();
                }
                if (null != in) {
                    in.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
           
  • 代碼執行結果如下。你覺得你雙行了。好吧,這次确實行了。但是機智的java官方早已看穿一切,于是提供了緩沖流。緩沖流的原理基本就是如此。
開始拷貝資料
實際讀取的位元組數:102400
實際讀取的位元組數:102400
實際讀取的位元組數:102400
實際讀取的位元組數:102400
實際讀取的位元組數:102400
實際讀取的位元組數:102400
實際讀取的位元組數:102400
實際讀取的位元組數:102400
實際讀取的位元組數:102400
實際讀取的位元組數:102400
實際讀取的位元組數:102400
實際讀取的位元組數:102400
實際讀取的位元組數:102400
實際讀取的位元組數:102400
實際讀取的位元組數:102400
實際讀取的位元組數:102400
實際讀取的位元組數:102400
實際讀取的位元組數:102400
實際讀取的位元組數:102400
實際讀取的位元組數:102400
實際讀取的位元組數:102400
實際讀取的位元組數:2623
耗時:37
           

緩沖流

  • 緩沖流提供了一個緩沖區,用于對流資料進行緩沖。就像上面檔案拷貝的例子三那樣。
  • 緩沖流的使用還是大同小異,但是需要注意一點:緩沖流是處理流,是以其構造方法的入參是InputStream和OutputStream。
  • 關閉緩沖流的時候,與其關聯的流也會自動關閉。是以每次操作完成後,close緩沖流即可。

BufferedOutputStream

  • 常用構造方法
方法聲明 功能
BufferedOutputStream(OutputStream out) 根據參數指定的輸出流來構造對象,預設緩沖大小為8192位元組。
BufferedOutputStream(OutputStream out, int size) 根據參數指定的輸出流來構造對象,并手動指定緩沖區大小

BufferedInputStream

  • 常用構造方法
方法聲明 功能
BufferedInputStream(InputStream in) 根據參數指定的輸入流構造對象,預設緩沖大小為8192位元組。
BufferedInputStream(InputStream in, int size) 根據參數指定的輸入流構造對象,并手動指定緩沖區大小

用緩沖流拷貝檔案

  • 還是上面那個例子的檔案,我們再用緩沖流來試一試:
public class BufferTest {
    public static void main(String[] args) {
        BufferTest test = new BufferTest();
        test.fileCopyThird("E:\\Gitee repository\\java-practice\\JavaPractice\\src\\com\\UsefulNativeClass\\IO\\FileIO\\background.jpg",
                "E:\\Gitee repository\\java-practice\\JavaPractice\\src\\com\\UsefulNativeClass\\IO\\FileIO\\background-back.jpg");
    }

    private void fileCopyThird(String srcPath, String destPath) {
        BufferedOutputStream out = null;
        BufferedInputStream in = null;
        try {
            out = new BufferedOutputStream(new FileOutputStream(destPath), 102400);
            in = new BufferedInputStream(new FileInputStream(srcPath), 102400);

            System.out.println("檔案大小:" + in.available());

            //記個時
            long beginTime = System.currentTimeMillis();

            //讀取資料至緩存
            System.out.println("開始拷貝資料");
            byte[] buffer = new byte[102400];
            int result;
            //隻要還沒有讀到檔案末尾,就一直循環
            while ((result = in.read(buffer)) != -1) {
                out.write(buffer, 0, result);//注意,因為最後一次寫入,可能并沒有放滿buffer,是以,每一次放的時候,應該是buffer有多少,就放多少
                System.out.println("實際讀取的位元組數:" + result);
            }
            System.out.println("實際讀取的位元組數:" + result);

            long endTime = System.currentTimeMillis();
            System.out.println("耗時:" + (endTime - beginTime));
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (null != out) {
                    out.close();
                }
                if (null != in) {
                    in.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
           
  • 運作結果如下。你大爺還是你大爺,果然牛逼是吧~注意啦,可以看到雖然我用了緩沖流,但我還是使用的第三種方式拷貝,即自己手動寫了個緩沖。實踐證明這樣的效率是最高的。如果我一次性隻讀一個位元組,即使使用緩沖流,耗時大概會在90左右。
檔案大小:2153023
開始拷貝資料
實際讀取的位元組數:102400
實際讀取的位元組數:102400
實際讀取的位元組數:102400
實際讀取的位元組數:102400
實際讀取的位元組數:102400
實際讀取的位元組數:102400
實際讀取的位元組數:102400
實際讀取的位元組數:102400
實際讀取的位元組數:102400
實際讀取的位元組數:102400
實際讀取的位元組數:102400
實際讀取的位元組數:102400
實際讀取的位元組數:102400
實際讀取的位元組數:102400
實際讀取的位元組數:102400
實際讀取的位元組數:102400
實際讀取的位元組數:102400
實際讀取的位元組數:102400
實際讀取的位元組數:102400
實際讀取的位元組數:102400
實際讀取的位元組數:102400
實際讀取的位元組數:2623
實際讀取的位元組數:-1
耗時:9
           

BufferedWriter、BufferedReader

  • 字元流依然有緩沖流,同樣也是效率很高的一種流。
  • BufferedWriter常用方法:
方法聲明 功能
BufferedWriter(Writer out) 根據參數指定的輸出流來構造對象。預設緩沖大小為8192。
BufferedWriter(Writer out, int sz) 根據參數指定的輸出流來構造對象。并指定緩沖大小。
void write(int c) 寫出單個字元
void write(char[] cbuf, int off, int len) 将字元數組cbuf中,從下标off開始的,len個字元寫出
void write(char[] cbuf) 将字元數組cbuf的所有内容寫出
void write(String s, int off, int len) 将字元串s中,下标從off開始的,len個字元寫出
void write(String str) 将字元串str的所有内容寫出
void newLine() 寫出行分隔符。對于WINDOWS而言,即寫出"\r\n"
void flush() 重新整理流
void close() 關閉流對象并釋放有關的資源
  • BufferedReader常用方法:
方法聲明 功能
BufferedReader(Reader in) 根據參數指定的輸入流來構造對象。預設緩沖大小為8192.
BufferedReader(Reader in, int sz) 根據參數指定的輸入流來構造對象。并指定緩沖大小。
int read() 從輸入流讀取單個字元并傳回。如果讀取到末尾則傳回-1。
int read(char[] cbuf, int off, int len) 從輸入流中讀取len個字元,放入數組cbuf中,放入的位置從下标off開始。傳回實際讀取到的字元個數。如果讀到末尾,則傳回-1
int read(char[] cbuf) 從輸入流中讀滿整個數組cbuf。傳回實際讀取到的字元個數。如果讀到末尾,則傳回-1
String readLine() 讀取一行字元串并傳回,傳回null表示讀取到末尾
void close() 關閉流對象并釋放有關的資源

列印流——PrintStream、PrintWriter

  • 列印流也是一個處理流。
  • 這個流其實小夥伴們都非常非常非常非常非常非常熟悉,為什麼呢?因為我們天天在用的

    System.out

    的這個out,其實就是一個PrintStream。哈哈哈!意不意外?
  • 隻是說System的這個out呢,都是向控制台寫出資料。但實際上,PrintStream是一個處理流。是以可以關聯OutputStream,進而向其他檔案寫資料。
  • PrintStream常用方法如下:
方法聲明 功能
PrintStream(OutputStream out) 根據參數指定的輸出流來構造對象。
void print(String s) 輸出字元串内容。
void println(String x) 輸出字元串内容,并追加換行符。
void flush() 重新整理流
void close() 用于關閉輸出流并釋放有關的資源
  • PrintWriter除了構造方法入參是字元輸出流,其他的方法基本一緻。

轉換流——OutputStreanWriter、InputStreamReader

  • 用于将位元組流轉換為字元流。是一種處理流。
  • OutputStreamWriter的主要方法如下:
方法聲明 功能
OutputStreamWriter(OutputStream out) 根據參數指定的位元組流來構造對象
OutputStreamWriter(OutputStream out, String charsetName) 根據參數指定的位元組流和編碼構造對象
void write(String str) 将參數指定的字元串寫入
void flush() 重新整理流
void close() 用于關閉輸出流并釋放有關的資源
  • 可以看到,依然大同小異,隻是構造方法的參數是位元組流。輸出流也是同理,就不列舉啦~

舉栗子:模拟聊天日志功能

  • 我們可以通過緩沖位元組流、列印流、轉換流,來實作一個聊天日志的功能。而聊天的内容通過控制台輸入。輸入bye表示聊天結束,結束程式。
  • 代碼如下:
public class ChatRoom {
    private void chatLog() {
        BufferedReader reader = null;
        PrintWriter writer = null;
        try {
            //首先,System.in是一個擷取鍵盤輸入的标準方法,但是這個in呢,它是一個位元組輸入流。
            //然後呢,我們要擷取的是字元,是以理所應當用緩沖字元輸入流。
            //是以,我們需要在二者之間加一個隔壁老王——轉換流
            reader = new BufferedReader(new InputStreamReader(System.in));
            //因為聊天一般都是一個換行符作為一段話,是以正好可以用列印流
            writer = new PrintWriter(new FileWriter("D:\\logs.txt", true));

            //用于判斷該誰說話勒
            boolean flag = false;

            String content;
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyy-MM-dd hh:mm:ss");
            while (true) {
                System.out.println(flag ? "張三說:" : "李四說:");
                content = reader.readLine();
                if ("bye".equals(content)) {
                    writer.println("聊天結束");
                    System.out.println("聊天結束");
                    break;
                } else {
                    writer.println(formatter.format(LocalDateTime.now()) + (flag ? " 張三說:" : " 李四說:") + content);
                    flag = !flag;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (null != reader) {
                    reader.close();
                }
                if (null != writer) {
                    writer.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        ChatRoom chatRoom = new ChatRoom();
        chatRoom.chatLog();
    }
}
           
  • 控制台内容如下:
李四說:
hello, little jade
張三說:
who?
李四說:
sorry
張三說:
bye
聊天結束
           
  • 檔案中内容如下:
2020-12-02 10:19:24 李四說:hello, little jade
2020-12-02 10:19:28 張三說:who?
2020-12-02 10:19:33 李四說:sorry
聊天結束
           
  • 這個栗子中,因為始終是對聊天内容的處理,是以最外層都是選擇字元流進行操作。而控制台輸入是一個位元組流,是以用到了轉換流。
  • 通常情況下,不論是輸出還是輸入,都應當使用一個緩沖流來提高效率。
  • 而這個栗子中,為什麼輸出還套了個列印流呢?因為println這個方法真香,哈哈哈~

資料流——DataOutputStream、DataInputStream

  • 用于對基本資料類型進行讀寫。是一個處理流。
  • DataOutputStream常用方法:
方法聲明 功能
DataOutputStream(OutputStream out) 根據參數指定的輸出流構造對象。
void writeInt(int v) 将一個整數一次性寫出。同理,所有基本類型都有一個對應的write方法
void close() 用于關閉檔案輸出流并釋放有關的資源。
  • DataInputStream
方法聲明 功能
DataInputStream(InputStream in) 根據參數輸入流來構造對象。
int readInt() 一次性讀取一個整數資料。同理,别的類型也有
void close() 用于關閉檔案輸出流并釋放有關的資源
  • 資料流雖然看起來很簡單,但是有一個坑,第一次接觸的小夥伴那是前仆後繼地往裡跳。下面來填一下:
public class DataIOTest {
    public static void main(String[] args) throws IOException {
        try (DataOutputStream dos = new DataOutputStream(new FileOutputStream("d:/data.txt"));
             DataInputStream dis = new DataInputStream(new FileInputStream("d:/data.txt"))) {

            int num = 69;//0000 0000 0000 0000 0000 0000 0100 0011
            dos.writeInt(num);
            System.out.println("資料寫入成功");

            System.out.println(dis.readInt());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
           
  • 運作結果如下。風平浪靜,波瀾不驚。但是再去看一下txt檔案,小夥伴們可能會有點懵
資料寫入成功
69
           
  • data.txt内容如下。這是什麼意思呀?其實道理很簡單:
    • 我們知道int是4個位元組,是以69對應的二進制編碼如上面代碼裡面的注釋所示。
    • 但是我們把檔案的類型設定為了txt,也就是說當成文本檔案來看了。
    • 當我們打開檔案的時候,将二進制編碼以預設編碼UTF-8進行文本解析。
    • 解析結果就是:空字元(0000 0000)、空字元(0000 0000)、空字元(0000 0000)、E(0100 0011)。
    • 注意空字元和空格不是一個意思哈,雖然看起來是一樣的。
E
           
  • 資料流還有一個小坑:
    • 如果檔案裡面就隻有1個位元組的内容。
    • 此時我們readInt(),那麼就會發生EOFException(檔案末尾異常)。
    • 這個異常有點數組下标越界異常内味兒,但還是不一樣的哈。

對象流——ObjectOutputStream、ObjectInputStream

  • 将對象作為一個整體,進行讀寫操作。是一個處理流。
  • ObjectOutputStream常用方法:
方法聲明 功能
ObjectOutputStream(OutputStream out) 根據參數指定的輸出流來構造對象
void writeObject(Object obj) 将一個對象整體寫出
void close() 用于關閉輸出流并釋放有關的資源
  • ObjectInputStream常用方法:
方法聲明 功能
ObjectInputStream(InputStream in) 根據參數指定的輸入流來構造對象。
Object readObject() 讀取一個對象。無法通過傳回值來判斷是否讀取到檔案的末尾。
void close() 用于關閉輸入流并釋放有關的資源。
  • 可以看到,對象流的寫出和讀入方法,都是對對象進行操作的。并且,操作的對象必須要啟用序列化,否則會抛出NotSerializableException(不可序列化異常)。
  • 因為readObject()無法判斷是否讀取到檔案末尾,是以我個人建議一個序列化檔案中就存放一個對象。
    • 那如果有一個檔案中存放多個對象的需求怎麼辦呢?很簡單啦,可以采用集合存儲,然後序列化集合對象喽~

說到對象流,就得說一說序列化

what is 序列化?

  • 所謂序列化,是指将一個對象需要存儲的相關資訊通過一定規則,有效地組織成位元組序列的過程。
  • 而反序列化,是指将有效組織的位元組序列,按照一定規則,恢複成對象的過程。
  • 而實作序列化的方式很簡單,隻需要實作Serializable接口即可,不需要重寫任何方法。因為其實所有類已經是可以序列化的,但預設情況都未啟用。隻有當實作了接口,才會告訴JVM:這個類啟用序列化。

序列化版本号

  • 為了在一定程度保證安全性,序列化機制通過serialVersionUID來驗證版本一緻性的。
  • 在進行反序列化時,JVM會把傳來的流中的serialVersionUID與本地相應類的serialVersionUID進行比較。
    • 如果相同就認為是一緻的,可以進行反序列化。
    • 如果不一緻,就會抛出InvalidCastException(序列化版本不一緻)異常。

transient

  • 是Java語言的關鍵字。當給屬性加上該關鍵字後,該屬性将不會參與序列化。

啃一塊栗子

public class Person implements Serializable {
    private static final long serialVersionUID = 13412341324L;
    private String name;
    private transient String gender;

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

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

    public static void main(String[] args) {
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("d:/se.txt"));
             ObjectInputStream ois = new ObjectInputStream((new FileInputStream("d:/se.txt")))) {
            Person person = new Person("angel", "糙漢子");
            oos.writeObject(person);
            System.out.println("寫出成功");

            Object newPer = ois.readObject();
            System.out.println(newPer);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}
           
  • 運作結果如下。這個栗子有幾點說一下:
    • 檔案内容就是一堆亂碼,因為不是按照字元寫的檔案嘛。
    • 可以看到,序列化主要是用于将類中的屬性寫入檔案。是以序列化主要适用于Bean。
    • 序列化版本号其實不寫也不會報錯。但是上面說了,出于安全性考慮,建議添加序列化版本号。
    • readObject的傳回值其實用到了多态。雖然傳回值是Object,但是執行個體依然是對應的類的執行個體。
    • 這個栗子中,我把gender用了transient關鍵字修飾。可以發現gender沒有序列化,是以是String的預設值null。
寫出成功
Person{name='angel', gender='null'}
           

RandomAccessFile

基本概念

  • 這個類支援對檔案的随機讀寫。
    • 這裡所謂的随機,是指在檔案中讀寫的位置是任意的。而不是像通常的流,固定從開頭往後讀。

常用方法

方法聲明 功能
RandomAccessFile(String name, String mode) 根據參數指定的路徑和模式構造對象。如果檔案不存在則報錯。
RandomAccessFile(File file, String mode) 根據參數指定的File對象和模式構造對象。
int read() 讀取單個位元組的資料
void seek(long pos) 将讀寫位置設定成距檔案開頭pos個位元組的位置
void write(int b) 寫出單個位元組的資料,寫出的資料覆寫目前位置的資料。
void close() 用于關閉流并釋放有關的資源
  • 構造方法中的這個模式啊,簡單來說有幾種:
    • r:以隻讀方式打開;
    • rw:以讀寫方式打開;
    • rwd:以讀寫方式打開,且同步檔案内容的更新;
    • rws:以讀寫方式打開,且同步檔案内容和中繼資料的更新。
  • 什麼叫同步呢?意思是每進行一次寫操作,就會将内容及時寫入磁盤。
    • 這樣做的好處是當系統發生崩潰時,已經調用write()方法的内容不會丢失。
    • 壞處也很明顯,就是降低了寫出的速度。
    • 而rwd和rws有什麼差別呢?目前我隻知道rws需要同步的東西更多,速度更慢。具體什麼是中繼資料,我還沒有研究。
  • 這個類還有很多東西比較複雜,但是平時工作中基本不會用到,是以就不展開講了。

栗子↓

public class RandomAccessTest {
    public static void main(String[] args) {
        //這個類不會自動建立檔案,記得先用别的方式建立一個
        try (RandomAccessFile file = new RandomAccessFile("d:/ra.txt", "rw")) {
            //為了測試友善,先向檔案中寫一點内容。
            //建議不要寫中文,這個類的寫方法對非ASCII碼的字元沒有太好的支援
            file.writeBytes("what are you doing?");

            //然後我們來讀“doing”這個單詞。
            //注意,seek偏移的是位元組量。而一個ASCII碼字元是1個位元組。
            //是以可以算出,“doing”在檔案中,需要偏移13個位元組
            file.seek(13);
            System.out.println(file.readLine());

            //然後再來測試一下把you改成she。
            //不要忘了這個類的寫方法是覆寫的。
            file.seek(9);
            file.writeBytes("she");

            //展示一下結果
            file.seek(0);
            System.out.println(file.readLine());

        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}
           
  • 執行結果如下。重要的資訊我都寫在代碼注釋裡面了,小夥伴們可以自行欣賞~~~~
doing?
what are she doing?
           

彩蛋

  • 剛剛發文章的時候,看到CSDN系統給我發了個段子哈哈哈。分享一下:
  • 論Java變量指派的鄙視鍊
    Java基礎(五)——異常機制、IO類的使用與原理前言異常機制IO流彩蛋