java IO(六):額外功能處理流

来源:https://www.cnblogs.com/f-ck-need-u/archive/2018/01/03/8180496.html
-Advertisement-
Play Games

額外功能處理流的意思是在基礎流(InputStream/OutputStream/Reader/Writer)的基礎上提供額外的功能。常見的額外功能可歸納為以下幾種。 Bufferedxxx類和Array相關的功能此處不做介紹。本文將介紹除此之外的其餘功能以及對象序列化時涉及到的序列化介面Seria ...


額外功能處理流的意思是在基礎流(InputStream/OutputStream/Reader/Writer)的基礎上提供額外的功能。常見的額外功能可歸納為以下幾種。

  1. 是否使用緩衝功能:BufferedInputStream/BufferedOutputStream/BufferedReader/BufferedWriter(字元流的緩衝對象還提供了操作行的方法)
  2. 是否串聯多個位元組輸入流:SequenceInputStream
  3. 是否使用對象序列化功能:ObjectInputStream/ObjectOutputStream(涉及序列化介面Serializable)
  4. 是否讓流保證數據類型不變:DataInputStream/DataOutputStream
  5. 是否讓輸出流輸出時保證輸出字面符號:PrintStream/PrintWriter(列印流)
  6. 是否要操作記憶體中的字元串和數組:ByteArrayInputStream/ByteArrayOutputStream/CharArrayReader/CharArrayWriter

Bufferedxxx類和Array相關的功能此處不做介紹。本文將介紹除此之外的其餘功能以及對象序列化時涉及到的序列化介面Serializable。

1.輸入流的串聯:SequenceInputStream

SequenceInputStream按照IO體系命名的特點來理解,大致是"將位元組輸入流存放到Sequence序列中",實際上,它可用來串聯多個輸入流。意思是:有輸入流1、輸入流2、輸入流3,原本的行為是按照順序先後讀取輸入流1、2、3,現在將這3個輸入流按順序連起來當作一個大輸入流,直到輸入流3讀完後才到流的末尾。

這個序列輸入流類在IO體系裡有點特立獨行,它只有輸入流,沒有對應的輸出流。它的作用是以操作一個輸入流的方式來將多個輸入流按序追加讀取。例如,將多個文件的數據以追加的方式寫入到一個目標文件中。

當調用SequenceInputStream的close()方法時,它將會自動關閉所有它所串聯的輸入流。

如下圖:

要使用SequenceInputStream,首先看構造方法SequenceInputStream(Enumeration<? extends InputStream> e),可見它只能接收枚舉出來的位元組輸入流。但如何獲取到這些枚舉元素?可以將各個輸入流存放到一個集合中,然後使用Collections工具類中的enumeration(Collection c)方法將這個集合轉換為Enumeration對象。在此還需說明的是,通常SequenceInputStream要串聯的多個流都是有先後順序的,例如1.txt,2.txt,3.txt依序串聯下去,所以枚舉時也要保證能夠依序枚舉出來,這也要求在Collection轉換為Enumeration時,集合中的流對象在集合中也是有序的,這意味著使用List集合來存儲這些流對象是最佳的。

例如,下麵的示例中將{1..6}.txt共6個txt文件按文件名排序先後串聯成一個SequenceInputStream。

//存儲多個位元組輸入流對象到List集合中
List<FileInputStream> list = new ArrayList<FileInputStream>();
for(int i=1;i<=6;i++){
    list.add(new FileInputStream(i+".txt"));
}
//將List集合轉換為枚舉對象Enumeration
Enumeration<FileInputStream> en = Collections.enumeration(list);
//將枚舉出來的各個位元組輸入流串聯起來
SequenceInputStream sis = new SequenceInputStream(en);

2. ObjectInputStream/ObjectOutputStream和序列化介面Serializable

輸入流和輸出流可以按位元組、存儲讀取媒體類、文本類文件,但能否將java中的對象也作為數據持久化到文件中呢?io包中提供了ObjectInputStream和ObjectOutputStream來讀、寫對象。

例如給定如下Student類,將以此類作為ObjectInputStream/ObjectOutputStream流讀、寫的對象。

class Student {
    String name;
    int age;
    Student(String name,int age){
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public String toString(){
        return "{name="+name+",age="+age+"}";
    }
}

下麵使用ObjectOutputStream將Student對象寫入到文件中,這類文件的規範尾碼名為".object"。該類的構造方法為ObjectOutputStream(OutputStream out)

import java.io.*;
import java.util.*;

public class ObjectStream {
    public static void main(String[] args) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("d:/temp/a.txt"));
        Student stu = new Student("malongshuai",22);
        oos.writeObject(stu);
    }
}

編譯並執行上述代碼,將拋出NotSerializableException異常,意思是未序列化。那麼誰沒有序列化?Student對象。要想讓某對象序列化的方式很簡單,只需讓Student類實現Serializable介面即可。如下:

class Student implements Serializable {
    String name;
    int age;
    Student(String name,int age){
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public String toString(){
        return "{name="+name+",age="+age+"}";
    }
}

可以使用ObjectInputStream從文件中讀取曾被序列化的數據。這稱為"反序列化"。

ObjectInputStream ois = new ObjectInputStream(new FileInputStream("d:/temp/a.txt"));
Object obj = ois.readObject();
System.out.println("read Object from file:"+stu1.toString());  //多態

看上去序列化和反序列化是一件很簡單的事情。確實如此,但其有不少知識點和需要註意的關鍵點。

  1. 什麼是序列化?
    從前面的例子中可以看出,序列化的方式非常簡單,只需實現Serializable介面即可。它不提供任何方法。序列化的意義僅僅只是為類進行一種特殊的標識,即所謂的"蓋戳"。就像夫妻如何證明他們是夫妻,頒發一個結婚證即可,再例如豬肉憑什麼是合格的?給它貼一張合格標簽即可。
  2. 序列化的目的是什麼?
    為了將某些對象持久化保存起來,供以後反序列化的時候讀取。
  3. 序列化後的對象在存儲時會存儲哪些數據?
    存儲的內容包括:類名和類簽名(類的序列化版本號SerialVersionUID)、對象的欄位值和數組值,以及從初始對象中引用的其他所有對象的閉包。大致可以看作是存儲了一個類的版本號、類名、某些欄位的值以及引用的對象。當然,並非所有欄位值都會存儲,見下文的第6點。
  4. 對某個對象序列化後,修改對象的屬性(例如將成員變數的修飾符從public改為private),反序列化時將會如何?
    因為保存起來的序列化數據帶有一個類簽名SerialVersionUID,而修改類的定義後,編譯時這個類會生成Class文件,而這個文件中的版本號將不再和之前序列化時保存的版本號相同。於是拋出異常。
    java.io.InvalidClassException: Student; local class incompatible: stream classdesc serialVersionUID = -9151998530267376490, local class serialVersionUID = -3521625297801190192
    
    由此可以明確一點:class文件中僅只存儲類的定義語句,在new對象時將在堆記憶體中開闢一段空間並存儲對象數據(如成員變數)。反序列化實際上是將保存起來的類對象數據載入到這個開闢出來的對象空間中。
  5. 如何保證反序列化的成功?
    強烈建議顯式在實現了Serializable介面的類中,聲明一個固定的序列化。如:
    public/private/... static final long serialVersionUID = 123456L;
    
    這樣一來,無論是序列化保存時,還是後來修改了類定義生成的class文件中,其版本號都是固定且相同的,也就是說不會因為序列化版本號不同而反序列化失敗。
  6. 類中所有屬性都應該序列化嗎?
    顯然不是。有兩類數據不會被保存:靜態變數(static)、瞬態變數(transient)。例如密碼欄位、時間點等隨時改變、有安全隱患的數據不應該被序列化保存。在進行序列化的時候,只是將堆記憶體中的數據保存起來,所以加了static關鍵字的靜態屬性不會被序列化。而加了transient關鍵字的(如為Student類的age加上瞬態屬性public transient int age;)也不會被序列化。

看上去說了一大堆,其實操作起來非常簡單,只需為待序列化的對象實現Serializable介面並聲明serialVersionUID就可以了。

以下是ObjectInputStream和ObjectOutputStream序列化、反序列化多個對象的示例。序列化的時候使用了集合的方式,將多個Student對象存儲到集合中,然後遍歷集合來序列化各個Student對象。反序列化的時候,由於ObjectInputStream的readObject()一次讀取一個對象示例的數據,且沒有提供合適的判斷流結尾的返回值,只是在讀取到結尾時會拋出EOFException異常。因此此處採用while無限迴圈的方式,並通過拋出的EOFException異常來結束迴圈。

import java.io.*;
import java.util.*;

public class ObjectStream {
    public static void main(String[] args) {
        //將各學生對象存放到集合中
        List<Student> list = new ArrayList<Student>();
        list.add(new Student("Malongshuai",22));
        list.add(new Student("Gaoxiaofang",22));

        //序列化
        //writeObj(list,"d:/temp/a.object");

        //反序列化
        readObj("d:/temp/a.object");
    }


    //序列化
    public static void writeObj(List list,String filename) {
        //遍歷集合中的對象並將它們序列化
        ObjectOutputStream oos = null;
        try {
            oos = new ObjectOutputStream(new FileOutputStream(filename));
            for(Iterator it = list.iterator();it.hasNext();) {
                oos.writeObject(it.next());
            }
        } catch (FileNotFoundException f) {
            f.printStackTrace();
        } catch (IOException i) {
            i.printStackTrace();
        } finally {
            if(oos!=null) {
                try {
                    oos.close();
                } catch(IOException i){
                    i.printStackTrace();
                }
            }
        }
    }


    //反序列化:讀取序列化數據
    public static void readObj(String filename) {
        ObjectInputStream ois = null;
        try {
            ois = new ObjectInputStream(new FileInputStream(filename));
            while(true) {
                Student student = (Student)ois.readObject();
                System.out.println(student.toString());
            }
        } catch (EOFException e){

        } catch (FileNotFoundException f) {
            f.printStackTrace();
        } catch (IOException i) {
            i.printStackTrace();
        } catch (ClassNotFoundException c){
            c.printStackTrace();
        } finally {
            if(ois!=null) {
                try {
                    ois.close();
                } catch (IOException i) {
                    i.printStackTrace();
                }
            }
        }
    }
}



class Student implements Serializable {
    static final long serialVersionUID = 123456l;
    String name = "hello";
    public int age;
    Student(String name,int age){
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public String toString(){
        return "{name="+name+",age="+age+"}";
    }
}

3.PrintStream/PrintWriter

首先看一個容易出現疑惑的現象。

import java.io.*;

public class DataStream {

    public static void main(String[] args) throws IOException {
        FileOutputStream fos = new FileOutputStream("d:/temp/b.txt");
        fos.write(97);
        fos.write(353);
        fos.close();

        FileInputStream fis = new FileInputStream("d:/temp/b.txt");
        byte[] buf = new byte[10];
        int len = 0;
        while((len=fis.read(buf))!=-1) {
            String str = new String(buf);
            System.out.println(str);
        }
        fis.close();
    }
}

上面的代碼中,向文件中寫入的是數值97和353,但無論是用記事本解析還是從這裡讀取到的結果都是"aa"共兩個位元組的字母。為什麼會如此?

在write(Int i)方法寫入數據時,它會將最低位位元組寫入,而忽略前三個位元組。例如97的二進位碼為"00000000 00000000 00000000 01100001",忽略前三個位元組,寫入到文件中的二進位數據就只剩下"01100001",而這被讀取或被解析時正好解析為字母a。同理353,它的二進位數據為"00000000 00000000 00000001 01100001",雖然第三個位元組最後一位為1,但它還是被忽略,導致寫入到文件中的二進位數據仍然為"01100001",解析後就是字母a。

要避免這種問題,可以使用位元組列印流PrintStream或字元列印流PrintWriter,它會將數據按照字面展現形式輸出。例如下麵的例子中,將會向文件中分別寫入"a97"。

FileOutputStream fos = new FileOutputStream("d:/temp/a.txt");
PrintStream ps = new PrintStream(fos);
//PrintStream ps = new PrintStream("d:/temp/a.txt")
ps.write(97);   //它調用的其實還是fos.write(),所以仍然存儲字母a
ps.print(97);   //存儲字面符號97
ps.close();

使用println()可以換行,使用printf()可以以C語言的列印格式輸出。

另外,在PrintWriter中(不包括PrintStream),有一個自動更新autoFlush的概念,它表示每輸出一次換行符就自動flush一次。但註意,PrintWriter的自動刷新只對println()和printf()方法有效,對print()無效。之所以不包括PrintStream,是因為PrintWriter因為字元集處理的原因在輸出的時候涉及了一個額外的緩衝區,自動刷新就是將此緩衝區的數據flush,而PrintStream則沒有這個額外的緩衝區,因此它是實時輸出的。

4.DataInputStream/DataOutputStream

還是前面的問題,如何將97作為int數據類型保存到文件中(即將4個位元組的97存到文件中)。也就是保證數據的數據類型不變。使用DataInputStream/DataOutputStream即可。

DataOutputStream dos = new DataOutputStream(new FileOutputStream("d:/temp/a.txt"));
dos.writeInt(97);

DataInputStream dis = new DataInputStream(new FileInputStream("d:/temp/a.txt"));
System.out.println(dis.readInt());

這樣將會把97的4個位元組存儲到文件中,如果使用記事本去解析,得到的結果會是" a",共4個位元組,雖然得到的結果是a,但這隻是用記事本解析的而已。使用上面的readInt()讀取的結果則是正確的。

註:若您覺得這篇文章還不錯請點擊右下角推薦,您的支持能激發作者更大的寫作熱情,非常感謝!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 問題1: 範圍(Scope) 思考以下代碼: 控制台會列印出什麼? 答案 上述代碼會列印出5。 (1)在立即執行函數表達式(IIFE)中,有兩個命名,但是其中變數是通過關鍵詞var來聲明的。這就意味著a是這個函數的局部變數。與此相反,b是在全局作用域下的。 (2)在函數中他沒有使用_“嚴格模式”_ ...
  • 錯誤碼: This dependency was not found: * !!vue-style-loader!css-loader?{"minimize":false,"sourceMap":false}!../../node_modules/vue-loader/lib/style-rewri ...
  • 7.1 模塊的概念 把原本實現在一起的功能離散地分散到每個能實現部分功能的塊,這些塊稱為模塊。模塊具有以下幾個好處: 1 程式小,易理解,易調試測試 2 有助於抽象編程設計和複雜程式的封裝 3 內聚性強,耦合性弱 7.2 模塊的引用方法 1.基於ES2015的語法是:import 語句 2 基於Co ...
  • 初看runtime源碼,如入迷宮,小模塊間跳來跳去,我是誰,我在哪,我為什麼要打開它;再看runtime,眉目初現,繪出調用棧,如坐時光機,骨架漸漸明晰。再再看,炳如觀火,代碼層次結構已瞭然於胸。Vue 運行時模塊主要是圍繞 Vue 實例的生命周期展開的,它涵蓋了 Vue 實例生命周期內所需要的全部... ...
  • JQ 實現左右兩側菜單添加、移除 效果圖: JS代碼 html代碼 ...
  • 先不管模式, 把他和他的名字都忘了, 來看看問題 和 設計思路. 為啥要這麼做. 有一家店鋪, 裡面有一個售貨員, 售貨員當然是要賣東西的啦, 客戶進來買完東西, 找售貨員結賬, 那售貨員得知道一共多少錢吧? 一. 初步設計 商品類: 由於價格我使用的是 Long 類型, 所以, 要有一個轉換輸出的 ...
  • 概要 概要 準備工作 檢查數據 處理缺失數據 添加預設值 刪除不完整的行 刪除不完整的列 規範化數據類型 必要的轉換 重命名列名 保存結果 更多資源 準備工作 檢查數據 處理缺失數據 添加預設值 刪除不完整的行 刪除不完整的列 規範化數據類型 必要的轉換 重命名列名 保存結果 更多資源 Pandas ...
  • 上一篇文章通過通過HelloWorld程式講解了Spring boot的基本原理和使用,本文在上篇的基礎上講解Spring boot對資料庫訪問的支持,通過實際的例子演示了Spring boot與JdbcTemplate、JPA以及MyBatis的集成,並對這個過程中遇到的問題進行了分析。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...