詳解Java中的IO輸入輸出流!


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

IO流是實現輸入輸出的基礎,它可以很方便地實現數據的輸入輸出操作,即讀寫操作。

本片要點

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

基本分類

  • 根據方向
    • 輸入流:數據從外部流向程序,例如從文件中讀取數據
    • 輸出流:數據從程序流向外部,例如向文件中寫數據
  • 根據形式
    • 字符流:字符類文件,【如 txt、 java、 html】,操作16位的字符。
    • 字節流:【圖片、視頻、音頻】 ,操作8位的字節。
  • 根據功能
    • 節點流:直接從/向數據源【如磁盤、網絡】進行數據讀寫
    • 處理流:封裝其他的流,來提供增強流的功能。
輸入流 輸出流
字符流 Reader Writer
字節流 InputStream OutputStream
  • 上面四大基本流都是抽象類,都不能直接創建實例對象。
  • 數據的來源/目的地:磁盤、網絡、內存、外部設備。

發展史

  • 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:沒有緩沖區,可以單個字符的讀取,也可以自定義數組緩沖區。

輸出的基本結構

在實際應用中,異常處理的方式都需要按照下面的結構進行,本篇為了節約篇幅,之后都將采用向上拋出的方式處理異常。

    //將流對象放在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對象構建自身,自身提供緩沖區其他新增方法,通過減少磁盤讀寫次數來提高輸入和輸出的速度。

除此之外,字節流同樣也存在緩沖流,分別是BufferedInputStreamBufferedOutputStream

轉換流(適配器)

利用轉換流可以實現字符流和字節流之間的轉換

  • 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為例,構建該轉換流時需要傳入一個字節流,而寫入的數據最開始是由字符形式給定的,也就是說該轉換流實現了從字符向字節的轉換,讓兩個不同的類在一起共同辦事。

標准流/系統流

程序的所有輸入都可以來自於標准輸入,所有輸出都可以發送到標准輸出,所有錯誤信息都可以發送到標准錯誤

標准流分類

對象 解釋 封裝類型
System.in 標准輸入流 InputStream
System.out 標准輸出流 PrintStream
System.err 標准錯誤流 PrintStream

可以直接使用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();
    }
}

參考閱讀


寫在最后:如果本文有敘述錯誤之處,還望評論區批評指正,共同進步。

參考資料:《Java 編程思想》、《Java語言程序設計》、《大話設計模式》、《瘋狂Java講義》


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM