Java IO流之IO流基礎

寫在前面

   這是我專欄IO流的第一篇博文。同時也是Java中IO操作的基礎。爲後續講解NIO、BIO、AIO等技術奠定一個基礎,那麼話不多說開始今天的內容。

一、IO流介紹

1.什麼是IO

   IO(輸入輸出)是指計算機同外部設備之間的數據傳遞。常見的輸入輸出設備有文件、鍵盤、打印機、屏幕等。數據可以按記錄(或稱數據塊)的方式傳遞,也可以 流的方式傳遞。
   那麼對於輸入和輸出應該怎麼去理解呢?其實很簡單,當你內存中寫入數據時這個操作就是輸入(input),當你將內存中的數據寫入到硬盤中時這個操作就叫輸出(output)。比如說讀取文件就是是input,保存(寫入)文件就是output。

2.什麼是“流”

   “流”是一種抽象概念,它表示數據的無結構化傳遞。按照流的方式進行輸入輸出,數據被當成無結構的字節序或字符序列。從流中取得數據的操作稱爲提取操作,而向流中添加數據的操作稱爲插入操作。
  當然這麼說偏向於官方文檔上的說明,那麼怎麼理解“流”這個概念呢?舉個例子,在一桶水(數據)裏放入一些帶顏色的顆粒(數據塊),將它轉移到另一個水桶中。第一種方法,用水管將桶裏的水轉移到另一個桶中,在轉移的過程中可以看到這些顆粒每次轉移的順序是不一定的。還有一種方法就是將水凍成冰,然後直接倒入另一個桶中,在這個過程中顆粒的位置沒有發生變化。不管是那種方法最後數據都成功轉移了,區別是數據塊轉移順序的變化。第一種轉移方法就是類似於“流”。
  那麼,可能有人就會想,數據塊位置變了那麼數據不也就變了嗎?其實我以前也這麼想過,但是後來閱讀了源碼發現IO流操作是會涉及到一個接口:Serializable接口(序列化)。而正是由於這個接口才使得我們的數據在傳輸前後不會改變。

3.Serializable接口

  Serializable接口僅爲標記接口,不包含任何方法定義。因此實現Serializable接口的類不需要重寫任何方法。
  當一個類實現了Serializable接口,表示該類可以序列化,序列化的目的是將一個實現了Serializable接口的對象可以轉換成一個字節序列,保存對象的狀態。正是因爲這個接口才能在數據傳輸前後保證數據的一致性。(PS:Serializable接口實現原理會單獨放到一篇博文裏講,這裏不贅述。)

4.IO流的分類

   先看下IO流的結構圖,如圖1.4.1所示:
IO流結構圖

圖 1.4.1

  其實IO流的分類不止這一種,比如按照讀寫方式可分爲輸入流和輸出流,這裏之所以這麼分類是爲了後面講解字符流與字節流之間的相互轉換。

二、IO基礎之File類

  之所以把File類放在這個位置講解是因爲後面的字符流/字節流都會涉及到文件的操作。下面來看看File類的具體內容:

1. File類介紹

  Java中File類以抽象的方式代表文件名和目錄路徑名。該類主要用於文件和目錄的創建、文件的查找和文件的刪除等。即可以通過new File(String string)去操作文件或者目錄。

2. File類的構造方法

構造方法 說明
File(String child, File parent) 通過給定的父路徑名和子路徑名字符串創建一個新的File實例
File(String pathname) 通過將給定路徑名字符串轉換成路徑名來創建一個新 File 實例
File(String parent, String child) 根據 parent 路徑名字符串和 child 路徑名字符串創建一個新 File 實例
File(URI uri) 通過將給定的 file: URI 轉換成一個路徑名來創建一個新的 File 實例

前三種可以說是最常用的構造方法,用來讀取本地文件。最後一種通過生成一個URI對象實例的形式可以用來解析網絡資源。屬於HTTP協議中的的內容這裏不是講解的重點,所以不再贅述。
   URI:統一資源標識符
   URL:統一資源定位符

	// 假設桌面有個文件叫a.txt
	String dirName = "C:\\Users\\User\\Desktop";
	String fileName = "a.txt";
	String path = dirName+"\\"+fileName;
	File file1 = new File(path);
	File file2 = new File(dirName,fileName);
	File file3 = new Flei(new File(firName),fileName);

3.File類常用方法

序號 方法名 返回值類型 方法說明
1 getName() String 返回由此路徑名錶示的文件或目錄的名稱
2 getParent() String 返回此路徑名的父路徑名的路徑名字符串,如果此路徑名沒有指定父目錄,則返回 null
3 getParentFile() File 返回此路徑名的父路徑名的抽象路徑名,如果此路徑名沒有指定父目錄,則返回 null
4 getPath() String 將此路徑名轉換爲一個路徑名字符串
5 isAbsolute() boolean 測試此路徑名是否爲絕對路徑
6 getAbsolutePath() String 返回路徑名的絕對路徑名字符串
7 canRead() boolean 測試該文件是否可讀
8 canWrite() boolean 測試該文件是否可寫
9 exists() boolean 測試文件或目錄是否存在
10 isDirectory() boolean 測試該路徑名錶示的文件是否是目錄
11 isFile() boolean 測試此路徑名錶示的文件是否是一個標準文件
12 lastModified() long 返回此路徑名錶示的文件最後一次被修改的時間(毫秒數)
13 length() long 返回由此路徑名錶示的文件的長度
14 createNewFile() boolean 當且僅當不存在具有此路徑名指定的名稱的文件時,原地創建由此路徑名指定的一個新的空文件(可能會拋出IOException)
15 delete() boolean 刪除此路徑名錶示的文件或目錄(在文件系統中直接刪除,不經過回收站)
16 deleteOnExit() void 在虛擬機終止時,請求刪除此路徑名錶示的文件或目錄(在文件系統中直接刪除,不經過回收站)
17 list() String[] 返回由此路徑名所表示的目錄中的文件和目錄的名稱所組成字符串數組
18 list(FilenameFilter filter) String[] 返回由包含在目錄中的文件和目錄的名稱所組成的字符串數組,這一目錄是通過滿足指定過濾器的路徑名來表示的
19 listFiles() File[] 返回一個路徑名數組,這些路徑名錶示此抽象路徑名所表示目錄中的文件
20 listFiles(FileFilter filter) File[] 返回表示此路徑名所表示目錄中的文件和目錄的路徑名數組,這些路徑名滿足特定過濾器
21 mkdir() boolean 創建此路徑名指定的目錄,如果父路徑不存在則創建失敗
22 mkdirs() boolean 創建此路徑名指定的目錄,如果父目錄不存在則會先創建父目錄再創建子目錄
23 renameTo(File dest) boolean 重命名此路徑名錶示的文件
24 setLastModified(long time) boolean 設置由此路徑名所指定的文件或目錄的最後一次修改時間(參數爲毫秒數)
25 setReadOnly() boolean 設置只讀
26 createTempFile(String prefix, String suffix, File directory) File 在指定目錄中創建一個新的空文件,使用給定的前綴和後綴字符串生成其名稱
27 createTempFile(String prefix, String suffix) File 在默認臨時文件目錄中創建一個空文件,使用給定前綴和後綴生成其名稱
28 compareTo(File pathname) int 按字母順序比較兩個路徑名
29 compareTo(Object o) int 按字母順序比較路徑名與給定對象
30 equals(Object o) boolean 測試此路徑名與給定對象是否相等
31 toString() String 返回此路徑名的路徑名字符串

  看完這些方法,我們知道File類可以實現文件/目錄的創建與刪除、文件信息的獲取(比如:文件名,文件大小等等)、設置文件屬性(只讀)等,不支持讀寫文件,因爲File類不屬於流。在進行這些操作之前特別是涉及到文件的讀寫,筆者建議先調用exists()方法,以避免出現錯誤。下面看一個小demo:讀取桌面上名爲a.txt的文件信息。

public class demo {
    public static void main(String[] args) {
        File file = new File("C:\\Users\\woodenYi\\Desktop\\a.txt");
        System.out.println("文件" + (file.exists() ? "存在" : "不存在") + ",文件名:" + file.getName());
        System.out.println("文件" + (file.exists() ? "存在" : "不存在") + ",最後修改時間:" + file.lastModified() + "(" + new Date(file.lastModified()) + ")");
        System.out.println("文件" + (file.exists() ? "存在" : "不存在") + ",文件大小:" + file.length());
        System.out.println("文件" + (file.exists() ? "存在" : "不存在") + ",文件刪除" + (file.delete() ? "成功" : "失敗"));
        System.out.println("---------------------------------------------------------------------------------------------");
    }
}

執行結果:

  • 當文件不存在時:

文件不存在,文件名:a.txt
文件不存在,最後修改時間:0(Thu Jan 01 08:00:00 CST 1970)
文件不存在,文件大小:0
文件不存在,文件刪除失敗


  • 當文件存在時:

文件存在,文件名:a.txt
文件存在,最後修改時間:1585471143427(Sun Mar 29 16:39:03 CST 2020)
文件存在,文件大小:276
文件存在,文件刪除成功

  這裏可以很明顯的看到當文件不存在時,文件的最後修改時間是計算機時間的起始時間。但是發現可以解析到文件名,這是因爲文件名獲取是直接通過路徑來解析的。這是在我們知道自己的OS使用哪類字符來進行路徑分割的情況下。如果你不知道你的OS使用的是哪類分割符,那也沒關係,Java提供了兩類四個常量來獲取OS的分隔符。這部分可以參看File類的源碼,裏面的實現很簡單,並且也不是我們的討論重點,所以不再贅述。

三、IO流之字節流

1.什麼是字節流

  在數據傳輸過程中,傳輸數據的最基本單位是字節的流。

2.字節流的分類

  按照數據傳輸的方式分爲輸入流和輸出流。其基類分別是:InputStream和OutputStream,即,輸入流繼承自InputStream,輸出流繼承自OutputStream。

3.字節輸入流(InputStream)

字節輸入流的繼承關係圖:

繼承
繼承
繼承
繼承
繼承
繼承
繼承
繼承
繼承
繼承
FileInputStream
InputStream
BufferedInputStream
FilterInputStream
DataInputStreamr
PushbackInputStream
ObjectInputStream
PipedInputStream
SequenceInputStream
StringBufferInputStream
ByteArrayInputStream

下面介紹下幾個常用的字節輸入流:

3.1.InputStream類

3.1.1.定義以及常用方法

  InputStream是字節輸入流的抽象基類 ,InputStream作爲基類,給它的基類定義了幾個通用的函數:

  • read(byte[] b):從流中讀取b.length()個字節的數據存儲到b中,返回結果是讀取的字節個數(如果返回-1說明到了結尾,沒有了數據);
  • read(byte[] b, int off, int len):從流中從off的位置開始讀取len個字節的數據存儲到b中,返回結果是實際讀取到的字節個數(如果返回-1說明到了結尾,沒有了數據);
  • close():關閉流,釋放資源;

3.1.2.用法

  InputStream是抽象基類,所以它不可以創建對象,但它可以用來“接口化編程”,因爲大部分子類的函數在基類中都有定義,所以利用基類來調用函數。

InputStream inputStream = new FileInputStream(new File("xxx/xxx.txt"));
System.out.println(inputStream.read(new byte[1024]););

3.2. FileInputStream類

3.2.1.定義以及常用方法

  FileInputStream主要用來操作文件輸入流,它除了可以使用基類定義的函數外,它還實現了基類的read()函數:

  • read():從流中讀取1個字節的數據,返回結果是一個int,(如果編碼是以一個字節一個字符的,可以嘗試轉成char,用來查看數據);

3.2.2.用法

  FileInputStream是用來讀文件數據的流,所以它需要一個文件對象用來實例化,這個文件可以是一個File對象,也可以是文件名路徑字符串。如果文件不存在會拋出FileNotFoundException。
  這裏就舉個例子看下FileInputStream的用法,給定一個文件a.txt,文件內容是:“我是測試文件,我希望被FileInputStream讀取(回車)讀到Java了(回車回車)”將文件中的內容打印在IDE的控制檯中,並統計字節數。

public class demo {
    public static void main(String[] args) {
        // 創建文件對象
        File file = new File("C:\\Users\\woodenYi\\Desktop\\a.txt");
        // 定義變量count,用於統計文件字節長度
        int count = 0;
        // 定義一個字節數組bytes,表示每次讀取的字節數
        byte[] bytes = new byte[4];
        // 定義變量length,記錄按照bytes讀取的長度
        int length = 0;
        // 定義變量string,用來存儲文件內容
        String string = "";
        // 如果文件不存在 則拋出RuntimeException()
        if (!file.exists()){
            throw new RuntimeException("文件不存在!");
        }
        try (InputStream inputStream = new FileInputStream(file)){
            // read() 表示每次只讀取一個字節
            while (inputStream.read()!=-1){
                count ++;
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        try(InputStream inputStream = new FileInputStream(file)){
            // 每次讀取按照bytes。length個字節去讀
            while ((length = inputStream.read(bytes))!=-1){
                string += new String(bytes,0,length);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

        System.out.println("文件"+file.getName()+"共有"+file.length()+"字節");
        System.out.println("文件"+file.getName()+"共有"+count+"字節");
        System.out.println("==================================================");
        System.out.println("文件"+file.getName()+"內容:\n"+string);
        System.out.println("==================================================");
    }
}

執行結果:

文件a.txt共有73字節
文件a.txt共有73字節
==================================================
文件a.txt內容:
我�����試文�����我希�����FileInputStream讀���
讀���Java��
 
 
 ==================================================

從代碼和運行結果可以看出①read()方法讀到文件末尾會返回-1;②read()方法調用次數=文件大小(字節數)/每次讀取的字節數;③如果字節數設置不合理會導致讀出的文件內容產生亂碼。
  在使用read(byte[] byte)方法是建議最小讀取字節數爲1024字節或者1024字節的整數倍,這樣纔能有效避免出現亂碼現象。在上面的代碼中將最小讀寫單元改成1024字節後運行結果:

文件a.txt共有73字節
文件a.txt共有73字節
==================================================
文件a.txt內容:
我是測試文件,我希望被FileInputStream讀取
讀到Java了
  
   
==================================================

3.3.BufferedInputStream類

3.3.1.定義及常用方法

  BufferedInputStream帶有緩衝的意思,普通的流是從硬盤裏面讀,而帶有緩衝區之後,BufferedInputStream先將數據封裝到內存中,再從內存中操作數據,因此它的效率要比普通的流(eg:FileInputStream)的要高,所以又被稱爲緩衝流或高級流。需要注意的是BufferedInputStream並不是InputStream的直接子類,而是FilterInputStream的子類,這一點可以從上面的繼承圖看出來。它除了可以使用基類定義的函數外,它還實現了基類的read()函數(無參的):

  • read():從流中讀取1個字節的數據,返回結果是一個int(如果編碼是以一個字節一個字符的,則可以嘗試轉成char,用來查看數據);

3.3.2.用法

  BufferedInputStream作爲高級流,它可以嵌套在其他低級流上使用。這裏同樣用FileInputStream裏的例子做演示:

public class demo {
    public static void main(String[] args) {
        // 創建文件對象
        File file = new File("C:\\Users\\woodenYi\\Desktop\\a.txt");
        // 定義變量count,用於統計文件字節長度
        int count = 0;
        // 定義一個字節數組bytes,表示每次讀取的字節數
        byte[] bytes = new byte[4];
        // 定義變量length,記錄按照bytes讀取的長度
        int length = 0;
        // 定義變量string,用來存儲文件內容
        String string = "";
        // 如果文件不存在 則拋出RuntimeException()
        if (!file.exists()){
            throw new RuntimeException("文件不存在!");
        }
        // 將BufferedInputStream嵌套在FileInputStream上
        try (InputStream inputStream = new BufferedInputStream(new FileInputStream(file))){
            // read() 表示每次只讀取一個字節
            while (inputStream.read()!=-1){
                count ++;
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        try(InputStream inputStream = new BufferedInputStream(new FileInputStream(file))){
            // 每次讀取按照bytes。length個字節去讀
            while ((length = inputStream.read(bytes))!=-1){
                string += new String(bytes,0,length);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

        System.out.println("文件"+file.getName()+"共有"+file.length()+"字節");
        System.out.println("文件"+file.getName()+"共有"+count+"字節");
        System.out.println("==================================================");
        System.out.println("文件"+file.getName()+"內容:\n"+string);
        System.out.println("==================================================");
    }
}

執行結果:

文件a.txt共有73字節
文件a.txt共有73字節
==================================================
文件a.txt內容:
我�����試文�����我希�����FileInputStream讀���
讀���Java��
 
 
 ==================================================

從代碼和運行結果可以看出BufferedInputStream和FileInputStream幾乎相同,不同的是在操作文件的效率上,這裏不做演示 ,感興趣可以自己試試用BufferedInputStream和FileInputStream去操作同一個大文件,感受下執行效率的差異。這裏同樣將最小讀寫單元改成1024字節後,看下運行結果:

文件a.txt共有73字節
文件a.txt共有73字節
==================================================
文件a.txt內容:
我是測試文件,我希望被FileInputStream讀取
讀到Java了
  
   
==================================================

4. 字節輸出流(OutputStream)

字節輸出流的繼承關係圖:

繼承
繼承
繼承
繼承
繼承
繼承
繼承
繼承
FileOutputStream
OutputStream
BufferedOutputStream
FilterOutputStream
DataOutputStream
PrintStream
ObjectOutputStream
PipedOutputStream
ByteArrayOutputStream

這一部分建議大家對比字節輸入流去看,下面介紹下幾個常用的字節輸入流:

4.1.OutputStream類

4.1.1.定義與常用方法

  OutputStream是字節輸出流的基類, OutputStream作爲基類,給它的基類定義了幾個通用的函數:

  • write(byte[] b):將b.length()個字節數據寫到輸出流中;
  • write(byte[] b,int off,int len):從b的off位置開始,獲取len個字節數據,寫到輸出流中;
  • flush():刷新輸出流,把數據馬上寫到輸出流中;
  • close():關閉流,釋放系統資源;

4.1.2.用法

  OutputStream和InputStream一樣都是抽象基類,在用法上也一樣。

OutputStream outputStream = new FileOutputStream(file);
outputStream.write(bytes);
outputStream.close();

4.2.FileOutputStream類

4.2.1.定義與常用方法

  FileOutputStream是用於寫文件的輸出流,它除了可以使用基類定義的函數外,還實現了OutputStream的抽象函數write(int b):

  • write(int b):將b轉成一個字節數據,寫到輸出流中;

4.2.2.用法

  FileOutputStream需要一個文件作爲實例化參數,這個文件可以是File對象,也可以是文件路徑字符串(如果文件不存在,那麼將自動創建)。FileOutputStream實例化時可以給第二個參數,第二個參數表示是否使用追加寫入,爲true時代表在原有文件內容後面追加寫入數據,默認爲false。
  這裏還是用一個例子去了解一下FileOutputStream,需求如下:將a.txt中的內容讀取出來寫入b.txt中,讀取b.txt。再將“我是FileOutputStream寫入內容”寫入b.txt中,此時使用追加寫入和覆蓋寫入,再讀取出b.txt的內容。

public class demo1 {
    public static void main(String[] args) {
        String source = "C:\\Users\\woodenYi\\Desktop\\a.txt";
        String target = "C:\\Users\\woodenYi\\Desktop\\b.txt";
        String text = "我是FileOutputStream寫入內容";
        // 調用追加寫入方法
        Thread thread1 = new Thread(()->{
            additionalWrite(source,target,text);
        });
        // 調用覆蓋寫入函數
        Thread thread2 = new Thread(()->{
            overwrite(source,target,text);
        });
        thread1.start();
        // 這裏做一個阻塞操作 是打印的內容更直觀
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread2.start();
    }

    /**
     * 追加寫入text
     * @param source 源文件路徑
     * @param target 目標文件路徑
     * @param text 寫入內容
     */
    public static void additionalWrite(String source,String target,String text){
        // 調用內容轉移方法
        contentTransfer(source,target);
        // 創建輸入輸出流
        InputStream in = null;
        OutputStream out = null;
        // 將text寫入 target中
        try {
            out = new FileOutputStream(target,true);
            out.write(text.getBytes());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        // 創建變量bytes 存儲讀到的內容
        byte[] bytes = new byte[1024];
        int length = 0;
        StringBuilder string= new StringBuilder("");
        // 讀取 target的內容 這裏因爲先調用了OutputStream所以不需要判斷文件是否存在
        try {
            in = new FileInputStream(target);
            while ((length =in.read(bytes))!=-1){
                System.out.println(length);
                string.append(new String(bytes, 0, length));
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        System.out.println("============追加寫入後["+target+"]中的內容============");
        System.out.println(string.toString());
        System.out.println("============追加寫入輸出完畢============\n");
        // 爲保證兩次測試不受影響,執行刪除操作
        new File(target).delete();
    }

    /**
     * 覆蓋寫入text
     * @param source 源文件路徑
     * @param target 目標文件路徑
     * @param text 寫入內容
     */
    public static void overwrite(String source,String target,String text){
        contentTransfer(source,target);
        // 創建輸入輸出流
        InputStream in = null;
        OutputStream out = null;
        // 將text寫入 target中
        try {
            // 其實默認值就是false  這裏爲了區別
            out = new FileOutputStream(target,false);
            out.write(text.getBytes());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        // 創建變量bytes 存儲讀到的內容
        byte[] bytes = new byte[1024];
        int length = 0;
        StringBuilder string= new StringBuilder("");
        // 讀取 target的內容 這裏因爲先調用了OutputStream所以不需要判斷文件是否存在
        try {
            in = new FileInputStream(target);
            while ((length =in.read(bytes))!=-1){
                string.append(new String(bytes, 0, length));
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        System.out.println("============覆蓋寫入後["+target+"]中的內容============");
        System.out.println(string.toString());
        System.out.println("============覆蓋寫入輸出完畢============\n");
        // 爲保證兩次測試不受影響,執行刪除操作
        new File(target).delete();
    }

    /**
     * 將source中的內容寫入target中
     * @param source 源文件路徑
     * @param target 目標文件路徑
     */
    public static void contentTransfer(String source,String target){
        // 創建File對象
        File file = new File(source);
        // 判斷file是否存在,若源文件不存在直接throw RuntimeException
        if(!file.exists()){
            throw new RuntimeException("源文件不存在!");
        }
        // 創建變量bytes 存儲讀到的內容
        // 由於我這裏的文件很小  所以直接使用file.length()作爲字節數  否則在寫入時會寫入很多空格 實際應用時字節數視實際情況而定
        byte[] bytes = new byte[(int) file.length()];
        int length = 0;
        StringBuilder string= new StringBuilder("");
        // 創建輸入流和輸出流
        InputStream in = null;
        InputStream newIn = null;
        OutputStream out = null;
        try {
            in = new FileInputStream(file);
            out = new FileOutputStream(target);
            newIn = new FileInputStream(target);
            // 讀取source並寫入target
            while (in.read(bytes) !=-1){
                out.write(bytes);
            }
            // 讀取target
            while ((length =newIn.read(bytes))!=-1){
                string.append(new String(bytes, 0, length));
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                if (in != null) {
                    in.close();
                }
                if (newIn != null) {
                    newIn.close();
                }
                if (out != null) {
                    out.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        System.out.println("============從["+source+"]中轉移到["+target+"]後的內容=========================");
        System.out.println(string.toString());
        System.out.println("===============轉移內容讀取完畢======================\n");
    }
}

執行結果:

============[C:\Users\woodenYi\Desktop\a.txt]中轉移到[C:\Users\woodenYi\Desktop\b.txt]後的內容=========================
我是測試文件,我希望被FileInputStream讀取

讀到Java了


===============轉移內容讀取完畢======================

109
============追加寫入後[C:\Users\woodenYi\Desktop\b.txt]中的內容============
我是測試文件,我希望被FileInputStream讀取

讀到Java了

我是FileOutputStream寫入內容
============追加寫入輸出完畢============

============[C:\Users\woodenYi\Desktop\a.txt]中轉移到[C:\Users\woodenYi\Desktop\b.txt]後的內容=========================
我是測試文件,我希望被FileInputStream讀取

讀到Java了


===============轉移內容讀取完畢======================

============覆蓋寫入後[C:\Users\woodenYi\Desktop\b.txt]中的內容============
我是FileOutputStream寫入內容
============覆蓋寫入輸出完畢============

4.3.BufferedOutputStream 類

4.3.1.定義與常用方法

  BufferedOutputStream像上面那個BufferedInputStream一樣,都可以提高效率。它除了可以使用基類定義的函數外,它還實現了OutputStream的抽象函數write(int b):

  • write(int b):將b轉成一個字節數據,寫到輸出流中

4.3.2.用法

  BufferedOutputStream需要一個輸出流作爲實例化參數,具體用法與上面的BufferedInputStream大同小異。

四、IO流之字符流

1.什麼是字符流

  在數據傳輸過程中,傳輸數據的最基本單位是字符的流。

2.字符流的分類

  按照數據傳輸的方式分爲輸入流和輸出流。其基類分別是:Reader和Writer,即,輸入流繼承自Reader,輸出流繼承自Writer。

3.字符輸入流(Reader)

字符輸入流的繼承關係圖:

繼承
繼承
繼承
繼承
繼承
繼承
繼承
繼承
BufferedReader
Reader
FileReader
InputStreamReader
StringReader
PipedReader
ByteArrayReader
PushbackReader
FilterReader

3.1.Reader類

3.1.1.定義及常用方法

  Reader是字符輸入流的抽象基類 ,它定義了以下幾個函數:

  • read() :讀取單個字符,返回結果是一個int,到達流的末尾時,返回-1
  • read(char[] cbuf):讀取cbuf.length()個字符到cbuf中,返回結果是讀取的字符數,到達流的末尾時,返回-1
  • close() :關閉流,釋放佔用的系統資源

3.1.2.用法

  Reader不能實例化,用於“接口化編程”

Reader reader = new FileReader("a.txt");
reader.read();

3.2.InputStreamReader類

3.2.1.定義及常用方法

  InputStreamReader 可以把InputStream中的字節數據流根據字符編碼方式轉成字符數據流。它除了可以使用基類定義的函數,它自己還實現了以下函數:

  • read(char[] cbuf, int offset, int length) :從offset位置開始,讀取length個字符到cbuf中,返回結果是實際讀取的字符數,到達流的末尾時,返回-1

3.2.2.用法

  InputStreamReader需要一個字節輸入流對象作爲實例化參數。還可以指定第二個參數,第二個參數是字符編碼。

InputStreamReader isr1 = new InputStreamReader(new FileInputStream("a.txt"));
isr1.read();
InputStreamReader isr2 = new InputStreamReader(new FileInputStream("a.txt"),"utf8");
isr2.read();

3.3.FileReader類

3.3.1.定義及常用方法

  FileReader它是InputStreamReader的子類,可以把FileInputStream中的字節數據轉成根據字符編碼方式轉成字符數據流

3.3.2.用法

  FileReader 需要一個文件對象作爲實例化參數,可以是File類對象,也可以是文件的路徑字符串

FileReader reader = new FileReader("a.txt");
reader.read();

3.4.BufferedReader類

3.4.1.定義及常用方法

  BufferedReader可以把字符輸入流進行封裝,將數據進行緩衝,提高讀取效率。是一個高級流,它除了可以使用基類定義的函數,它自己還實現了以下函數:

  • read(char[] cbuf, int offset, int length) :從offset位置開始,讀取length個字符到cbuf中,返回結果是實際讀取的字符數,到達流的末尾時,返回-1
  • readLine() :按行讀取,以行結束符作爲末尾,返回結果是讀取的字符串。如果已到達流末尾,則返回 null

3.4.2.用法

  BufferReader需要一個字符輸入流對象作爲實例化參數,需要和其他流嵌套配合使用。

BufferedReader bfr = new BufferedReader(new FileReader("a.txt"));
bfr.readLine();

4.字符輸出流(Writer)

字符輸出流的繼承關係圖:

繼承
繼承
繼承
繼承
繼承
繼承
繼承
繼承
BufferedWriter
Writer
FileWriter
OutputStreamWriter
StringWriter
PipedWriter
CharArrayReader
PrinterWriter
FilterWriter

4.1.Writer類

4.1.1.定義及常用方法

  Writer是字符輸出流的抽象基類, ,它定義了以下幾個函數:

  • write(char[] cbuf) :往輸出流寫入一個字符數組
  • write(int c) :往輸出流寫入一個字符
  • write(String str) :往輸出流寫入一串字符串
  • write(String str, int off, int len) :往輸出流寫入字符串的一部分
  • close() :關閉流,釋放資源(抽象方法)
  • flush():刷新輸出流,把數據馬上寫到輸出流中(抽象方法)

4.2.InputStreamWriter類

4.2.1.定義及常用方法

  OutputStreamWriter可以使我們直接往流中寫字符串數據,它裏面會幫我們根據字符編碼方式來把字符數據轉成字節數據再寫給輸出流。

4.3.FileWriter類

4.3.1.定義及常用方法

  FileWriter與OutputStreamWriter功能類似,我們可以直接往流中寫字符串數據,FileWriter內部會根據字符編碼方式來把字符數據轉成字節數據再寫給輸出流

4.4.BufferedWriter類

4.4.1.定義及常用方法

  BufferedWriter利用了緩衝區來提高寫的效率。它還多出了一個函數:

  • newLine() :寫入一個換行符

4.5.字符輸出流用法

  字符輸出流的用法與字符輸入流的用法幾乎相同,這裏不再去展示演示代碼了。可以對比字符輸入流去學習。

五、IO流之Scanner類

  Scanner這個類都不陌生,初學Java時一定都用過。他的功能就好像是C++裏的cin用來獲取鍵盤輸入的字符。至於爲什麼把Scanner放在IO流中是因爲Scanner本就是一個輸入流,雖然Scanner類不在java.io包中。

1.Scanner類簡介

  Java 5添加了java.util.Scanner類,這是一個用於掃描輸入文本的新的實用程序。它是以前的StringTokenizer和Matcher類之間的某種結合。由於任何數據都必須通過同一模式的捕獲組檢索或通過使用一個索引來檢索文本的各個部分。於是可以結合使用正則表達式和從輸入流中檢索特定類型數據項的方法。這樣,除了能使用正則表達式之外,Scanner類還可以任意地對字符串和基本類型(如int和double)的數據進行分析。藉助於Scanner,可以針對任何要處理的文本內容編寫自定義的語法分析器。

2.Scanner構造器及常用方法

1.構造器

  Scanner的構造器支持多種方式,可以從字符串(Readable)、輸入流、文件等等來直接構建Scanner對象。

2.常用方法

  • delimiter() :返回此 Scanner 當前正在用於匹配分隔符的 Pattern。
  • hasNext() :判斷掃描器中當前掃描位置後是否還存在下一個標記(字符,文本等等)
  • hasNextLine() :如果在此掃描器的輸入中存在另一行,則返回 true
  • next() :查找並返回來自此掃描器的下一個完整標記
  • nextLine() :此掃描器執行當前行,並返回信息

3.Scanner的分割符

  Scanner默認使用空格作爲分割符來分隔文本,但允許你指定新的分割符。

Scanner s = new Scanner("123 asdf。。。hjksdfojk----634345664,erg"); 
// 設置新的分割符 使用空格或逗號或點號作爲分隔符
s.useDelimiter(" |,|\\."); 

4.Scanner關閉問題

Scanner sc = new Scanner(System.in);
Scanner sc2 = new Scanner(System.in);
sc.close();

  這時候你會發現sc2這個實例也沒有辦法使用了,這是因爲sc執行close操作是把System.in也關閉了,所以導致sc2不能用。建議在Scanner操作結束後統一調用close()方法來關閉流釋放系統資源。

六、字節流與字符流轉換

  雖然Java支持字節流和字符流,但有時需要在字節流和字符流兩者之間轉換。InputStreamReader和OutputStreamWriter,這兩個爲類是字節流和字符流之間相互轉換的類。

// 獲取標準輸入流
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));

// 獲取標準輸出流
Writer out = new BufferedWriter(new OutputStreamWriter(System.out));

 &#8195基於這兩個操作,來實現從控制檯獲取輸入的文字寫入文件以及在獲取文件內容在控制檯輸出。

  • 從控制檯獲取輸入的文字寫入文件
public class demo4 {
    public static void main(String[] args) {
        // 創建字符流對象
        BufferedWriter bw = null;
        BufferedReader br = null;

        try {
            // 實例化字符流對象 通過 InputStreamReader 將字節輸入流轉化成字符輸入流
            br = new BufferedReader(new InputStreamReader(System.in));
            bw = new BufferedWriter(new FileWriter("C:\\Users\\woodenYi\\Desktop\\c.txt"));
            // 定義讀取數據的行
            String line = null;
            // 讀取數據
            System.out.println("請輸入要寫入的信息:");
            while ((line = br.readLine()) != null) {
                // 如果輸入的是"exit"就退出
                if("exit".equals(line)){
                    break;
                }
                // 將數據寫入文件
                bw.write(line);
                // 寫入新的一行
                bw.newLine();
                // 刷新數據緩衝
                bw.flush();
            }
        } catch (Exception e){
            e.printStackTrace();
        } finally {
            // 釋放資源
            try {
                if(bw != null) {
                    bw.close();
                }
                if (br != null) {
                    br.close();
                }
            } catch (IOException e){
                e.printStackTrace();
            }
        }
        System.out.println("寫入完畢!");
    }
}

執行結果

請輸入要寫入的信息:
124365787798
dsafghjjk和身體恢復功能費
大富豪都是
exit
寫入完畢!
  • 獲取文件內容在控制檯輸出
public class Demo5 {

    public static void main(String[] args) {

        // 定義字節流的對象
        BufferedReader br = null;
        BufferedWriter bw = null;
        try {
            // 實例化字節輸出流 使用
            bw = new BufferedWriter(new OutputStreamWriter(System.out));
            br = new BufferedReader(new FileReader("C:\\Users\\woodenYi\\Desktop\\c.txt"));
            // 定義讀取行的字符串
            String line = null;
            // 讀取數據
            while ((line = br.readLine()) != null) {
                // 輸出到控制檯
                bw.write(line);
                // 新的一行 
                bw.newLine();
                // 刷新緩衝
                bw.flush();
            }
        } catch (Exception e){
            e.printStackTrace();
        } finally {
            // 釋放資源
            try {
                if(bw != null) {
                    bw.close();
                }
                if (br != null) {
                    br.close();
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
}

執行結果

124365787798
dsafghjjk和身體恢復功能費
大富豪都是

七、總結

  • 流是用來表示數據的無結構化傳遞的抽象概念
  • 在使用File進行文件操作是要先判斷文件是否存在
  • 在使用字節流讀寫文件時要選擇合適的字節數來進行操作,一般選擇1024字節的整數倍
  • 高級流要嵌套在低級流上使用
  • 不管是字節流還是字符流,它們的抽象基類只適用於“接口化編程”,不建議進行實例化
  • 流在使用後一定要關閉,否則會佔用系統資源
  • 字節流和字符流可以通過InputStreamReader和OutputStreamWriter進行轉換
  • Scanner不僅可以進行鍵盤讀取還可以進行文件操作(雖然有現成的字符流和字節流)

寫在後面

  說實話,這篇博文從開始寫到寫完前後花了四天時間。究其原因還是自己對這部分內容的一些概念有些模糊不清,中間需要去翻看API文檔、閱讀底層源碼。其實一度也想過放棄,不寫這篇博文。但最後還是堅持下來了。人吧,不能有放棄的念頭,更不能因爲出現了放棄的念頭就開始鬆懈,不然就只能習慣放棄了。這對於我們,特別是技術性崗位實在是太可怕了。一旦習慣放棄,那就要習慣被優化了。希望與諸君共勉。
  這裏我要感謝一下我的徒弟,雖然平時很皮,而且還是個鐵憨憨,但是我在寫這篇博文的時候她陪我熬了三四夜。雖然最後呈現的博文與我自己預想的還有些差距,但是也比較滿意了。希望對走在奮鬥路上的你有所幫助!
  我們下篇博文見!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章