Java面向對象系列[v1.0.0][Buffer和Channel]

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,爲下一次讀取數據做準備

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