1. 程式人生 > >Java基礎梳理之-IO操作

Java基礎梳理之-IO操作

效率 一個 最小 pri 進入 數據流 對比 cep 寫文件

回想最開始學習Java IO相關的操作時, 被各種Reader/Stream繞暈。 現在再回頭梳理這一塊的知識點,感覺清晰了很多。 Java作為編程語言,
大部分東西都是從系統層面帶來的, 所以學習的知識點雖然在Java, 但是背後的答案卻在操作系統層面。

首先理解核心概念:IO, 究竟什麽是IO? 所謂IO就是內存與外設相關的數據傳輸。常用的外設有硬盤,網卡,打印機, 鼠標...
我們接觸最頻繁的IO操作是硬盤上文件的讀寫,所以學習IO基本上都是以文件操作為例子。IO作為操作系統的核心,知識點相當龐雜,如果沒有合適的切入點,容易迷失其中。

如果一定要找一個切入點,學習的先後順序,個人建議如下:

  1. RandomAccessFile

對於文件的操作,最適合是RandomAccessFile: 它能讀能寫能定位。 使用RandomAccessFile, 就是把文件當作一個數組,只不過這個數組是在硬盤上而已。讀寫文件就像操作數組一樣。更難能可貴的是, RandomAccessFile封裝了Java所有的基礎類型, 可以說基本滿足操作單個文件的使用需求了。

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

        RandomAccessFile bigArray = new RandomAccessFile(new File("/home/shgy/a.txt"),"rw");
        // 寫
        bigArray.seek(10);
        bigArray.writeUTF("hello,world");

        System.out.println("filePointer at " + bigArray.getFilePointer());
        // 讀
        bigArray.seek(10);
        System.out.println(bigArray.readUTF());
        bigArray.close();
    }

RandomAccessFile類有14個寫的方法,17個讀的方法,2個定位方法,2個長度相關操作,1個獲取當前文件遊標getFilePointer()的方法, 1個關閉文件釋放系統資源的方法。 剩下的兩個方法getChannel()getFD()getChannel() 跟NIO相關,getFD()是系統文件描述符,就本文所要總結的內容而言,已經超出三界之外,不在五行之中,暫時略過不提。

當文件數量多了以後,必然面臨管理的問題。無論是windows還是Linux都采用層級管理,最後形成目錄樹。這帶來一個新的問題就是文件的路徑以及文件的歸類。 面對這樣的需求,Java提供了Files類來解決。通過了解Files類提供的API, 可以看出,其功能特點在於粗粒度的文件讀寫及文件屬性的管理。

使用Files來讀寫文件更簡單:

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

        Files.write("hello world", new File("/home/shgy/a.txt"),Charset.defaultCharset());

        System.out.println(Files.readLines(new File("/home/shgy/a.txt"), Charset.defaultCharset()));
    }

Files 可以讀寫文件,可以重命名文件,可以讀取設置文件屬性,簡直是瑞士×××般的存在。這裏涉及了更多文件相關的知識點,如果有學習過《鳥哥的Linux私房菜》第七章,再學習代碼操作文件,就不會那麽困惑了。

使用Files操縱文件引出了一個新的知識點Charset, 即字符集。字符集產生的原因很簡單: 人類語言是字符形式,計算機只能以字節的方式存儲數據,字符跟字節之間得有個映射關系。比如上例中存儲的hello world, 實際上存儲的內容可以使用vim的xdd命令查看:

// vim  + %!xdd 命令即可

00000000: 6865 6c6c 6f20 776f 726c 640a            hello world.

關於字符集的知識,可以參考阮一峰的《字符編碼筆記:ASCII,Unicode 和 UTF-8》。

理解了字符集,再進入Java的IO模塊,才順理成章。前面已經說過,所謂IO,就是內存與計算機外設的數據傳輸。Java從語言層面對IO進行了抽象, 這個抽象就是Steam, 數據流。這樣的話,無論數據來源是文件,網頁,內存塊還是其他,都以一種統一的視角和處理方式看待。 所以Java定義了InputStream和OutputStream。
InputStream用於將數據讀入內存, 對應的操作是read; OutputStream用於將數據寫入外設,對應的操作是write。InputStream和OutputStream操縱的數據只能是字節或者字節數組, 這樣就不用關心數據是文本,圖片,音頻,視頻了,畢竟不管什麽類型的數據,最終的呈現形式就是字節流。
這樣,文件的操作就相當繁瑣了:

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

        // 讀取文件
        FileInputStream fis = new FileInputStream(new File("/home/shgy/a.txt"));
        byte[] bytes = new byte[1024];
        int n = fis.read(bytes);
        if(n>0){
            System.out.println(new String(bytes,0,n));
        }
        fis.close();

        // 寫文件
        FileOutputStream fos = new FileOutputStream(new File("/home/shgy/a.txt"));
        fos.write("hello, world".getBytes());
        fos.close();
}

鑒於我們處理的文件,絕大部分都是字符類型的文件,而且以字節的方式操縱字符確實過於原始,於是Java也定義了字符IO, 即Reader/Writer。

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

        // 讀取文件
        FileReader fr = new FileReader(new File("/home/shgy/a.txt"));
        char[] buf = new char[1024];
        int n = fr.read(buf);
        System.out.println(new String(buf,0,n));
        fr.close();

        FileWriter fw = new FileWriter(new File("/home/shgy/a.txt"));
        fw.write("hello, world");
        fw.close();
    }

由於計算機本質是處理字節,所以字符和字節之間需要一個橋梁,這個就是InputStreamReader/OutputStreamWriter. 為了應對各種字符集和字節之間的編碼解碼,所以定義了StreamEncoder/StreamDecoder。

對於文件的讀寫,由於是需要操作硬盤或者網卡;考慮到安全性, 在系統層面需要系統調用,由用戶態切入內核態。這個操作代價較高。所以又添加了一層緩沖,即BufferedInputStream/BufferedOutputStream 和 BufferedReader/BufferedWriter。

整個IO操作在InputStream/OutputStream和Reader/Writer基礎之上豐富多彩起來。

由於外設,比如硬盤和網絡數據的傳輸效率相比CPU的處理效率相差太遠, 在《性能之顛》中有這樣一個讓人影響深刻的對比:
1個CPU周期為0.3ns, 1次機械磁盤IO周期為1~10ms, 1次從舊金山到紐約的互聯網傳輸需要40ms; 由於時間單位太小,我們沒有概念。 我們放大一下,假如:
1個CPU周期為1s, 則一次機械磁盤IO周期為1~12個月,1次從舊金山到紐約的互聯網傳輸需要4年。 在這樣一個差距面前,如何提高IO的效率,就顯得尤為重要,
這就是NIO的由來。

在《UNIX網絡編程卷1:套接字聯網API》一書中總結了5種IO模型: 阻塞,非阻塞,IO復用,信號驅動,異步IO。Java的NIO是采用了IO復用(select)模型。

NIO處理數據,方式跟Stream有所不同。 Stream比較碎,以字節為最小粒度; NIO以數據塊為最小粒度。所以可以避免數據的反復搬運,更高效,操作起來就更繁瑣一些。

// 使用NIO寫數據到文件
    public static void main(String[] args) throws IOException {

        FileOutputStream fos = new FileOutputStream(new File("/home/shgy/a.txt"));

        FileChannel fc = fos.getChannel();

        ByteBuffer buf = ByteBuffer.allocate(1024);
        buf.put("hello,world".getBytes());
        buf.flip();
        fc.write(buf);
        System.out.println("file channel position is " + fc.position());
        fos.close();

    }

NIO有如下的幾個優點:

  1. channel是支持讀寫的,所以相比Stream更靈活。
  2. buffer可以分配堆外內存,這個對於IO來說,避免了數據從堆內存中倒騰一邊,也避免了Java的GC, 性能自然有提升。
  3. 對於網絡IO, NIO可以在同一個線程同時監聽多個端口,避免了創建多個線程和線程管理的開銷。

由於IO這一塊的知識點過於龐雜,不是一篇博客能說清楚的,這裏只是簡單梳理一下學習思路。

Java基礎梳理之-IO操作