天天看點

備戰-Java IO

備戰-Java IO

    君如載酒須盡醉,醉來不複思天涯。

簡介:備戰-Java IO。

Java 的 I/O 大概可以分成以下幾類:

磁盤操作:File

位元組操作:InputStream 和 OutputStream

字元操作:Reader 和 Writer

對象操作:Serializable

網絡操作:Socket

新的輸入/輸出:NIO

File 類可以用于表示檔案和目錄的資訊,但是它不表示檔案的内容。

遞歸地列出一個目錄下所有檔案:

備戰-Java IO
備戰-Java IO

View Code

備戰-Java IO

從 Java7 開始,可以使用 Paths 和 Files 代替 File。

備戰-Java IO
備戰-Java IO

Java I/O 使用了裝飾者模式來實作。以 InputStream 為例,

InputStream 是抽象元件;

FileInputStream 是 InputStream 的子類,屬于具體元件,提供了位元組流的輸入操作;

FilterInputStream 屬于抽象裝飾者,裝飾者用于裝飾元件,為元件提供額外的功能。例如 BufferedInputStream 為 FileInputStream 提供緩存的功能。

備戰-Java IO

執行個體化一個具有緩存功能的位元組流對象時,隻需要在 FileInputStream 對象上再套一層 BufferedInputStream 對象即可。

DataInputStream 裝飾者提供了對更多資料類型進行輸入的操作,比如 int、double 等基本類型。

編碼就是把字元轉換為位元組,而解碼是把位元組重新組合成字元。

如果編碼和解碼過程使用不同的編碼方式那麼就出現了亂碼。

GBK 編碼中,中文字元占 2 個位元組,英文字元占 1 個位元組;

UTF-8 編碼中,中文字元占 3 個位元組,英文字元占 1 個位元組;

UTF-16be 編碼中,中文字元和英文字元都占 2 個位元組。

UTF-16be 中的 be 指的是 Big Endian,也就是大端。相應地也有 UTF-16le,le 指的是 Little Endian,也就是小端。

Java 的記憶體編碼使用雙位元組編碼 UTF-16be,這不是指 Java 隻支援這一種編碼方式,而是說 char 這種類型使用 UTF-16be 進行編碼。char 類型占 16 位,也就是兩個位元組,Java 使用這種雙位元組編碼是為了讓一個中文或者一個英文都能使用一個 char 來存儲。

String 可以看成一個字元序列,可以指定一個編碼方式将它編碼為位元組序列,也可以指定一個編碼方式将一個位元組序列解碼為 String。

在調用無參數 getBytes() 方法時,預設的編碼方式不是 UTF-16be。雙位元組編碼的好處是可以使用一個 char 存儲中文和英文,而将 String 轉為 bytes[] 位元組數組就不再需要這個好處,是以也就不再需要雙位元組編碼。getBytes() 的預設編碼方式與平台有關,一般為 UTF-8。

不管是磁盤還是網絡傳輸,最小的存儲單元都是位元組,而不是字元。但是在程式中操作的通常是字元形式的資料,是以需要提供對字元進行操作的方法。

InputStreamReader 實作從位元組流解碼成字元流;

OutputStreamWriter 實作字元流編碼成為位元組流。

備戰-Java IO
備戰-Java IO

序列化就是将一個對象轉換成位元組序列,友善存儲和傳輸。

序列化:ObjectOutputStream.writeObject()

反序列化:ObjectInputStream.readObject()

不會對靜态變量進行序列化,因為序列化隻是儲存對象的狀态,靜态變量屬于類的狀态。

序列化的類需要實作 Serializable 接口,它隻是一個标準,沒有任何方法需要實作,但是如果不去實作它的話而進行序列化,會抛出異常。

備戰-Java IO
備戰-Java IO

transient 關鍵字可以使一些屬性不會被序列化。

transient 關鍵字文法連結:https://www.cnblogs.com/taojietaoge/p/10260145.html

ArrayList 中存儲資料的數組 elementData 是用 transient 修飾的,因為這個數組是動态擴充的,并不是所有的空間都被使用,是以就不需要所有的内容都被序列化。通過重寫序列化和反序列化方法,使得可以隻序列化數組中有内容的那部分資料。

Java 中的網絡支援:

InetAddress:用于表示網絡上的硬體資源,即 IP 位址;

URL:統一資源定位符;

Sockets:使用 TCP 協定實作網絡通信;

Datagram:使用 UDP 協定實作網絡通信。

沒有公有的構造函數,隻能通過靜态方法來建立執行個體。

可以直接從 URL 中讀取位元組流資料。

備戰-Java IO
備戰-Java IO

ServerSocket:伺服器端類

Socket:用戶端類

伺服器和用戶端通過 InputStream 和 OutputStream 進行輸入輸出。

備戰-Java IO

DatagramSocket:通信類

DatagramPacket:資料包類

新的輸入/輸出 (NIO) 庫是在 JDK 1.4 中引入的,彌補了原來的 I/O 的不足,提供了高速的、面向塊的 I/O;非阻塞IO,作為原始IO的補充,為了應對高性能高并發的應用場景。

I/O 與 NIO 最重要的差別是資料打包和傳輸的方式,I/O 以流的方式處理資料,而 NIO 以塊的方式處理資料。

面向流的 I/O 一次處理一個位元組資料:一個輸入流産生一個位元組資料,一個輸出流消費一個位元組資料。為流式資料建立過濾器非常容易,連結幾個過濾器,以便每個過濾器隻負責複雜處理機制的一部分。不利的一面是,面向流的 I/O 通常相當慢。

面向塊的 I/O 一次處理一個資料塊,按塊處理資料比按流處理資料要快得多。但是面向塊的 I/O 缺少一些面向流的 I/O 所具有的優雅性和簡單性。

I/O 包和 NIO 已經很好地內建了,java.io.* 已經以 NIO 為基礎重新實作了,是以現在它可以利用 NIO 的一些特性。例如,java.io.* 包中的一些類包含以塊的形式讀寫資料的方法,這使得即使在面向流的系統中,處理速度也會更快。

通道 Channel 是對原 I/O 包中的流的模拟,可以通過它讀取和寫入資料。

通道與流的不同之處在于,流隻能在一個方向上移動(一個流必須是 InputStream 或者 OutputStream 的子類),而通道是雙向的,可以用于讀、寫或者同時用于讀寫。

通道包括以下類型:

FileChannel:從檔案中讀寫資料;

DatagramChannel:通過 UDP 讀寫網絡中資料;

SocketChannel:通過 TCP 讀寫網絡中資料;

ServerSocketChannel:可以監聽新進來的 TCP 連接配接,對每一個新進來的連接配接都會建立一個 SocketChannel。

發送給一個通道的所有資料都必須首先放到緩沖區中,同樣地,從通道中讀取的任何資料都要先讀到緩沖區中。也就是說,不會直接對通道進行讀寫資料,而是要先經過緩沖區。

緩沖區實質上是一個數組,但它不僅僅是一個數組。緩沖區提供了對資料的結構化通路,而且還可以跟蹤系統的讀/寫程序。

緩沖區包括以下類型:

ByteBuffer

CharBuffer

ShortBuffer

IntBuffer

LongBuffer

FloatBuffer

DoubleBuffer

以下展示了使用 NIO 快速複制檔案的執行個體:

備戰-Java IO
備戰-Java IO

NIO 常常被叫做非阻塞 IO,主要是因為 NIO 在網絡通信中的非阻塞特性被廣泛使用。

NIO 實作了 IO 多路複用中的 Reactor 模型,一個線程 Thread 使用一個選擇器 Selector 通過輪詢的方式去監聽多個通道 Channel 上的事件,進而讓一個線程就可以處理多個事件。

通過配置監聽的通道 Channel 為非阻塞,那麼當 Channel 上的 IO 事件還未到達時,就不會進入阻塞狀态一直等待,而是繼續輪詢其它 Channel,找到 IO 事件已經到達的 Channel 執行。

因為建立和切換線程的開銷很大,是以使用一個線程來處理多個事件而不是一個線程處理一個事件,對于 IO 密集型的應用具有很好地性能。

應該注意的是,隻有套接字 Channel 才能配置為非阻塞,而 FileChannel 不能,為 FileChannel 配置非阻塞也沒有意義。

備戰-Java IO

通道必須配置為非阻塞模式,否則使用選擇器就沒有任何意義了,因為如果通道在某個事件上被阻塞,那麼伺服器就不能響應其它事件,必須等待這個事件處理完畢才能去處理其它事件,顯然這和選擇器的作用背道而馳。

在将通道注冊到選擇器上時,還需要指定要注冊的具體事件,主要有以下幾類:

SelectionKey.OP_CONNECT

SelectionKey.OP_ACCEPT

SelectionKey.OP_READ

SelectionKey.OP_WRITE

它們在 SelectionKey 的定義如下:

可以看出每個事件可以被當成一個位域,進而組成事件集整數。例如:

使用 select() 來監聽到達的事件,它會一直阻塞直到有至少一個事件到達。

備戰-Java IO
備戰-Java IO

因為一次 select() 調用不能處理完所有的事件,并且伺服器端有可能需要一直監聽事件,是以伺服器端處理事件的代碼一般會放在一個死循環内。

備戰-Java IO
備戰-Java IO
備戰-Java IO
備戰-Java IO

記憶體映射檔案 I/O 是一種讀和寫檔案資料的方法,它可以比正常的基于流或者基于通道的 I/O 快得多。

向記憶體映射檔案寫入可能是危險的,隻是改變數組的單個元素這樣的簡單操作,就可能會直接修改磁盤上的檔案。修改資料與将資料儲存到磁盤是沒有分開的。

下面代碼行将檔案的前 1024 個位元組映射到記憶體中,map() 方法傳回一個 MappedByteBuffer,它是 ByteBuffer 的子類。是以,可以像使用其他任何 ByteBuffer 一樣使用新映射的緩沖區,作業系統會在需要時負責執行映射。

NIO 與普通 I/O 的差別主要有以下兩點:

NIO 是非阻塞的;

NIO 面向塊,I/O 面向流。

君如載酒須盡醉

醉來不複思天涯