Java IO

1. Stream(流) Category(分類)

  • 數據單位不同:字節(jié)流(8 bit,處理字節(jié),如圖片視頻)/字符流(16 bit,處理文本)
  • 流的流向不同:輸入流/輸出流(在程序的角度看待流的流向)
  • 流的角色不同:節(jié)點流(也叫“文件流”,直接作用于文件上)/處理流(在節(jié)點流之上又裝飾一層流)
抽象基類 字節(jié)流 字符流
輸入流 InputStream Reader
輸出流 OutputStream Writer

2. 節(jié)點流

直接作用在文件上的流就是節(jié)點流,也叫做文件流,java.io包下有四個類是節(jié)點流。
FileInputStream
FileOutputStream
FileReader
FileWriter

3. 處理流

處理流作用在基本流之上,可以額外的添加一些功能,比如一開始使用FileReader讀入文件,如果在FileReader之上再包裝一層緩沖流BufferReader可以增加我呢見的讀取速度。這種嵌套包裝的方式稱作裝飾者模式。

4. 標準輸入、輸出流

在不同編程語言中,標準輸入/輸出都是指鍵盤(標準輸入)、顯示器或控制臺(標準輸出)。再Java中標準輸入輸出對應了兩個流:

  • 標準輸入:System.in -> InputStream
  • 標準輸出:System.out -> OutputStream

那么標準輸入輸出有什么用呢?Java編程中我們通過要獲取鍵盤輸出,有兩種方式:

  1. Scanner
  2. 使用標準輸入流

方式二代碼:

public class test1 {
    public static void main(String[] args) {
        InputStreamReader isr = new InputStreamReader(System.in);
        BufferedReader br = new BufferedReader(isr);
        try {
            // br.readLine()方法會阻塞住,直到有鍵盤輸入
            String s = br.readLine();
            System.out.println(s);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                isr.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

3. flush()方法作用

flush()方法是在類java.io.OutputStream中的,所有的輸出流都有這個方法,作用是:刷新緩沖區(qū)

image

上圖中,web服務器通過一個輸出流向客戶端發(fā)送300字節(jié)的信息,但是緩沖區(qū)的大小有1024字節(jié),默認情況下輸出流會等待緩沖區(qū)填滿后再一次性把緩沖區(qū)全部數據發(fā)送給客戶端,所有此時300字節(jié)的信息太小,服務器就不會發(fā)送,客戶端也就接受不帶消息。而調用輸出流的flush()函數會強制刷新緩沖區(qū),將緩沖區(qū)的數據發(fā)送出去。

4. 對象流實現Java對象序列化和反序列化


/**
 * @description: 序列化目標類
 * @author: sanjin
 * @date: 2019/7/6 18:59
 */
public class Person implements Serializable {
    
    private static final long serialVersionUID = 1L;
    
    private String name;
    private Integer age;

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

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


/**
 * @description: 進行序列化
 * @author: sanjin
 * @date: 2019/7/6 18:59
 */
public class 對象序列化 {
    public static void main(String[] args) {
        ObjectOutputStream oos = null;
        try {
            oos = new ObjectOutputStream(new FileOutputStream("P:\\Projects\\Java\\Demos\\spring-framework\\my-spring-demo\\src\\main\\java\\com\\sanjin\\blog02\\java對象序列化\\person.txt"));
            Person perosn = new Person("小白", 11);
            oos.writeObject(perosn);
            oos.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (oos!=null) {
                    oos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }
}


/**
 * @description: 從文本反序列化出對象
 * @author: sanjin
 * @date: 2019/7/6 19:08
 */
public class 反序列化對象 {
    public static void main(String[] args) {
        ObjectInputStream ois = null;

        try {
            ois = new ObjectInputStream(new FileInputStream("P:\\Projects\\Java\\Demos\\spring-framework\\my-spring-demo\\src\\main\\java\\com\\sanjin\\blog02\\java對象序列化\\person.txt"));
            Person object = (Person) ois.readObject();
            System.out.println(object);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            if (ois !=null){
                try {
                    ois.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }
}

序列化的意義在于通過序列化將對象能夠持久化到硬盤中,在需要的時候在通過反序列化將對象從硬盤中加載到內存,比如我們在使用redis,MQ時候就需要考慮序列化問題。

序列化中的serialVersionUID

如果要序列化某個對象,該對象就必須實現java.io.Serializable接口才能被序列化,
使用該接口后,我們通常還會添加一個字段:
private static final long serialVersionUID = 1L// 值可以改變
那么這個字段有什么作用呢?
serialVersionUID用來表明類的不同版本之間的兼容性。
簡單來說,就是為了解決這么一個問題?
之前我們的Person類是這樣的:

/**
 * @description: 序列化目標類
 * @author: sanjin
 * @date: 2019/7/6 18:59
 */
public class Person implements Serializable {

    private static final long serialVersionUID = 1L;

    private String name;
    private Integer age;

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

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

我們顯示的聲明了serialVersionUID,然后使用ObjectOutputStream對其進行序列化,然后下一步我們修改這個類,修改后如下:

/**
 * @description: 序列化目標類
 * @author: sanjin
 * @date: 2019/7/6 18:59
 */
public class Person implements Serializable {

    private static final long serialVersionUID = 545641L;

    private String name;
    private Integer age;
    private int id;

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

    public Person(String name, Integer age, int id) {
        this.name = name;
        this.age = age;
        this.id = id;
    }

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

可以看到我在Person類中添加了一個Id字段,并且為這個字段添加了一個三個參數的構造器。那么此時,我們還能反序列化出這個Persion對象么?如果能正確序列化,那么Person對象有沒有這個id值呢?
結果如下:


1.png

可以看到,我們在對Person添加了一個id字段,還是可以完成反序列化,并且id字段的值是int類型的默認值。這是我們手動給定了一個serialVersionUID的情況,那么我們不手動指定會出現什么情況呢?
再次修改Person類:

/**
 * @description: 序列化目標類
 * @author: sanjin
 * @date: 2019/7/6 18:59
 */
public class Person implements Serializable {

    private String name;
    private Integer age;

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

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

可以看到我僅僅只是刪除了serailVersionUID字段,實現序列化接口后,如果不手動指定serialVersionUID,那么Java在運行時環(huán)境會根據類的內部細節(jié)自動生成,這就意味著我們修改Person類會生成不同的serialVersionUID。
修改Person后進行序列化。
再次修改Person類,添加id字段。

/**
 * @description: 序列化目標類
 * @author: sanjin
 * @date: 2019/7/6 18:59
 */
public class Person implements Serializable {

    private String name;
    private Integer age;
    private int id;

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

    public Person(String name, Integer age, int id) {
        this.name = name;
        this.age = age;
        this.id = id;
    }

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

再次進行反序列化:

1.png

可以看到,在反序列化時出現了異常,總結一下serialVersionUID作用:
Java序列化機制通過運行時判斷類的serialVersionUID來驗證版本的一致性。在進行反序列化時,JVM從字節(jié)流中讀取到的serialVersionUID是Person(沒有id字段),JVM會將這個serialVersionUID與本地Person(添加了id字段)的SerialVersionUID進行對比,如果一致,則進行序列化,若不一致則拋出java.io.InvalidClassException異常。
一句話總結:手動指定serialVersionUID則可以兼容進行序列化,若由JVM自動生成如果修改了類字段或者其他內部信息,其生成的serialVersionUID就會與第一次不同,序列化就會拋出異常。所以在實現了序列化接口后最好指定一下serialVersionUID。

?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容