Java I/O學習(二)

Stream概述

Stream是一個數據流,可以從它讀取數據或寫入數據。它是連接數據源或數據目的地,例如文件,網絡連接。

Stream中沒有和數組一樣,讀、寫數據時利用索引訪問的概念。也沒有和數組或RandomAccessFile一樣的,向前或向后移動。Stream是一個連續的數據流。

某些流,如PushbackInputStream可以把數據放回流中重新讀取,但是這只能是有限個數的數據,而且無法隨意遍歷數據。數據只能被順序訪問。

翻譯自Java IO: Streams

流的特性:

  • 先進先出,最先寫入輸出流的數據最先被輸入流讀取到。
  • 順序存取,可以一個接一個地往流中寫入一串字節,讀出時也將按寫入順序讀取一串字節,不能隨機訪問中間的數據。
  • 只讀或只寫,每個流只能是輸入流或輸出流的一種,不能同時具備兩個功能,在一個數據傳輸通道中,如果既要寫入數據,又要讀取數據,則要分別提供兩個流。

I/O流分類

有無數據源或目的地

根據流對象構造時是否需要數據源或數據目的地,可以分為兩類:節點流和處理流。

節點流需要有數據源或數據目的地,處理流需要一個另一個流作為參數。java.io包結構采用裝飾者模式設計流。

數據源一般有,File、ByteArray、String、char、pipes。

pipes

pipes,中文意思為通道。他的能力是為2個在同一個JVM中運行的線程提供通信。所以它可以是數據源也可以是數據目的地。不可以使用pipe在兩個不同進程中的線程間提供通信。

Java中的pipe和Linux/Unix中的概念不同。后者可以用于運行在兩塊不同空間地址的進程間通信。

使用時一個pipedInputStream應該和一個PipedOutputStream相連。寫入pipe輸出流的數據是在另一個線程中通過與其相連的pipe輸入流讀取到的。然后調用PipedOutputStream.write()輸出到當前線程。

public class PipeExample {

    public static void main(String[] args) throws IOException {

        final PipedOutputStream output = new PipedOutputStream();
        final PipedInputStream  input  = new PipedInputStream(output);


        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    output.write("Hello world, pipe!".getBytes());
                } catch (IOException e) {
                }
            }
        });


        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    int data = input.read();
                    while(data != -1){
                        System.out.print((char) data);
                        data = input.read();
                    }
                } catch (IOException e) {
                }
            }
        });

        thread1.start();
        thread2.start();

    }
}

可以看到輸出流作為參數傳遞給輸入流的構造器,使它們相連。當然可以用PipedOutputStream/PipedInputStream.connect()方法去連接另一個pipe流。

注意,pipe流的read()或者write()都是阻塞方法,必須在同一個進程中的不同線程中使用。如果在同一個線程中使用會造成線程死鎖。而且一般線程通信,傳遞的都是一個完整對象,很少有用raw byte。翻譯自Java IO: Pipes

流向

依據流的方向,可以分為輸入流河輸出流。

輸入流只進行讀操作,輸出流寫進行寫操作。

處理數據類型

按照處理數據類型,可以分為:字符流和字節流。

由于字符集的原因,導致同一個字符根據不同的字符集有不同的編碼標示。于是有了適合處理字符的便捷流。其本質就是字符和字節根據字符集之間的相互轉換。兩者區別:

  • 讀寫單位不同:字節流以字節(8bit)為單位,字符流以字符為單位,根據碼表映射字符,一次可能讀多個字節。
  • 處理對象不同:字節流能處理所有類型的數據(如圖片、avi等),而字符流只能處理字符類型的數據。

如果只是處理文本字符,優先使用字符流。其它數據類型(字符數據也可以)使用字節流。

字節流

java.io包中的類結構是采用裝飾者模式設計的,所以分別介紹字節流輸入,輸出流的節點流和處理流。

輸入流

所有字節輸入流的父類是一個抽象類,InputStream。java.io包中,它的直接子類如下:

Stream Name Dec
ByteArrayInputStream 屬于節點流,連接byte數組作為數據源
PipedInputStream 屬于節點流,連接線程間共享的通道(pipe)
StringBufferInputStream 屬于節點流,連接字符串(已廢棄)
FileInputStream 屬于節點流,連接本地文件(通過FileSystem連接JVM可訪問的file system中的文件)
ObjectInputStream 屬于處理流,反序列化先前通過ObjectOutputStream寫入的數據
FilterInputStream 屬于處理流,覆寫了InputStream中的方法來實現數據的轉換或提供額外的方法。它的子類擴展了更多的功能也屬于處理流
SequenceInputStream 屬于處理流,用于其他輸入流的邏輯連接。它依照輸入流集合順序開始讀取,直到讀到最后一個流中數據源尾為止
輸出流

所有字節流輸出流的負累是一個抽象類,OutputStream。java.io包中,他的直接子類如下:

Stream name Dec
ByteArrayOutputStream 屬于節點流,連接byte數組作為數據輸出對象
PipedOutputStream 屬于節點流,連接線程間共享的通道(pipe)
FileOutputStream 屬于節點流,連接本地文件(通過FileSystem連接JVM可訪問的file system中的文件)
FilterOutputStream 屬于處理流,覆寫了OutputStream中的方法來實現數據的轉換或提供額外的方法。它的子類擴展了更多的功能也屬于處理流
ObjectOutputStream 屬于處理流,向包含的OutputStream輸出基本數據類型和對象實例。可寫入的對象實例必須是后期可通過ObjectInputStream反序列化的,即它必須是可序列化的
PrintStream 屬于處理流,給其他輸出流添加功能,使用系統默認的字符集編碼各種類型的數據(基本數據類型,字符串,引用類型),轉換成字節輸出
流詳解
PushbackInputStream

一個裝飾流,他的主要作用就是回退字節(字節數組)或者稱作字節(字節數組)未讀,下一個讀取操作繼續讀取該字節(字節數組)。它適用于一個片段代碼讀取一串以特定字節值結束,數量不確定的字節數組;當讀取到特定的字節并調用unread()后以便于下一個讀取操作讀取到回退的字節。

PushbackInputStream有兩個字段:

  • buf,用于緩存回退字節的字節數組。
  • pos,緩存字節數組中元素個數。

構造PushbackInputStream時,不指定buf大小,默認值為1。注意unread()操作并不會跳過回退的一個或多個字節,下一個讀取操作一定會從緩存數組取出回退的項目。

SequenceInputStream

一個裝飾流,用于邏輯連接多個輸入流,并且按照集合順序開始讀取操作。它的構造函數有:

  • SequenceInputStream(Enumeration<? extends InputStream> e)
  • SequenceInputStream(InputStream s1, InputStream s2)

Enumeration封裝了有關遍歷集合的方法,同樣還可以遍歷集合的接口是Iterator。

注意遍歷并不是指單純的獲取,它的行為類似for循環。所以這兩個接口和集合類中的獲取元素方法并不重疊。

它們的區別在于:

  1. Enumeration只能獲取集合中的數據,不能修改集合結構。而Iterator除了遍歷集合,還可以刪除集合中的數據,修改集合結構。
  2. Iterator支持fail-fast錯誤檢測機制,而Enumeration不支持。
  3. Enumeration只能為Vector,Hashtable類型集合提供遍歷,且由它們生成對象;而Iterator可以為HashMap,ArrayList等集合提供遍歷。

相同點:它們的方法都是線程安全,支持同步。

可以看出Enumeration的命名和它本身提供的功能有關,只能夠枚舉集合元素,不能修改集合結構。這和Enum類似。

fail-fast錯誤檢測機制

fail-fast錯誤檢查機制指,同一時間有多個線程,使用除了Iterator自身的方法對集合的結構修改,會快速失敗。并且拋出ConcurrentModificationException異常。

關于fail-fast,這里需要注意兩點:

  1. Iterator自身方法支持線程安全,所以不會觸發fail-fast
  2. 必須是同一時間有多個線程對集合作出結構上的修改。

實踐測試fail-fast

public class TestFailFast {
    private static List<Integer> list = new ArrayList<>();
    private static Hashtable<String, Integer> table = new Hashtable<>();
    public static void main(String[] args) {
        for(int i = 0; i < 10; i++) {
            list.add(i);
            table.put(String.valueOf(i), i);
        }
        System.out.println("測試fail-fast發生時機");
        //測試fail-fast,以及不同時操作不觸發fail-fast的情況
        new Thread01().start();
        new Thread02().start();

        try {
            Thread.sleep(1000);
        }catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("測試Enumeration不支持fail-fast");
        //測試Enumeration不支持fail-fast
        new Thread03().start();
        new Thread04().start();
    }

    private static class Thread01 extends Thread {
        public void run() {
            //A.測試線程1,2不同時操作集合list,
            //會不會拋出ConcurrentModificationException
            //給線程1睡眠10毫秒,讓線程2先執行
            // try {
            //     Thread.sleep(10);
            // }catch(InterruptedException e) {
            //     e.printStackTrace();
            // }
            Iterator<Integer> iterator = list.iterator();
            while(iterator.hasNext()) {
                int i = iterator.next();
                System.out.println("Thread 1 iterator in: " + i);
                //B.如果希望拋出ConcurrentModificationException,
                //就把當前線程睡眠10毫秒。并且注釋A代碼片段執行
                try {
                    Thread.sleep(10);
                }catch(InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private static class Thread02 extends Thread {
        public void run() {
            int i = 0;
            while(i < list.size()) {
                System.out.println("Thread 2 run in: " + i);
                if(i == list.size() / 2) {
                    list.remove(i);
                }
                i++;
            }
        }
    }

    private static class Thread03 extends Thread {
        public void run() {
            Enumeration<Integer> e = table.elements();
            //c.測試同時有另一個線程改變了table結構,會不會在后續的遍歷中看到改變的結果
            //線程3睡眠10毫秒,等待線程4修改集合后再遍歷。
            // try {
            //     Thread.sleep(10);
            // }catch(InterruptedException ex) {
            //     ex.printStackTrace();
            // }
            while(e.hasMoreElements()) {

                int i = e.nextElement();
                System.out.println("Thread 3 iterator in:" + i);

                //d.測試同時有兩個線程操作table,
                //會不會拋出ConcurrentModificationException。
                //給當前線程3睡眠10毫秒,讓線程4操作集合。并且注釋c代碼片段執行。
                try {
                    Thread.sleep(10);
                }catch(InterruptedException ex) {
                    ex.printStackTrace();
                }
            }
        }
    }

    private static class Thread04 extends Thread {
        public void run() {
            int count = table.size();
            int i = 0;
            while(i < count) {
                System.out.println("Thread 4 run in: " + i);
                if(i == count / 2) {
                    table.put("random", 23);
                }
                i++;
            }
        }
    }
}

A,C情況同時運行時,輸出如下:

測試Iterator支持fail-fast以及Enumeration不支持fail-fast.png

B,D情況同時運行時,輸出如下:

測試多線程不同時操作集合不會觸發fail-fast,以及Enumeration遍歷時集合被修改可以遍歷修改部分.png

注意Hashtable使用Enumeration遍歷時,是從后往前遍歷。參考Java 集合系列18之 Iterator和Enumeration比較Java 集合系列04之 fail-fast總結Java提高篇(三四)—–fail-fast機制

字符流

同樣以裝飾者設計模式角度來看看java.io包下的字符流

Reader

Reader是字符流輸入流的父類,它是一個抽象類。

Reader Name Dec
BufferedReader 屬于處理流,緩存字符,提高讀取字符,數組以及行的效率
CharArrayReader 屬于節點流,連接char數組數據源
FilterReader 屬于處理流,過濾式字符讀取流的抽象父類,子類擴展該類
InputStreamReader 屬于處理流,連接字節流和字符流的橋梁,將字節轉換為字符
PipedReader 屬于節點流,連接線程間共享的通道(pipe)數據源
StringReader 屬于節點流,連接String對象數據源
Writer

Writer是字符輸出流的父類,它是一個抽象類

Writer Name Dec
BufferedWriter 屬于處理流,緩沖字符,提高輸出字符,數組,字符串的效率
CharArrayWriter 屬于節點流,連接char數組作為數據寫入對象
FilterWriter 屬于處理流,字符過濾式輸出流的抽象父類,子類擴展該類功能
OutputStreamWriter 屬于處理流,連接字節流與字符流的橋梁,將字符編碼轉換為字節
PipedWriter 屬于節點流,連接線程件共享的通道(pipe)作為數據輸出對象
PrintWriter 屬于處理流,向文本輸出流打印對象的格式化形式(包括基本數據類型,字符串和對象)
StringWriter 屬于節點流,一個String緩沖輸出流,生成String對象
流詳解
PushbackReader&PushbackInputStream

兩個都具有回退功能,前者針對字符,后者針對字節。PushbackReader內部維護了一個char數組緩存回退字符;PushbackInputStream內部維護了一個byte數組緩存回退字節。兩個又一個共性,在沒有指定緩存區大小時,默認只能回退一個字符或字節。

BufferedReader&inputStreamReader

BufferedReader是給其它Reader對象添加字符緩沖區,而InputStreamReader內部有一個字節數組緩沖區,用于每次進行底層讀取(native關鍵詞的讀方法,磁盤I/O交互操作)時,盡可能讀取更多字節而不是滿足必須的數量。

官方文檔中建議為了提高效率,應該使用BufferedReader+InputStreamReader組合。既然InputStreamReader有了緩沖區,干嘛還需要BufferedReader?

這是因為兩者緩存的并不是同一樣東西,提高的效率也不是同一個對象。

BufferedReader的作用是每一次的讀操作盡量從底層的字節流或字符流(這里的底層字節、字符流是指BufferedReader包含的其他流對象)讀取更多的字符放入緩沖區,從而避免多次字節轉換字符,并為其分配內存。如果底層是字節流,還會減少與磁盤文件的I/O交互。

InputStreamReader屬于處理流,必須由InputStream對象作為構造參數來實例化對象。它內部的緩存區主要作用是減少與磁盤的底層I/O交互。每一次讀取時盡可能多的讀取字節,放入字節數組緩存區。

InputStreamReader和BufferedReader組合的意義是,每調用BufferedReader對象的讀方法,盡可能多的從InputStreamReader中獲取解碼后的字符,放入緩存區。而此時InputStreamReader對象與底層磁盤文件交互時盡可能多的讀取字節放入緩存區,減少I/O交互。

實踐效率差

public class TestEfficiency {
    public static void main(String[] args) {
        File file = new File("../file/TestFile0.txt");
        long startTime = 0;
        long endTime = 0;

        InputStreamReader in = null;
        BufferedReader reader = null;
        FileInputStream underlyIn = null;

        try {
            try {
                underlyIn = new FileInputStream(file);
                in = new InputStreamReader(underlyIn, "GBK");
                int c = -1;
                startTime = System.nanoTime();
                while((c = in.read()) != -1) {
                }
                endTime = System.nanoTime() - startTime;
                System.out.println(
                    "using InputStreamReader input characters from text spend time:"
                    + endTime);

                reader = new BufferedReader(in);
                startTime = System.nanoTime();
                while((c = reader.read()) != -1) {
                }
                endTime = System.nanoTime() - startTime;
                System.out.println(
                    "using BufferedReader input characters from text spend time:"
                    + endTime);
            }finally {
                reader.close();
            }
        }catch(IOException e) {
            e.printStackTrace();
        }
    }
}

時間輸出比較:

using InputStreamReader input characters from text spend time:22936559
using BufferedReader input characters from text spend time:33166

從上面的分析可以看出,從JVM外部文件讀寫都是采用字節流中的native方法,實現I/O交互。而在內存中操作字符優化(如為字符、字節分配內存)是依靠BufferedReader/BufferedWriter。

LineNumberReader

一個緩沖字符輸入流,監視行數。從0開始,沒讀取到一個行結束數據加一。雖然類中有setLineNumber()可以改變行數的數值,實際上無法達到隨機訪問文件的效果,仍舊是順序讀取。設置的行數只是改變了getLineNumber()的返回值(也就是類內部記錄行數的變量值被修改)。

FileReader

InputStreamReader是連接字符和字節的橋梁,而一般讀取文件使用字符流是它的子類FileReader。內部實現了FileInputStream和Reader之間的轉換,提供了讀取文本的快捷方式。

PrintWriter&PrintStream

打印對象格式化形式

通過文檔得知,兩個類都可以對引用類型對象進行格式化打印。那么怎么打印呢?

PrintStream.java&PrintWriter.java

    public void print(Object obj) {
        write(String.valueOf(obj));
    }

String.java

public static String valueOf(Object obj) {
        return (obj == null) ? "null" : obj.toString();
    }

分析:首先使用String.valueOf(),調用Object.toString()得到String對象。然后使用系統默認的字符集轉換成字節,使用write(int)方法輸出。

原理

兩個類都有涉及到OutputStreamWriter,它是字符和字節之間的橋梁。

PrintStream

類內部有以下幾個成員變量:

  • BufferedWriter textOut
  • OutputStreamWriter charOut

在構造PrintStream時,傳入OutputStream(因為PrintStream是處理流),并且執行父類構造函數。然后使用當前對象(實際類型為PrintStream)構造charOut對象,最后構造textOut對象。

由于是字節流(繼承自OutputStream),有兩個公共的write()重載方法,輸出字節。其內部都是調用父類中的成員變量out(這個out就是PrintStream構造函數傳入的OutputStream參數)相應的write()

這樣做的目的是為了沖刷BufferedWriter的緩沖區。而out變量不一定有沖刷緩沖區的方法。

所有的print()重載方法內部都是調用了PrintStream的write(byte[])write(String)私有方法。而println()是調用了相應的print()后再跟上一個依賴系統的換行符。

write(String)為例

    private void write(String s) {
        try {
            synchronized (this) {
                ensureOpen();
                textOut.write(s);
                textOut.flushBuffer();
                charOut.flushBuffer();
                if (autoFlush && (s.indexOf('\n') >= 0))
                    out.flush();
            }
        }
        catch (InterruptedIOException x) {
            Thread.currentThread().interrupt();
        }
        catch (IOException x) {
            trouble = true;
        }
    }

流程分析:

  1. 字符串s傳給textOut.write()
  2. BufferedWriter內部也有一個out變量,就是charOut。傳遞給charOut.write()
  3. OutputStreamWriter內部有一個se變量,實際類型為SteamEncoding。而構造se對象時,需要傳入OutputStream,OutputStreamWriter本身,以及字符集名稱。
  4. 在StreamEncoding內部,通過字符集編碼字符串s,并且轉傳成字節序列,調用傳入的OutputStream對象的write()。而這個OutputStream對象實際類型就是PrintStream,也就是它自身實現的Write()公共方法。

PrintWriter

PrintWriter類內部有一個成員變量out,其表現類型為Writer。該變量實際類型有兩種情況,一是BuferedWriter,另一種是構造PrintWriter對象時傳入的Writer對象。

類的print()重載方法都是調用相應的write()方法。而write()方法內部都是調用out.write()方法。

println()內部就是調用print()然后加上以來系統的換行符。其余方法內部實現可以參考Java I/O PrintWriter

總結

從上述分析來看,兩者在print()重載方法方面沒有什么區別。但兩者根本區別是PrintStream是字節流,有自己處理字節的方法。而PrintWriter是字符流,沒有處理字節的方法。

另一方面就是自動沖刷緩沖區機制的區別:

  • PrintStream自動沖刷情況,write(byte[]),println()的重載方法,print()的重載方法,以及write(byte)輸出換行符字節或者字節值為10的調用時。
  • PrintWriter自動沖刷情況,printf(),println()的重載方法,format()調用時。

猜測PrintStream的存在是為了讓字節流使用字節意外的數據進行I/O操作。一般字節流,如FileOutputStream沒有一個輸出方法可以傳入除字節類型的數據。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,837評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,196評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,688評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,654評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,456評論 6 406
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,955評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,044評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,195評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,725評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,608評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,802評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,318評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,048評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,422評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,673評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,424評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,762評論 2 372

推薦閱讀更多精彩內容

  • 一、流的概念和作用。 流是一種有順序的,有起點和終點的字節集合,是對數據傳輸的總成或抽象。即數據在兩設備之間的傳輸...
    布魯斯不吐絲閱讀 10,075評論 2 95
  • tags:io categories:總結 date: 2017-03-28 22:49:50 不僅僅在JAVA領...
    行徑行閱讀 2,189評論 0 3
  • 在經過一次沒有準備的面試后,發現自己雖然寫了兩年的android代碼,基礎知識卻忘的差不多了。這是程序員的大忌,沒...
    猿來如癡閱讀 2,860評論 3 10
  • 一、IO流整體結構圖 流是一組有順序的,有起點和終點的字節集合,是對數據傳輸的總稱或抽象。即數據在兩設備間的傳輸稱...
    慕凌峰閱讀 1,169評論 0 12
  • 慢慢來 時間久遠 慢慢是長策
    NatashaDobby閱讀 175評論 0 0