靈魂一擊!詳解Java中的IO輸入輸出流

什麼是流?流表示任何有能力產生數據的數據源對象或者是有能力接收數據的接收端對象,它屏蔽了實際的I/O設備中處理數據的細節。html

IO流是實現輸入輸出的基礎,它能夠很方便地實現數據的輸入輸出操做,即讀寫操做。java

本片要點

  • 介紹流的定義和基本分類。
  • 介紹文件字符流、字節流、轉換流、合併流、打印流等使用。
  • 介紹序列化的意義。
  • 介紹兩種自定義序列化方式。

基本分類

  • 根據方向輸入流:數據從外部流向程序,例如從文件中讀取數據輸出流:數據從程序流向外部,例如向文件中寫數據
  • 根據形式字符流:字符類文件,【如 txt、 java、 html】,操做16位的字符。字節流:【圖片、視頻、音頻】 ,操做8位的字節。
  • 根據功能節點流:直接從/向數據源【如磁盤、網絡】進行數據讀寫處理流:封裝其餘的流,來提供加強流的功能。
靈魂一擊!詳解Java中的IO輸入輸出流

 

  • 上面四大基本流都是抽象類,都不能直接建立實例對象。
  • 數據的來源/目的地:磁盤、網絡、內存、外部設備。

發展史

  • java1.0版本中,I/O庫中與輸入有關的全部類都將繼承InputStream,與輸出有關的全部類繼承OutputStream,用以操做二進制數據。
  • java1.1版本對I/O庫進行了修改:在原先的庫中新增了新類,如ObjectInputStreamObjectOutputStream。增長了Reader和Writer,提供了兼容Unicode與面向字符的I/O功能。在Reader和Writer類層次結構中,提供了使字符與字節相互轉化的類,OutputStreamWriterInputStreamReader
  • 兩個不一樣的繼承層次結構擁有類似的行爲,它們都提供了讀(read)和寫(write)的方法,針對不一樣的狀況,提供的方法也是相似的。
  • java1.4版本的java.nio.*包中引入新的I/O類庫,這部分之後再作學習。

文件字符流

  • 文件字符輸出流 FileWriter自帶緩衝區,數據先寫到到緩衝區上,而後從緩衝區寫入文件。
  • 文件字符輸入流 FileReader:沒有緩衝區,能夠單個字符的讀取,也能夠自定義數組緩衝區。

輸出的基本結構

在實際應用中,異常處理的方式都須要按照下面的結構進行,本篇爲了節約篇幅,以後都將採用向上拋出的方式處理異常。linux

//將流對象放在try以外聲明,並附爲null,保證編譯,能夠調用close
    FileWriter writer = null;
    try {
        //將流對象放在裏面初始化
        writer = new FileWriter("D:\\b.txt");
        writer.write("abc");
        
        //防止關流失敗,沒有自動沖刷,致使數據丟失
        writer.flush();
        
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        //判斷writer對象是否成功初始化
        if(writer!=null) {
            //關流,不管成功與否
            try {
                writer.close();
            } catch (IOException e) {
                e.printStackTrace();
            }finally {
                //不管關流成功與否,都是有意義的:標爲垃圾對象,強制回收
                writer = null;
            }
        }
    }
  • 並不會直接將數據寫入文件中,而是先寫入緩衝區,待緩衝區滿了以後纔將緩衝區的數據寫入文件。
  • 假設數據寫入緩衝區時且緩衝區還沒滿,數據還沒可以寫入文件時,程序就已經結束,會致使數據慘死緩衝區,這時須要手動沖刷緩衝區,將緩衝區內的數據沖刷進文件中。writer.flush();
  • 數據寫入完畢,釋放文件以容許別的流來操做該文件。關閉流能夠調用close()方法,值得注意的是,在close執行以前,流會自動進行一次flush的操做以免數據還殘存在緩衝區中,但這並不意味着flush操做是多餘的。

流中的異常處理

  • 不管流操做成功與否,關流操做都須要進行,因此須要將關流操做放到finally代碼塊中
  • 爲了讓流對象在finally中依然可以使用,因此須要將流對象放在try以外聲明而且賦值爲null,而後在try以內進行實際的初始化過程。
  • 在關流以前要判斷流對象是否初始化成功,實際就是判斷流對象是否爲nullwriter!=null時才執行關流操做。
  • 關流可能會失敗,此時流依然會佔用文件,因此須要將流對象置爲null,標記爲垃圾對象進行強制回收以釋放文件。
  • 若是流有緩衝區,爲了防止關流失敗致使沒有進行自動沖刷,因此須要手動沖刷一次,以防止有數據死在緩衝區而產生數據的丟失。

異常處理新方式

JDK1.7提出了對流進行異常處理的新方式,任何AutoClosable類型的對象均可以用於try-with-resourses語法,實現自動關閉。面試

要求處理的對象的聲明過程必須在try後跟的()中,在try代碼塊以外。設計模式

try(FileWriter writer = new FileWriter("D:\\c.txt")){
    writer.write("abc");
}catch (IOException e){
    e.printStackTrace();
}

讀取的基本結構

public static void main(String[] args) throws IOException {
        FileReader reader = new FileReader("D:\\b.txt");
        //定義數組做爲緩衝區
        char[] cs = new char[5];
        //定義一個變量記錄每次讀取的字符
        int hasRead;
        //讀取到末尾爲-1
        while ((hasRead = reader.read(cs)) != -1) {
            System.out.println(new String(cs, 0, hasRead));
        }
        reader.close();
    }
  • read方法能夠傳入字符數組,每次讀取一個字符數組的長度。
  • 定義變量m記錄讀取的字符,以達到末尾爲終止條件。m!=-1時,終止循環。
  • 讀取結束,執行關流操做。

運用輸入與輸出完成複製效果

運用文件字符輸入與輸出的小小案例:數組

public static void copyFile(FileReader reader, FileWriter writer) throws IOException {
    //利用字符數組做爲緩衝區
    char[] cs = new char[5];
    //定義變量記錄讀取到的字符個數
    int hasRead;
    while((hasRead = reader.read(cs)) != -1){
        //將讀取到的內容寫入新的文件中
        writer.write(cs, 0, hasRead));

    }
    reader.close();
    writer.close();
}

文件字節流

  • 文件字節輸出流 FileOutputStream 在輸出的時候沒有緩衝區,因此不須要進行flush操做。
public static void main(String[] args) throws Exception {
        FileOutputStream out = new FileOutputStream("D:\\b.txt");
        //寫入數據
        //字節輸出流沒有緩衝區
        out.write("天喬巴夏".getBytes());
        //關流是爲了釋放文件
        out.close();
    }
  • 文件字節輸入流 FileInputStream,能夠定義字節數組做爲緩衝區。
public static void main(String[] args) throws Exception{
        FileInputStream in = new FileInputStream("E:\\1myblog\\Node.png");
       //1.讀取字節
       int i;
       while((i = in.read()) ! =-1)
           System.out.println(i);
       //2.定義字節數組做爲緩衝區
       byte[] bs = new byte[10];
       //定義變量記錄每次實際讀取的字節個數
       int len;
       while((len = in.read(bs)) != -1){
           System.out.println(new String(bs, 0, len));
       }
       in.close();

    }

緩衝流

字符緩衝流

  • BufferedReader:在構建的時候須要傳入一個Reader對象,真正讀取數據依靠的是傳入的這個Reader對象BufferedReadReader對象中獲取數據提供緩衝區
public static void main(String[] args) throws IOException {
        //真正讀取文件的流是FileReader,它自己並無緩衝區
        FileReader reader = new FileReader("D:\\b.txt");
        BufferedReader br = new BufferedReader(reader);
        //讀取一行
        //String str = br.readLine();
        //System.out.println(str);

        //定義一個變量來記錄讀取的每一行的數據(回車)
        String str;
        //讀取到末尾返回null
        while((str = br.readLine())!=null){
            System.out.println(str);
        }
        //關外層流便可
        br.close();
    }
  • BufferedWriter:提供了一個更大的緩衝區,提供了一個newLine的方法用於換行,以屏蔽不一樣操做系統的差別性
public static void main(String[] args) throws Exception {
        //真正向文件中寫數據的流是FileWriter,自己具備緩衝區
        //BufferedWriter 提供了更大的緩衝區
        BufferedWriter writer = new BufferedWriter(new FileWriter("E:\\b.txt"));
        writer.write("天喬");
        //換行: Windows中換行是 \r\n linux中只有\n
        //提供newLine() 統一換行
        writer.newLine();
        writer.write("巴夏");
        writer.close();
    }

裝飾設計模式

緩衝流基於裝飾設計模式,即利用同類對象構建本類對象,在本類中進行功能的改變或者加強。網絡

例如,BufferedReader自己就是Reader對象,它接收了一個Reader對象構建自身,自身提供緩衝區其餘新增方法,經過減小磁盤讀寫次數來提升輸入和輸出的速度。ide

靈魂一擊!詳解Java中的IO輸入輸出流

 

除此以外,字節流一樣也存在緩衝流,分別是BufferedInputStreamBufferedOutputStream學習

轉換流(適配器)

利用轉換流能夠實現字符流和字節流之間的轉換this

  • OutputStreamWriter
public static void main(String[] args) throws Exception {
        //在構建轉換流時須要傳入一個OutputStream 字節流
        OutputStreamWriter ow = 
                new OutputStreamWriter(
                        new FileOutputStream("D:\\b.txt"),"utf-8");
        //給定字符--> OutputStreamWriter轉化爲字節-->以字節流形式傳入文件FileOutputStream
        //若是沒有指定編碼,默認使用當前工程的編碼
        ow.write("天喬巴夏");
        ow.close();
    }

最終與文件接觸的是字節流,意味着將傳入的字符轉換爲字節


  • InputStreamReader
public static void main(String[] args) throws IOException {
        //以字節形式FileInputStream讀取,通過轉換InputStreamReader -->字符
        //若是沒有指定編碼。使用的是默認的工程的編碼
        InputStreamReader ir = 
                new InputStreamReader(
                        new FileInputStream("D:\\b.txt"));
        char[] cs = new char[5];
        int len;
        while((len=ir.read(cs))!=-1){
            System.out.println(new String(cs,0,len));
        }
        ir.close();
    }

最初與文件接觸的是字節流,意味着將讀取的字節轉化爲字符

適配器設計模式

緩衝流基於適配器設計模式,將某個類的接口轉換另外一個用戶所但願的類的接口,讓本來因爲接口不兼容而不能在一塊兒工做的類能夠在一塊兒進行工做。

OutputStreamWriter爲例,構建該轉換流時須要傳入一個字節流,而寫入的數據最開始是由字符形式給定的,也就是說該轉換流實現了從字符向字節的轉換,讓兩個不一樣的類在一塊兒共同辦事。

靈魂一擊!詳解Java中的IO輸入輸出流

 

標準流/系統流

程序的全部輸入均可以來自於標準輸入,全部輸出均可以發送到標準輸出,全部錯誤信息均可以發送到標準錯誤

標準流分類

靈魂一擊!詳解Java中的IO輸入輸出流

 

能夠直接使用System.outSystem.err,可是在讀取System.in以前必須對其進行封裝,例如咱們以前常常會使用的讀取輸入:Scanner sc = new Scanner(System.in);實際上就封裝了System.in對象。

  • 標準流都是字節流
  • 標準流對應的不是類而是對象。
  • 標準流在使用的時候不用關閉。
/** * 從控制檯獲取一行數據 * @throws IOException readLine 可能會拋出異常 */
    public static void getLine() throws IOException {
        //獲取一行字符數據 -- BufferedReader
        //從控制檯獲取數據 -- System.in
        //System是字節流,BufferedReader在構建的時候須要傳入字符流
        //將字節流轉換爲字符流
        BufferedReader br =
                new BufferedReader(
                        new InputStreamReader(System.in));
        //接收標準輸入並轉換爲大寫
        String str = br.readLine().toUpperCase();
        //發送到標準輸出
        System.out.println(str);
    }

經過轉換流,將System.in讀取的標準輸入字節流轉化爲字符流,發送到標準輸出,打印顯示。

打印流

打印流只有輸出流沒有輸入流

  • PrintStream: 打印字節流
public static void main(String[] args) throws IOException {
        //建立PrintStream對象
        PrintStream p = new PrintStream("D:\\b.txt");
        p.write("abc".getBytes());
        p.write("def".getBytes());
        p.println("abc");
        p.println("def");
        //若是打印對象,默認調用對象身上的toString方法
        p.println(new Object());
        p.close();
    }
  • PrintWriter:打印字符流
//將System.out轉換爲PrintStream
    public static void main(String[] args) {
        //第二個參數autoFlash設置爲true,不然看不到結果
        PrintWriter p = new PrintWriter(System.out,true);
        p.println("hello,world!");
    }

合併流

  • SequenceInputStream用於將多個字節流合併爲一個字節流的流。
  • 有兩種構建方式:將多個合併的字節流放入一個Enumeration中來進行。傳入兩個InputStream對象。
  • 合併流只有輸入流沒有輸出流。

以第一種構建方式爲例,咱們以前說過,Enumeration能夠經過Vector容器的elements方法建立。

public static void main(String[] args) throws IOException {
        FileInputStream in1 = new FileInputStream("D:\\1.txt");
        FileInputStream in2 = new FileInputStream("D:\\a.txt");
        FileInputStream in3 = new FileInputStream("D:\\b.txt");
        FileInputStream in4 = new FileInputStream("D:\\m.txt");

        FileOutputStream out = new FileOutputStream("D:\\union.txt");
        //準備一個Vector存儲輸入流
        Vector<InputStream> v = new Vector<>();
        v.add(in1);
        v.add(in2);
        v.add(in3);
        v.add(in4);

        //利用Vector產生Enumeration對象
        Enumeration<InputStream> e = v.elements();
        //利用迭代器構建合併流
        SequenceInputStream s = new SequenceInputStream(e);

        //讀取
        byte[] bs = new byte[10];
        int len;
        while((len = s.read(bs))!=-1){
            out.write(bs,0,len);
        }
        out.close();
        s.close();
    }

序列化/反序列化流

  • 序列化:將對象轉化爲字節數組的過程。
  • 反序列化:將字節數組還原回對象的過程。

序列化的意義

對象序列化的目標是將對象保存在磁盤中,或容許在網絡中直接傳輸對象。對象序列化機制容許把內存中的Java對象轉換成平臺無關的二進制流,從而容許把這種二進制流持久地保存在磁盤上,經過網絡將這種二進制流傳輸到另外一個網絡節點。其餘程序一旦得到了這種流,均可以將這種二進制流恢復爲原來的Java對象。

讓某個對象支持序列化的方法很簡單,讓它實現Serializable接口便可:

public interface Serializable {
}

這個接口沒有任何的方法聲明,只是一個標記接口,代表實現該接口的類是可序列化的。

咱們一般在Web開發的時候,JavaBean可能會做爲參數或返回在遠程方法調用中,若是對象不可序列化會出錯,所以,JavaBean須要實現Serializable接口。

序列化對象

建立一個Person類。

//必須實現Serializable接口
class Person implements Serializable {
    //序列化ID serialVersionUID
    private static final long serialVersionUID = 6402392549803169300L;
    private String name;
    private int age;

    public int getAge() {
        return age;
    }

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

    public String getName() {
        return name;
    }

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

建立序列化流,將對象轉化爲字節,並寫入"D:\1.data"。

public class ObjectOutputStreamDemo {
    public static void main(String[] args) throws IOException {
        Person p = new Person();
        p.setAge(18);
        p.setName("Niu");
        //建立序列化流
        //真正將數據寫出的流是FileOutputStream
        //ObjectOutputStream將對象轉化爲字節
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("D:\\1.data"));
        out.writeObject(p);
        out.close();
    }
}

建立反序列化流,將從"D:\1.data"中讀取的字節轉化爲對象。

public static void main(String[] args) throws IOException, ClassNotFoundException {
        //建立反序列化流
        //真正讀取文件的是FileInputStream
        //ObjectInputStream將讀取的字節轉化爲對象
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("D:\\1.data"));
        //讀取數據必須進行數據類型的強制轉換
        Person p = (Person)in.readObject();
        in.close();
        System.out.println(p.getName());//Niu
        System.out.println(p.getAge());//18

    }

須要注意的是:

  • 若是一個對象要想被序列化,那麼對應的類必須實現接口serializable,該接口沒有任何方法,僅僅做爲標記使用。
  • statictransient修飾的屬性不會進行序列化。若是屬性的類型沒有實現serializable接口可是也沒有用這二者修飾,會拋出NotSerializableException
  • 在對象序列化的時候,版本號會隨着對象一塊兒序列化出去,在反序列化的時候,對象中的版本號和類中的版本號進行比較,若是版本號一致,則容許反序列化。若是不一致,則拋出InvalidClassException
  • 集合容許被總體序列化 ,集合及其中元素會一塊兒序列化出去。
  • 若是對象的成員變量是引用類型,這個引用類型也須要是可序列化的。
  • 當一個可序列化類存在父類時,這些父類要麼有無參構造器,要麼是須要可序列化的,不然將拋出InvalidClassException的異常。

關於版本號

  • 一個類若是容許被序列化,那麼這個類中會產生一個版本號 serialVersonUID。若是沒有手動指定版本號,那麼在編譯的時候自動根據當前類中的屬性和方法計算一個版本號,也就意味着一旦類中的屬性發生改變,就會從新計算新的,致使先後不一致。可是,手動指定版本號的好處就是,不須要再計算版本號。
  • 版本號的意義在於防止類產生改動致使已經序列化出去的對象沒法反序列化回來。版本號必須用static final修飾,自己必須是long類型。

自定義序列化的兩種方法

Serializable自定義

// 實現writeObject和readObject兩個方法
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Person implements Serializable {

    private String name;
    private int age;

    // 將name的值反轉後寫入二進制流
    private void writeObject(ObjectOutputStream out) throws IOException {
        out.writeObject(new StringBuffer(name).reverse());
        out.writeInt(age);
    }

    // 將讀取的字符串反轉後賦給name
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        this.name = ((StringBuffer) in.readObject()).reverse().toString();
        this.age = in.readInt();
    }
}

還有一種更加完全的自定義機制,直接將序列化對象替換成其餘的對象,須要定義writeReplace

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Person implements Serializable {

    private String name;
    private int age;

    private Object writeReplace(){
        ArrayList<Object> list = new ArrayList<>();
        list.add(name);
        list.add(age);
        return list;
    }
}

Externalizable自定義

Externalizable實現了Seriablizable接口,並規定了兩個方法:

public interface Externalizable extends java.io.Serializable {

    void writeExternal(ObjectOutput out) throws IOException;

    void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}

實現該接口,並給出兩個方法的實現,也能夠實現自定義序列化。

@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements Externalizable {

    String name;
    int age;

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(new StringBuffer(name).reverse());
        out.writeInt(age);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        this.name = ((StringBuffer) in.readObject()).reverse().toString();
        this.age = in.readInt();
    }
}

最後

若是本文有敘述錯誤之處,還望評論區批評指正,共同進步。

原文連接:https://www.cnblogs.com/summerday152/p/14152925.html

若是以爲本文對你有幫助,能夠關注一下我公衆號,回覆關鍵字【面試】便可獲得一份Java核心知識點整理與一份面試大禮包!另有更多技術乾貨文章以及相關資料共享,你們一塊兒學習進步!

相關文章
相關標籤/搜索