Java NIO

Java NIO

Java NIO簡介

Java NIO ( New IO )是從 Java 1.4 版本開始引入的一個新的 IO API ,
可以替代標準的 Java IO API 。NIO 與原來的 IO 有同樣的作用和目的,但是使用的方式完全不同, NIO 支持面向緩衝區的、基於通道的 IO 操作。 NIO 將以更加高效的方式進行文件的讀寫操作.

Java IO 與 NIO 的區別

IO NIO
面向流(StreamOriented) 面向緩衝區(BufferOriented)
阻塞IO(BlockingIO) 非阻塞IO(NonBlockingIO)
(無) 選擇器(Selectors)

通道( Channel )與緩衝區(Buffer)

Java NIO 系統的核心在於:通道 (Channel) 和緩衝區(Buffer) 。
通道表示打開到 IO 設備 ( 例如:文件、套接字 ) 的連接。若需要使用 NIO 系統,需要獲取用於連接 IO 設備的通道以及用於容納數據的緩衝區。然後操作緩衝區,對數據進行處理。
簡而言之, Channel 負責傳輸, Buffer 負責存儲

緩衝區(Buffer)

緩衝區( Buffer ):一個用於特定基本數據類型的容器。由 java.nio 包定義的,所有緩衝區都是 Buffer 抽象類的子類。
Java NIO 中的 Buffer 主要用於與 NIO 通道進行交互,數據是從通道讀入緩衝區,從緩衝區寫入通道中的。
Buffer 就像一個數組,可以保存多個相同類型的數據。
根據數據類型不同 (boolean 除外 ) ,有以下 Buffer 常用子類:
ByteBuffer
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer
上述 Buffer 類 他們都採用相似的方法進行管理數據,只是各自
管理的數據類型不同而已。都是通過如下方法獲取一個 Buffer對象
static XxxBuffer allocate(int capacity) : 創建一個容量爲 capacity 的 XxxBuffer 對象

緩衝區的基本屬性

Buffer 中的重要概念:

 容量 (capacity) : 表示 Buffer 最大數據容量,緩衝區容量不能爲負,並且創
建後不能更改。

 限制 (limit) : 第一個不應該讀取或寫入的數據的索引,即位於 limit 後的數據
不可讀寫。緩衝區的限制不能爲負,並且不能大於其容量。

 位置 (position) : 下一個要讀取或寫入的數據的索引。緩衝區的位置不能爲
負,並且不能大於其限制

 標記 (mark) 與重置 (reset) : 標記是一個索引,通過 Buffer 中的 mark() 方法
指定 Buffer 中一個特定的 position ,之後可以通過調用 reset() 方法恢復到這
個 position.
標記、位置、限制、容量遵守以下不變式:
0 <= mark <= position <= limit <= capacity

緩衝區的常用方法

緩衝區存取數據的兩個核心方法:

  • put() : 存入數據到緩衝區中
  • get() : 獲取緩衝區中的數據
  • flip(); 切換讀取數據模式
  • rewind() : 可重複讀
  • clear() : 清空緩衝區. 但是緩衝區中的數據依然存在,但是處於“被遺忘”狀態
  • mark() : 標記是一個索引,通過 Buffer 中的 mark() 方法
    指定 Buffer 中一個特定的 position ,之後可以通過調用 reset() 方法恢復到這
    個 position.
    Snipaste_2019-05-09_13-40-53.png

    列:

    public void test1(){
    String str = "abcde";

    //1. 分配一個指定大小的緩衝區
    ByteBuffer buf = ByteBuffer.allocate(1024);
    
    System.out.println("-----------------allocate()----------------");
    System.out.println(buf.position());
    System.out.println(buf.limit());
    System.out.println(buf.capacity());
    
    //2. 利用 put() 存入數據到緩衝區中
    buf.put(str.getBytes());
    
    System.out.println("-----------------put()----------------");
    System.out.println(buf.position());
    System.out.println(buf.limit());
    System.out.println(buf.capacity());
    
    //3. 切換讀取數據模式
    buf.flip();
    
    System.out.println("-----------------flip()----------------");
    System.out.println(buf.position());
    System.out.println(buf.limit());
    System.out.println(buf.capacity());
    
    //4. 利用 get() 讀取緩衝區中的數據
    byte[] dst = new byte[buf.limit()];
    buf.get(dst);
    System.out.println(new String(dst, 0, dst.length));
    
    System.out.println("-----------------get()----------------");
    System.out.println(buf.position());
    System.out.println(buf.limit());
    System.out.println(buf.capacity());
    
    //5. rewind() : 可重複讀
    buf.rewind();
    
    System.out.println("-----------------rewind()----------------");
    System.out.println(buf.position());
    System.out.println(buf.limit());
    System.out.println(buf.capacity());
    
    //6. clear() : 清空緩衝區. 但是緩衝區中的數據依然存在,但是處於“被遺忘”狀態
    buf.clear();
    
    System.out.println("-----------------clear()----------------");
    System.out.println(buf.position());
    System.out.println(buf.limit());
    System.out.println(buf.capacity());
    
    System.out.println((char)buf.get());

    }

    Buffer的常用方法

方法 描述
Buffer clear() 清空緩衝區並返回對緩衝區的引用
Buffer flip() 將緩衝區的界限設置爲當前位置,並將當前位置充值爲0
int capacity() 返回Buffer的capacity大小
boolean hasRemaining() 判斷緩衝區中是否還有元素
int limit() 返回Buffer的界限(limit)的位置
Buffer limit(intn) 將設置緩衝區界限爲n,並返回一個具有新limit的緩衝區對象
Buffer mark() 對緩衝區設置標記
int position() 返回緩衝區的當前位置position
Buffer position(int n) 將設置緩衝區的當前位置爲n,並返回修改後的Buffer對象
int remaining() 返回position和limit之間的元素個數
Buffer reset() 將位置position轉到以前設置的mark所在的位置
Buffer rewind() 將位置設爲爲0,取消設置的mark

緩衝區的數據操作

Buffer 所有子類提供了兩個用於數據操作的方法 get() 與 put() 方法

 獲取 Buffer 中的數據
get() :讀取單個字節
get(byte[] dst) :批量讀取多個字節到 dst 中
get(int index) :讀取指定索引位置的字節 ( 不會移動 position)
放入數據到 Buffer 中
put(byte b) :將給定單個字節寫入緩衝區的當前位置
put(byte[] src) :將 src 中的字節寫入緩衝區的當前位置
put(int index, byte b) :將指定字節寫入緩衝區的索引位置 ( 不會移動 position)

直接與非直接緩衝區

字節緩衝區要麼是直接的,要麼是非直接的。如果爲直接字節緩衝區,則 Java 虛擬機會盡最大努力直接在
此緩衝區上執行本機 I/O 操作。也就是說,在每次調用基礎操作系統的一個本機 I/O 操作之前(或之後),
虛擬機都會盡量避免將緩衝區的內容複製到中間緩衝區中(或從中間緩衝區中複製內容)。

直接字節緩衝區可以通過調用此類的 allocateDirect() 工廠方法 來創建。此方法返回的 緩衝區進行分配和取消
分配所需成本通常高於非直接緩衝區 。直接緩衝區的內容可以駐留在常規的垃圾回收堆之外,因此,它們對
應用程序的內存需求量造成的影響可能並不明顯。所以,建議將直接緩衝區主要分配給那些易受基礎系統的
本機 I/O 操作影響的大型、持久的緩衝區。一般情況下,最好僅在直接緩衝區能在程序性能方面帶來明顯好
處時分配它們。

直接字節緩衝區還可以通過 FileChannel 的 map() 方法 將文件區域直接映射到內存中來創建。該方法返回
MappedByteBuffer 。 Java 平臺的實現有助於通過 JNI 從本機代碼創建直接字節緩衝區。如果以上這些緩衝區中的某個緩衝區實例指的是不可訪問的內存區域,則試圖訪問該區域不會更改該緩衝區的內容,並且將會在
訪問期間或稍後的某個時間導致拋出不確定的異常。

字節緩衝區是直接緩衝區還是非直接緩衝區可通過調用其 isDirect() 方法來確定。提供此方法是爲了能夠在
性能關鍵型代碼中執行顯式緩衝區管理。

內存映射文件爲什麼效率高

文件i/o的讀操作,會先向文件設備發起讀請求,然後驅動把請求要讀的數據讀取到文件的緩衝區中,這個緩衝區位於內核,然後再把這個緩衝區中的數據複製到程序虛擬地址空間中的一塊區域中。

文件i/o的寫操作,會向文件設備發起寫請求,驅動把要寫入的數據複製到程序的緩衝區中,位於用戶空間,然後再把這個緩衝區的數據複製到文件的緩衝區中。

內存映射文件,是把位於硬盤中的文件看做是程序地址空間中一塊區域對應的物理存儲器,文件的數據就是這塊區域內存中對應的數據,讀寫文件中的數據,直接對這塊區域的地址操作,就可以,減少了內存複製的環節。

所以說,內存映射文件比起文件I/O操作,效率要高,而且文件越大,體現出來的差距越大。
Snipaste_2019-05-09_13-53-02.png
Snipaste_2019-05-09_13-54-48.png

通道(Channel)

通道( Channel ):由 java.nio.channels 包定義的。 Channel 表示 IO 源與目標打開的連接。
Channel 類似於傳統的“流”。只不過 Channel本身不能直接訪問數據, Channel 只能與Buffer 進行交互。
Java 爲 Channel 接口提供的最主要實現類如下
本地文件傳輸通道
FileChannel :用於讀取、寫入、映射和操作文件的通道

網絡數據傳輸的通道
DatagramChannel :通過 UDP 讀寫網絡中的數據通道
SocketChannel :通過 TCP 讀寫網絡中的數據。
ServerSocketChannel :可以監聽新進來的 TCP 連接,對每一個新進來的連接都會創建一個 SocketChannel
Snipaste_2019-05-09_14-08-46.png
Snipaste_2019-05-09_14-12-00.png
Snipaste_2019-05-09_14-13-17.png

獲取通道

獲取通道的一種方式是對支持通道的對象調用
getChannel() 方法。支持通道的類如下:
本地I/O
FileInputStream
FileOutputStream
RandomAccessFile

網絡 I/O
DatagramSocket
Socket
ServerSocket

獲取通道的其他方式是使用 Files 類的靜態方法 newByteChannel() 獲取字節通道。或者通過通道的靜態方法 open() 打開並返回指定通道。
例如:
在 JDK 1.7 中的 NIO.2 針對各個通道提供了靜態方法 open()
//打開一個讀取的通道
FileChannel in = FileChannel.open(Paths.get("MyTest.java"), StandardOpenOption.READ);
//打開一個寫的通道
FileChannel out = FileChannel.open(Paths.get("MyTest.java"),StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
在 JDK 1.7 中的 NIO.2 的 Files 工具類的 newByteChannel()

通道的數據傳輸

將 Buffer 中數據寫入 Channel

例如:inChannel.read(byteBuffer)

從 Channel 讀取數據到 Buffer

例如:outChannel.write(byteBuffer);

列文件複製:

使用非直接緩衝區:

public static void main(String[] args) throws IOException {
//創建文件輸入輸入流
FileInputStream in = new FileInputStream("短髮.mp3");
FileOutputStream out = new FileOutputStream("短髮2.mp3");
//文件輸入輸入流的getChannel()方法獲取通道
FileChannel inChannel = in.getChannel();
FileChannel outChannel = out.getChannel();
//獲取非直接緩衝區
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//將通道中的數據放入到緩衝區中
while (inChannel.read(byteBuffer) != -1) {
//切換讀取數據的模式
byteBuffer.flip();
//將緩衝區中的數據寫入通道中
outChannel.write(byteBuffer);
//清空緩衝區
byteBuffer.clear();
}
//釋放資源
in.close();
out.close();
inChannel.close();
outChannel.close();
}

使用直接緩衝區

public static void main(String[] args) throws IOException {
//通過文件通道的靜態方法,打開讀寫通道
//參1:通過Paths獲取源文件的路徑
//參2:操作模式 StandardOpenOption.READ 讀取模式
//打開讀取文件的通道
FileChannel in = FileChannel.open(Paths.get("短髮.mp3"), StandardOpenOption.READ);
//打開寫入的通道 模式要讀還要寫 StandardOpenOption.CREATE 意思是文件不存在就創建,如果存在就覆蓋
//StandardOpenOption.CREATE_NEW 意思是文件不存在就創建,如果存在就報錯
FileChannel out = FileChannel.open(Paths.get("短髮2.mp3"), StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
//操作內存映射文件(也就是這個緩衝區在物理內存中)
MappedByteBuffer inByteBuffer = in.map(FileChannel.MapMode.READ_ONLY, 0, in.size());
MappedByteBuffer outByteBuffer = out.map(FileChannel.MapMode.READ_WRITE, 0, in.size());
//直接對緩衝區進行讀寫操作
byte[] bytes = new byte[inByteBuffer.limit()];
inByteBuffer.get(bytes);
outByteBuffer.put(bytes);
//釋放資源
in.close();
out.close();

}

通道之間的數據傳輸

通道之間的數據傳輸 用的也是直接緩衝區的方式

  • transferFrom()
  • transferTo()

    列:

public static void main(String[] args) throws IOException {
FileChannel in = FileChannel.open(Paths.get("E:\MyTest.txt"), StandardOpenOption.READ);
FileChannel out = FileChannel.open(Paths.get("D:\MyTestCopy.txt"), StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
//in.transferTo(0,in.size(),out);//把數據讀到 輸出通道中取 完成文件的複製
//或者使用輸出通道
out.force(false);
out.transferFrom(in,0,in.size()); //寫出數據,寫出的數據從輸入通道中來
in.close();
out.close();

分散 (Scatter)和聚集(Gather)

分散讀取( Scattering Reads )是指從 Channel 中讀取的數據“分散”到多個Buffer緩衝區中
Snipaste_2019-05-13_14-40-10.png

注:按照緩衝區的順序,從Channel中讀取的數據依次將Buffer填滿。

聚集寫入( Gathering Writes )是指將多個 Buffer緩衝區 中的數據“聚集”到 Channel 。
Snipaste_2019-05-13_14-34-23.png

注:按照緩衝區的順序,寫入position和limit之間的數據到Channel.

列:

public static void main(String[] args) throws IOException {
RandomAccessFile in = new RandomAccessFile("E:\demo.txt", "rw");
RandomAccessFile out = new RandomAccessFile("E:\democopy.txt", "rw");
//獲取讀取通道
FileChannel inChannel = in.getChannel();
//創建多個緩衝區
ByteBuffer buffer1 = ByteBuffer.allocate(100);
ByteBuffer buffer2 = ByteBuffer.allocate(1024);
//分散讀取到多個緩衝區中
ByteBuffer[] byteBuffers=new ByteBuffer[]{buffer1,buffer2};//把多個緩衝區放到一個大的數組中
long read = inChannel.read(byteBuffers);//把這個大的緩衝區傳進去

    //當然我們可以看看,每個緩衝區中讀入的數據
    //byteBuffers[0].flip(); //切換到讀取模式 看一下第一個緩衝區,讀入的100個字節
    //byte[] array = byteBuffers[0].array();//把ByteBuffer轉換成字節數組
    //String s = new String(array, 0, byteBuffers[0].limit());
    //System.out.println(s);

    //把每個緩衝區,切換到讀取模式
    for (ByteBuffer buffer : byteBuffers) {
        buffer.flip();
    }
    //聚集寫入
    FileChannel outChannel = out.getChannel();
    outChannel.write(byteBuffers);

    //釋放資源
    inChannel.close();
    outChannel.close();
}

FileChannel 的常用方法

方法 描述
int read(ByteBufferdst**)** 從Channel中讀取數據到ByteBuffer
long read(ByteBuffer**[]dsts)** 將Channel中的數據“分散”到ByteBuffer[]
int write(ByteBuffersrc**)** 將ByteBuffer中的數據寫入到Channel
long write(ByteBuffer**[]srcs)** 將ByteBuffer[]中的數據“聚集”到Channel
long position() 返回此通道的文件位置
FileChannel position(long p) 設置此通道的文件位置
long size() 返回此通道的文件的當前大小
FileChannel truncate(long s) 將此通道的文件截取爲給定大小
void force(boolean metaData) 強制將所有對此通道的文件更新寫入到存儲設備中

Files 類中複製文件的方法

static long copy(InputStream in, Path target, CopyOption... options)
將所有字節從輸入流複製到文件。

static long copy(Path source, OutputStream out)
將從文件到輸出流的所有字節複製到輸出流中。

static Path copy(Path source, Path target, CopyOption... options)
將一個文件複製到目標文件。

Files類中的常用方法

java.nio.file.Files 用於操作文件或目錄的工具類

Files 常用方法:

    Path copy(Path src, Path dest, CopyOption … how) : 文件的複製

    Path createDirectory(Path path, FileAttribute<?> … attr) : 創建一個目錄

    Path createFile(Path path, FileAttribute<?> … arr) : 創建一個文件

    void delete(Path path) : 刪除一個文件

    Path move(Path src, Path dest, CopyOption…how) : 將 src 移動到 dest 位置

    long size(Path path) : 返回 path 指定文件的大小

Files 常用方法:用於判斷
boolean exists(Path path, LinkOption … opts) : 判斷文件是否存在
boolean isDirectory(Path path, LinkOption … opts) : 判斷是否是目錄
boolean isExecutable(Path path) : 判斷是否是可執行文件
boolean isHidden(Path path) : 判斷是否是隱藏文件
boolean isReadable(Path path) : 判斷文件是否可讀
boolean isWritable(Path path) : 判斷文件是否可寫
boolean notExists(Path path, LinkOption … opts) : 判斷文件是否不存在
public static <A extends BasicFileAttributes> A readAttributes(Path path,Class<A> type,LinkOption...
options) : 獲取與 path 指定的文件相關聯的屬性。

Files 常用方法:用於操作內容
SeekableByteChannel newByteChannel(Path path, OpenOption…how) : 獲取與指定文件的連接,how 指定打開方式。
DirectoryStream newDirectoryStream(Path path) : 打開 path 指定的目錄
InputStream newInputStream(Path path, OpenOption…how): 獲取 InputStream 對象
OutputStream newOutputStream(Path path, OpenOption…how) : 獲取 OutputStream 對象

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章