Java中文件操作功能小結(jié)
文件寫(xiě)入
為提供相對(duì)較高性能的文件讀寫(xiě)操作,這里果斷選擇了 NIO 對(duì)文件的操作,因?yàn)闃I(yè)務(wù)背景需要數(shù)據(jù)的安全落盤(pán)。這里主要采用 ByteBuffer 與 FileChannel 的組合,下面是代碼片段示例:
public static void write(String file, String content) throws IOException {
ByteBuffer writeBuffer = ByteBuffer.allocate(4096);
int cap = buffer.capacity();
try (FileChannel fileChannel = FileChannel.open(Path.of(file), StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.READ)) {
byte[] tmp = content.getBytes(StandardCharsets.UTF_8);
for (int i = 0; i < tmp.length; i = i + cap) {
if (tmp.length < i + cap) {
buffer.put(tmp, i, tmp.length - i);
} else {
buffer.put(tmp, i, cap);
}
buffer.flip();
fileChannel.write(buffer);
buffer.compact();
}
fileChannel.force(false);
} finally {
buffer.clear();
}
}ByteBuffer
在上面的代碼(基于JDK11)片段中,我們使用 ByteBuffer 作為待讀寫(xiě)數(shù)據(jù)的載體才能夠配合 FileChannel 一起使用。如果是 JDK8 獲取 FileChannel 可以采用 new RandomAccessFile(new File("xx"), "rw").getChannel() 。在講 ByteBuffer 初始化之前,我們需要先對(duì)數(shù)據(jù)單位有一個(gè)明確的概念。
KB 不是 kb
我們??吹降?kb 單位對(duì)應(yīng) kilobits ,而 KB 單位對(duì)應(yīng) kilobyte。Java 中的 1 byte 對(duì)應(yīng) 8 bits,所以 1 KB(1024 byte) = 8kb (8196 bits)。包括mb、MB等也是一樣的,為方便記憶,我們只需要記住小寫(xiě)的 b 表示 bits,而大寫(xiě)的 B 表示 byte 即可。
接下來(lái)初始化采用 allocate() 方法,容量是 4096,因?yàn)?ByteBuffer 底層數(shù)據(jù)結(jié)構(gòu)是 byte 數(shù)組,再結(jié)合上面的知識(shí),我們這里創(chuàng)建了 4KB 大小的 Buffer。具體大小需要根據(jù)實(shí)際測(cè)試進(jìn)行調(diào)整,普遍的說(shuō)法是 4KB 的整數(shù)倍會(huì)發(fā)揮最大性能優(yōu)勢(shì)。
為什么是 4KB 的整數(shù)倍呢?大致就是, 操作系統(tǒng)一次 I/O 操作會(huì)以 I/O 塊為單位進(jìn)行操作,這個(gè) I/O 塊的默認(rèn)大小是 4KB。但是這個(gè)數(shù)值并不嚴(yán)謹(jǐn),它受操作系統(tǒng),磁盤(pán)等因素影響,所以需要實(shí)際測(cè)試后調(diào)整。
初始化
另一種初始化的方式是通過(guò) wrap() 對(duì)已存在 byte 數(shù)組進(jìn)行包裝,應(yīng)用場(chǎng)景會(huì)略有不同,兩者區(qū)別如下代碼片段所示:
public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
throw createCapacityException(capacity);
return new HeapByteBuffer(capacity, capacity);
}
HeapByteBuffer(int cap, int lim) {
super(-1, 0, lim, cap, new byte[cap], 0)
}
public static ByteBuffer wrap(byte[] array, int offset, int length) {
try {
return new HeapByteBuffer(array, offset, length);
} catch (IllegalArgumentException x) {
throw new IndexOutOfBoundsException();
}
}
HeapByteBuffer(byte[] buf, int off, int len) {
super(-1, off, off + len, buf.length, buf, 0)
}最終調(diào)用的都是 Buffer(int mark, int pos, int lim, int cap) 這個(gè)初始化方法,該方法也揭示了 ByteBuffer 的基本屬性:
- position:表示下一個(gè)讀寫(xiě)操作的起始位置,可通過(guò)
position()方法獲取; - limit:表示下一個(gè)讀寫(xiě)操作的最大位置,可通過(guò)
limit()方法獲??; - capacity:表示容量,可通過(guò)
capacity()方法獲?。?/li> - mark:自定義標(biāo)記位置;
上述4個(gè)屬性的關(guān)系始終滿(mǎn)足:mark <= position <= limit <= capacity。在初始化后ByteBuffer的內(nèi)部結(jié)構(gòu)如下圖所示:

ByteBuffer 操作及屬性變化
通過(guò)上圖中結(jié)構(gòu)為 ByteBuffer 初始化的結(jié)構(gòu),寫(xiě)文件需要向 buffer 中寫(xiě)入數(shù)據(jù),ByteBuffer 提供了多個(gè) put() 方法,調(diào)用 put() 相關(guān)方法之后,如下圖所示向 buffer 寫(xiě)入 8 個(gè)byte的內(nèi)容后,其內(nèi)部結(jié)構(gòu)主要是 position 指向了后續(xù)插入數(shù)據(jù)的位置:

目前數(shù)據(jù)已經(jīng)寫(xiě)入了 buffer 中,接下來(lái)需要通過(guò) FileChannel 寫(xiě)入文件,年需要將數(shù)據(jù)從 buffer 中讀出來(lái)。在調(diào)用 FileChannel 的 write() 方法之前,需要調(diào)用 buffer 的 flip() 方法,flip() 方法將標(biāo)識(shí)屬性變換為下圖所示,也就是切換為讀取模式,即 position 重置到 0,而 limit 移動(dòng)到原 position 位置。這樣從 position 讀取到 limit 就是剛剛寫(xiě)入的數(shù)據(jù):

FileChannel 完成 write 操作后,即 buffer 內(nèi)數(shù)據(jù)讀取完,則 position 的位置會(huì)移動(dòng)到 limit 所在位置。為保證數(shù)據(jù)的完整性,此時(shí)需要調(diào)用 buffer 的 compact() 方法將 position 到 limit 間未讀取的數(shù)據(jù)移動(dòng)到 buffer 的頭部,開(kāi)啟新的一輪寫(xiě)入模式,調(diào)用方法后具體的屬性關(guān)系如下圖所示(下圖中例子為數(shù)據(jù)讀 3 個(gè) byte 后調(diào)用compact() 效果,將 position 與 limit 間的數(shù)據(jù)移動(dòng)到 buffer 的頭部,并將 limit 移動(dòng)到 capacity 的位置,position 移動(dòng)到未讀數(shù)據(jù)的末尾):

最后在整個(gè)寫(xiě)文件的結(jié)尾,需要通過(guò) FileChannel 的 force() 方法將數(shù)據(jù)強(qiáng)制刷盤(pán),其實(shí)上面的所有操作只是將數(shù)據(jù)寫(xiě)入了 PageCache 中,具體何時(shí)落入磁盤(pán)由操作系統(tǒng)調(diào)度,而 force() 方法就是通知操作系統(tǒng)將 PageCache 的內(nèi)容寫(xiě)入磁盤(pán),這樣才可以確保數(shù)據(jù)真正的持久化到磁盤(pán)中。
DirectByteBuffer
還有一種方式是通過(guò) allocateDirect() 方法創(chuàng)建 DirectByteBuffer 采用對(duì)外內(nèi)存,如果需要更高的性能,或者需要長(zhǎng)期且大數(shù)據(jù)量的 I/O 操作可以采用這種方式。但一定要注意代碼片段確保的 ((DirectBuffer) buffer).cleaner().clear() 對(duì)堆外內(nèi)存進(jìn)行回收(該方法在 JDK11 版本不可直接使用)。
如果不及時(shí)清理也會(huì)造成內(nèi)存溢出。如下圖所示,左側(cè)為未調(diào)用 clear() 方法前的堆外內(nèi)存使用情況,右側(cè)為調(diào)用后的情況。同時(shí)可以配合JVM 參數(shù) -XX:MaxDirectMemorySize 一起使用避免防止內(nèi)存申請(qǐng)過(guò)大而導(dǎo)致進(jìn)程被終止;

文件讀取
這里我們將文件讀取的代碼片段摘錄如下,關(guān)于文件讀取主要是注意中文字符的亂碼問(wèn)題,因?yàn)槲覀兌x的 buffer 是有容量的,一個(gè)容量讀滿(mǎn)之后,可能一個(gè)中文字符并沒(méi)有讀取完整。因?yàn)橐粋€(gè)中文字符可能需要 2-3 個(gè) byte,有可能存在只讀取 1 個(gè) byte 的情況。
所以需要結(jié)合 CharBuffer 對(duì)未讀取完整的中文字符進(jìn)行緩沖。具體代碼示例如下所示:
public static String read(String file) throws IOException {
StringBuilder content = new StringBuilder();
ByteBuffer buffer = ByteBuffer.allocate(4096);
CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder();
CharBuffer cb = CharBuffer.allocate(4096);
try (FileChannel fileChannel = FileChannel.open(Path.of(file), StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.READ)) {
while (fileChannel.read(buffer) != -1) {
buffer.flip();
//從ByteBuffer讀取數(shù)據(jù)到CharBuffer,最后如果不是完整的字符position的位置不會(huì)移動(dòng)
//可以認(rèn)為ByteBuffer中對(duì)應(yīng)的字符未被讀取
decoder.decode(buffer, cb, false);
cb.flip();
content.append(cb, cb.position(), cb.limit());
//將CharBuffer的position強(qiáng)制重制為0
cb.rewind();
buffer.compact();
}
} finally {
cb.clear();
buffer.clear();
}
return content.toString();
}并發(fā)寫(xiě)入
FileChannel 的 read/write 操作均是線(xiàn)程安全的,但是因?yàn)槲覀儾荒鼙WC數(shù)據(jù)被一次性寫(xiě)入,所以數(shù)據(jù)最終落在文件上會(huì)是混亂的片段。這里我們采用類(lèi)似分區(qū)寫(xiě)的方式,每個(gè)線(xiàn)程負(fù)責(zé)寫(xiě)入一個(gè)分區(qū)文件,最后再執(zhí)行合并操作。
同時(shí)這里介紹下 FileLock 這一進(jìn)程級(jí)別的文件鎖,它不能夠?qū)ν惶摂M機(jī)內(nèi)多個(gè)線(xiàn)程對(duì)文件的訪(fǎng)問(wèn)提供鎖的能力。而且該鎖的具體實(shí)現(xiàn)邏輯和操作系統(tǒng)有強(qiáng)相關(guān)。
到此這篇關(guān)于Java中文件操作功能小結(jié)的文章就介紹到這了,更多相關(guān)Java文件操作內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
spring-boot-maven-plugin:unknown的完美解決方法
這篇文章主要介紹了spring-boot-maven-plugin:unknown的完美解決方法,本文通過(guò)圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-11-11
利用Lambda表達(dá)式創(chuàng)建新線(xiàn)程案例
這篇文章主要介紹了利用Lambda表達(dá)式創(chuàng)建新線(xiàn)程案例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-08-08
MyBatis查詢(xún)無(wú)記錄時(shí)的返回值問(wèn)題
這篇文章主要介紹了MyBatis查詢(xún)無(wú)記錄時(shí)的返回值問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-01-01
Spring Boot教程之利用ActiveMQ實(shí)現(xiàn)延遲消息
這篇文章主要給大家介紹了關(guān)于Spring Boot教程之利用ActiveMQ實(shí)現(xiàn)延遲消息的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用Spring Boot具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-11-11
Java?C++題解leetcode769最多能完成排序的塊
這篇文章主要為大家介紹了Java?C++題解leetcode769最多能完成排序的塊示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10
mybatis中實(shí)現(xiàn)讓返回值與bean中字段相匹配
這篇文章主要介紹了mybatis中實(shí)現(xiàn)讓返回值與bean中字段相匹配,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-10-10
淺談web項(xiàng)目讀取classpath路徑下面的文件
這篇文章主要介紹了淺談web項(xiàng)目讀取classpath路徑下面的文件,具有一定借鑒價(jià)值,需要的朋友可以參考下2018-01-01

