Java新IO的概念和作用
新IO和傳統的IO目的是相同的,都是用於處理輸入和輸出,但新IO使用了不同的方式來處理,就是內存映射文件,它將文件或文件的一段區域映射到內存中,像訪問內存一樣來訪問文件,大幅提高了IO的性能
新IO相關的包如下:
- java.nio:主要包含各種與Buffer相關的類
- java.nio.channels:主要包含與Channel和Selector相關的類
- java.nio.charset:主要包含與字符集相關的類
- java.nio.channels.spi:主要包含於Channel相關的服務提供者編程接口
- java.nio.charset.spi:包含與字符集相關的服務提供者編程接口
- Channel(通道)和Buffer(緩衝)是新IO的兩個核心對象,Channel是對傳統的輸入輸出系統的模擬,在新IO系統中所有的數據都需要通過通道傳輸;
- Channel(通道)提供了一個map()方法,通過該map()方法可以直接將數據映射到內存中,傳統的IO是面向流的處理,新IO則是面向塊的處理
- Buffer可以理解爲一個容器,其本質是個數組,發送到Channel中的所有對象都必須首先放到Buffer中,而從Channel中讀取的數據也必須先放到Buffer中
- 新IO還提供了用於將Unicode字符串映射成字節序列以及逆映射操作的Charset類
- 新IO還提供了用於支持非阻塞式輸入/輸出的Selector類
使用Buffer和Channel完成輸入和輸出
Buffer類
Buffer是一個抽象類,它最常用的子類是ByteBuffer,它可以在底層字節數組上進行get/set操作,除了ByteBuffer以外,其他數據類型也都有對應的Buffer類:CharBuffer/ShortBuffer/IntBuffer/LongBuffer/FloatBuffer/DoubleBuffer
,這些Buffer類都沒有提供構造器,使用靜態方法來得到其對象,如下所示
- static XxxBuffer allocate(int capacity):創建一個容量爲capacity的XxxBuffer對象
ByteBuffer類還有一個子類,MappedByteBuffer,它用於表示Channel將磁盤文件的部分或全部內容映射到內存中後得到的結果,通常MappedByteBuffer對象由Channel的map()方法返回。
在Buffer中有3個概念:容量capacity、界限limit、位置position
- capacity:緩衝區的容量,表示該Buffer的最大數據容量,也就是最多可存儲多少數據,該值不能爲負且創建後不能改變
- limit:第一個不應該被讀出或者寫入的緩衝區位置索引,也就是說位於limit後的數據既不可被讀也不可被寫
- position:用於指明下一個可以被讀出的或者寫入的緩衝區位置索引,類似於IO流中的記錄指針,當使用Buffer從Channel中讀取數據時,position的值恰好等於已經讀到了多少數據。當新建一個Buffer對象時,其position爲0,如果從Channel中讀取了2個數據到該Buffer中,則position爲2,指向Buffer中的第三個位置(第一個位置的索引爲0)
- mark:Buffer裏還支持一個可選的標記mark,類似於傳統IO流中的mark,Buffer允許直接將position定位到該mark處
- 這些值的關係是:
0<=mark<=position<=limit<=capacity
如圖所示
- Buffer的主要作用就是裝入數據,然後輸出數據,創建一個Buffer對象時,Buffer的position爲0,limit爲capacity,程序可以通過put()方法向Buffer中放入一些數據(或者從Channel中獲取數據),每放入一些數據,Buffer的position相應的向後移動相應位置
- Buffer裝入數據結束後,調用Buffer的flip()方法,該方法將limit設置爲position所在位置,並將position設置爲0,這樣Buffer的讀寫指針就移動到了開始的位置
- Buffer調用flip()方法後,Buffer爲輸出數據做好了準備,當Buffer輸出數據結束後,Buffer調用clear()方法,它不是用來清除Buffer的數據,而是將position設置爲0,將limit設置爲capacity,自此Buffer可以再次被裝入數據
此外Buffer還包含如下常用方法:
- int capacity():返回Buffer的capacity大小
- boolean hasRemaining():判斷當前position和limit之間是否還有元素可供處理
- int limit():返回Buffer的limit的位置
- Buffer limit(int newLt):重新設置limit值,並返回一個具有新limit的Buffer對象
- Buffer mark():設置Buffer的mark位置,只能在0和position之間mark
- int position():返回Buffer的position
- Buffer position(int newPs):設置Buffer的position,並返回position被修改後的Buffer對象
- int remaining():返回當前位置和limit之間的元素個數
- Buffer reset():將position轉到mark所在的位置
- Buffer rewind():將position設置爲0,取消設置的mark
Buffer的所有子類還提供了兩個重要的方法:
- put():用於向Buffer中放入數據
- get():用於從Buffer中取出數據
當使用這兩個方法的時候,既可以對單個數據操作,也可以對多個數據操作,當需要對多個數據操作的時候只需要傳入數組參數即可
當時用這兩個方法來訪問Buffer的數據時,可以從Buffer的當前position處開始讀取或寫入數據,然後將position的值按處理元素的個數增加;也可以直接根據索引向Buffer中讀取或寫入數據,這個方式並不會影響position的值
import java.nio.*;
public class BufferTest
{
public static void main(String[] args)
{
// 創建CharBuffer,capacity和limit爲8,position爲0
CharBuffer buff = CharBuffer.allocate(8);
System.out.println("capacity: " + buff.capacity());
System.out.println("limit: " + buff.limit());
System.out.println("position: " + buff.position());
// 放入元素
buff.put('a');
buff.put('b');
buff.put('c');
System.out.println("加入三個元素後,position = "
+ buff.position());
// 調用flip()方法,將limit設置爲position處,把position設置爲0
buff.flip();
System.out.println("執行flip()後,limit = " + buff.limit());
System.out.println("position = " + buff.position());
// 取出第一個元素,position向後移動一位
System.out.println("第一個元素(position=0):" + buff.get());
System.out.println("取出一個元素後,position = "
+ buff.position());
// 調用clear方法,將position設置爲0,將limit設置爲與capacity相等
buff.clear();
System.out.println("執行clear()後,limit = " + buff.limit());
System.out.println("執行clear()後,position = "
+ buff.position());
// 根據索引來取值的方式不會影響Buffer的position
System.out.println("執行clear()後,緩衝區內容並沒有被清除:"
+ "第三個元素爲:" + buff.get(2));
System.out.println("執行絕對讀取後,position = "
+ buff.position());
}
}
通過allocate()方法創建的Buffer對象是普通Buffer,ByteBuffer還提供了一個allocateDirect()方法來創建直接Buffer,直接Buffer的創建成本比普通Buffer高,但讀取效率也更高
Channel類
- Channel可以直接將指定文件的部門或者全部映射成Buffer
- 程序不能直接訪問Channel中的數據,包括讀取、寫入都不行,Channel只能與Buffer進行交互
Java爲Channel接口提供了DatagramChannel、FileChannel、Pipe.SinkChannel、Pipe.SourceChannel、SelectableChannel、ServerSocketChannel、SocketChannel等實現類,所有的Channel都不應該通過構造器來直接創建,而是通過傳統的節點InputStream、OutputStream的getChannel()方法來返回對應的Channel,不同的節點流獲得的Channel不一樣。例如FileInputStream、FileOutputStream的getChannel()返回的是FileChannel,而PipedInputStream和PipedOutputStream的getChannel()返回的是Pipe.SinkChannel、Pipe.SourceChannel
Channel中最常用的三類方法是map()、read()和write(),其中map()方法用於將Channel對應的部分或全部數據映射成ByteBuffer,而read()和write()方法都有一系列重載形式,這些方法用於對Buffer讀取或寫入數據
map()方法的方法簽名爲:MappedByteBuffer map(FileChannel.MapMode mode, long position, long size),第一個參數是執行映射時的模式,分別有隻讀、讀寫等模式,第二個、第三個參數用於控制將Channel的哪些數據映射成ByteBuffer
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;
public class FileChannelTest
{
public static void main(String[] args)
{
var f = new File("FileChannelTest.java");
try (
// 創建FileInputStream,以該文件輸入流創建FileChannel
var inChannel = new FileInputStream(f).getChannel();
// 以文件輸出流創建FileBuffer,用以控制輸出
var outChannel = new FileOutputStream("a.txt").getChannel())
{
// 將FileChannel裏的全部數據映射成ByteBuffer
MappedByteBuffer buffer = inChannel.map(FileChannel
.MapMode.READ_ONLY, 0, f.length());
// 使用GBK的字符集來創建解碼器
Charset charset = Charset.forName("GBK");
// 直接將buffer裏的數據全部輸出
outChannel.write(buffer);
// 再次調用buffer的clear()方法,復原limit、position的位置
buffer.clear();
// 創建解碼器(CharsetDecoder)對象
CharsetDecoder decoder = charset.newDecoder();
// 使用解碼器將ByteBuffer轉換成CharBuffer
CharBuffer charBuffer = decoder.decode(buffer);
// CharBuffer的toString方法可以獲取對應的字符串
System.out.println(charBuffer);
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}
RandomAccessFile中也包含了一個getChannel()方法,RandomAccessFile返回的FileChannel()是隻讀的還是讀寫的,則取決於RandomAccessFile打開文件的模式
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
public class RandomFileChannelTest
{
public static void main(String[] args)
throws IOException
{
var f = new File("a.txt");
try (
// 創建一個RandomAccessFile對象
var raf = new RandomAccessFile(f, "rw");
// 獲取RandomAccessFile對應的Channel
FileChannel randomChannel = raf.getChannel())
{
// 將Channel中所有數據映射成ByteBuffer
ByteBuffer buffer = randomChannel.map(FileChannel
.MapMode.READ_ONLY, 0, f.length());
// 把Channel的記錄指針移動到最後
randomChannel.position(f.length());
// 將buffer中所有數據輸出
randomChannel.write(buffer);
}
}
}
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;
public class ReadFile
{
public static void main(String[] args)
throws IOException
{
try (
// 創建文件輸入流
var fis = new FileInputStream("ReadFile.java");
// 創建一個FileChannel
FileChannel fcin = fis.getChannel())
{
// 定義一個ByteBuffer對象,用於重複取水
ByteBuffer bbuff = ByteBuffer.allocate(256);
// 將FileChannel中數據放入ByteBuffer中
while (fcin.read(bbuff) != -1)
{
// 鎖定Buffer的空白區
bbuff.flip();
// 創建Charset對象
Charset charset = Charset.forName("GBK");
// 創建解碼器(CharsetDecoder)對象
CharsetDecoder decoder = charset.newDecoder();
// 將ByteBuffer的內容轉碼
CharBuffer cbuff = decoder.decode(bbuff);
System.out.print(cbuff);
// 將Buffer初始化,爲下一次讀取數據做準備
bbuff.clear();
}
}
}
}
每次讀取數據後調用flip()方法將沒有數據的區域封住,避免程序從Buffer中取出null值,數據取出後立即調用clear()方法將Buffer的position設爲0,爲下一次讀取數據做準備