java中最最讓人激動的部分就是IO和NIO了。IO的全稱是input output,是java程式跟外部世界交流的橋樑,IO指的是java.io包中的所有類,他們是從java1.0開始就存在的。NIO叫做new IO,是在java1.4中引入的新一代IO。
IO的本質是什麼呢?它和NIO有什麼區別呢?我們該怎麼學習IO和NIO呢?
本系列將會藉助小師妹的視角,詳細講述學習java IO的過程,希望大家能夠喜歡。
小師妹何許人也?姓名不詳,但是勤奮愛學,潛力無限,一起來看看吧。
本文的例子https://github.com/ddean2009/learn-java-io-nio
文章太長,大家可以直接下載本文PDF:下載連結java-io-all-in-one.pdf
第一章 IO的本質
IO的本質
IO的作用就是從外部系統讀取資料到java程式中,或者把java程式中輸出的資料寫回到外部系統。這裡的外部系統可能是磁碟,網路流等等。
因為對所有的外部資料的處理都是由作業系統核心來實現的,對於java應用程式來說,只是呼叫作業系統中相應的介面方法,從而和外部資料進行互動。
所有IO的本質就是對Buffer的處理,我們把資料放入Buffer供系統寫入外部資料,或者從系統Buffer中讀取從外部系統中讀取的資料。如下圖所示:
使用者空間也就是我們自己的java程式有一個Buffer,系統空間也有一個buffer。所以會出現系統空間快取資料的情況,這種情況下系統空間將會直接返回Buffer中的資料,提升讀取速度。
DMA和虛擬地址空間
在繼續講解之前,我們先講解兩個作業系統中的基本概念,方便後面我們對IO的理解。
現代作業系統都有一個叫做DMA(Direct memory access)的元件。這個元件是做什麼的呢?
一般來說對記憶體的讀寫都是要交給CPU來完成的,在沒有DMA的情況下,如果程式進行IO操作,那麼所有的CPU時間都會被佔用,CPU沒法去響應其他的任務,只能等待IO執行完成。這在現代應用程式中是無法想象的。
如果使用DMA,則CPU可以把IO操作轉交給其他的作業系統元件,比如資料管理器來操作,只有當資料管理器操作完畢之後,才會通知CPU該IO操作完成。現代作業系統基本上都實現了DMA。
虛擬地址空間也叫做(Virtual address space),為了不同程式的互相隔離和保證程式中地址的確定性,現代計算機系統引入了虛擬地址空間的概念。簡單點講可以看做是跟實際實體地址的對映,通過使用分段或者分頁的技術,將實際的實體地址對映到虛擬地址空間。
對於上面的IO的基本流程圖中,我們可以將系統空間的buffer和使用者空間的buffer同時對映到虛擬地址空間的同一個地方。這樣就省略了從系統空間拷貝到使用者空間的步驟。速度會更快。
同時為了解決虛擬空間比實體記憶體空間大的問題,現代計算機技術一般都是用了分頁技術。
分頁技術就是將虛擬空間分為很多個page,只有在需要用到的時候才為該page分配到實體記憶體的對映,這樣實體記憶體實際上可以看做虛擬空間地址的快取。
虛擬空間地址分頁對IO的影響就在於,IO的操作也是基於page來的。
比較常用的page大小有:1,024, 2,048, 和 4,096 bytes。
IO的分類
IO可以分為File/Block IO和Stream I/O兩類。
對於File/Block IO來說,資料是儲存在disk中,而disk是由filesystem來進行管理的。我們可以通過filesystem來定義file的名字,路徑,檔案屬性等內容。
filesystem通過把資料劃分成為一個個的data blocks來進行管理。有些blocks儲存著檔案的元資料,有些block儲存著真正的資料。
最後filesystem在處理資料的過程中,也進行了分頁。filesystem的分頁大小可以跟記憶體分頁的大小一致,或者是它的倍數,比如 2,048 或者 8,192 bytes等。
並不是所有的資料都是以block的形式存在的,我們還有一類IO叫做stream IO。
stream IO就像是管道流,裡面的資料是序列被消費的。
IO和NIO的區別
java1.0中的IO是流式IO,它只能一個位元組一個位元組的處理資料,所以IO也叫做Stream IO。
而NIO是為了提升IO的效率而生的,它是以Block的方式來讀取資料的。
Stream IO中,input輸入一個位元組,output就輸出一個位元組,因為是Stream,所以可以加上過濾器或者過濾器鏈,可以想想一下web框架中的filter chain。在Stream IO中,資料只能處理一次,你不能在Stream中回退資料。
在Block IO中,資料是以block的形式來被處理的,因此其處理速度要比Stream IO快,同時可以回退處理資料。但是你需要自己處理buffer,所以複雜程度要比Stream IO高。
一般來說Stream IO是阻塞型IO,當執行緒進行讀或者寫操作的時候,執行緒會被阻塞。
而NIO一般來說是非阻塞的,也就是說在進行讀或者寫的過程中可以去做其他的操作,而讀或者寫操作執行完畢之後會通知NIO操作的完成。
在IO中,主要分為DataOutPut和DataInput,分別對應IO的out和in。
DataOutPut有三大類,分別是Writer,OutputStream和ObjectOutput。
看下他們中的繼承關係:
DataInput也有三大類,分別是ObjectInput,InputStream和Reader。
看看他們的繼承關係:
ObjectOutput和ObjectInput類比較少,這裡就不列出來了。
統計一下大概20個類左右,搞清楚這20個類的用處,恭喜你java IO你就懂了!
對於NIO來說比較複雜一點,首先,為了處理block的資訊,需要將資料讀取到buffer中,所以在NIO中Buffer是一個非常中要的概念,我們看下NIO中的Buffer:
從上圖我們可以看到NIO中為我們準備了各種各樣的buffer型別使用。
另外一個非常重要的概念是channel,channel是NIO獲取資料的通道:
NIO需要掌握的類的個數比IO要稍稍多一點,畢竟NIO要複雜一點。
就這麼幾十個類,我們就掌握了IO和NIO,想想都覺得興奮。
總結
後面的文章中,我們會介紹小師妹給你們認識,剛好她也在學java IO,後面的學習就跟她一起進行吧,敬請期待。
第二章 try with和它的底層原理
簡介
小師妹是個java初學者,最近正在學習使用java IO,作為大師兄的我自然要給她最給力的支援了。一起來看看她都遇到了什麼問題和問題是怎麼被解決的吧。
IO關閉的問題
這一天,小師妹一臉鬱悶的問我:F師兄,我學Java IO也有好多天了,最近寫了一個例子,讀取一個檔案沒有問題,但是讀取很多個檔案就會告訴我:”Can't open so many files“,能幫我看看是什麼問題嗎?
更多內容請訪問www.flydean.com
小師妹的要求當然不能拒絕,我立馬響應:可能開啟檔案太多了吧,教你兩個命令,檢視最大檔案開啟限制。
一個命令是 ulimit -a
第二個命令是
ulimit -n
256
看起來是你的最大檔案限制太小了,只有256個,調大一點就可以了。
小師妹卻說:不對呀F師兄,我讀檔案都是一個一個讀的,沒有同時開這麼多檔案喲。
好吧,看下你寫的程式碼吧:
BufferedReader bufferedReader = null;
try {
String line;
bufferedReader = new BufferedReader(new FileReader("trywith/src/main/resources/www.flydean.com"));
while ((line = bufferedReader.readLine()) != null) {
log.info(line);
}
} catch (IOException e) {
log.error(e.getMessage(), e);
}
看完程式碼,問題找到了,小師妹,你的IO沒有關閉,應該在使用之後,在finally裡面把你的reader關閉。
下面這段程式碼就行了:
BufferedReader bufferedReader = null;
try {
String line;
bufferedReader = new BufferedReader(new FileReader("trywith/src/main/resources/www.flydean.com"));
while ((line = bufferedReader.readLine()) != null) {
log.info(line);
}
} catch (IOException e) {
log.error(e.getMessage(), e);
} finally {
try {
if (bufferedReader != null){
bufferedReader.close();
}
} catch (IOException ex) {
log.error(ex.getMessage(), ex);
}
}
小師妹道了一聲謝,默默的去改程式碼了。
使用try with resource
過了半個小時 ,小師妹又來找我了,F師兄,現在每段程式碼都要手動新增finally,實在是太麻煩了,很多時候我又怕忘記關閉IO了,導致程式出現無法預料的異常。你也知道我這人從來就怕麻煩,有沒有什麼簡單的辦法,可以解決這個問題呢?
那麼小師妹你用的JDK版本是多少?
小師妹不好意思的說:雖然最新的JDK已經到14了,我還是用的JDK8.
JDK8就夠了,其實從JDK7開始,Java引入了try with resource的新功能,你把使用過後要關閉的resource放到try裡面,JVM會幫你自動close的,是不是很方便,來看下面這段程式碼:
try (BufferedReader br = new BufferedReader(new FileReader("trywith/src/main/resources/www.flydean.com")))
{
String sCurrentLine;
while ((sCurrentLine = br.readLine()) != null)
{
log.info(sCurrentLine);
}
} catch (IOException e) {
log.error(e.getMessage(), e);
}
try with resource的原理
太棒了,小師妹非常開心,然後又開始問我了:F師兄,什麼是resource呀?為什麼放到try裡面就可以不用自己close了?
resource就是資源,可以打開個關閉,我們可以把實現了java.lang.AutoCloseable介面的類都叫做resource。
先看下AutoCloseable的定義:
public interface AutoCloseable {
void close() throws Exception;
}
AutoCloseable定義了一個close()方法,當我們在try with resource中打開了AutoCloseable的資源,那麼當try block執行結束的時候,JVM會自動呼叫這個close()方法來關閉資源。
我們看下上面的BufferedReader中close方法是怎麼實現的:
public void close() throws IOException {
synchronized (lock) {
if (in == null)
return;
in.close();
in = null;
cb = null;
}
}
自定義resource
小師妹恍然大悟:F師兄,那麼我們是不是可以實現AutoCloseable來建立自己的resource呢?
當然可以了,我們舉個例子,比如給你解答完這個問題,我就要去吃飯了,我們定義這樣一個resource類:
public class CustResource implements AutoCloseable {
public void helpSister(){
log.info("幫助小師妹解決問題!");
}
@Override
public void close() throws Exception {
log.info("解決完問題,趕緊去吃飯!");
}
public static void main(String[] args) throws Exception {
try( CustResource custResource= new CustResource()){
custResource.helpSister();
}
}
}
執行輸出結果:
[main] INFO com.flydean.CustResource - 幫助小師妹解決問題!
[main] INFO com.flydean.CustResource - 解決完問題,趕緊去吃飯!
總結
最後,小師妹的問題解決了,我也可以按時吃飯了。
第三章 File檔案系統
簡介
小師妹又遇到難題了,這次的問題是有關檔案的建立,檔案許可權和檔案系統相關的問題,還好這些問題的答案都在我的腦子裡面,一起來看看吧。
檔案許可權和檔案系統
早上剛到公司,小師妹就湊過來神神祕祕的問我:F師兄,我在伺服器上面放了一些重要的檔案,是非常非常重要的那種,有沒有什麼辦法給它加個保護,還兼顧一點隱私?
更多內容請訪問www.flydean.com
什麼檔案這麼重要呀?不會是你的照片吧,放心沒人會感興趣的。
小師妹說:當然不是,我要把我的學習心得放上去,但是F師兄你知道的,我剛剛開始學習,很多想法都不太成熟,想先保個密,後面再公開。
看到小師妹這麼有上進心,我老淚縱橫,心裡很是安慰。那就開始吧。
你知道,這個世界上作業系統分為兩類,windows和linux(unix)系統。兩個系統是有很大區別的,但兩個系統都有一個檔案的概念,當然linux中檔案的範圍更加廣泛,幾乎所有的資源都可以看做是檔案。
有檔案就有對應的檔案系統,這些檔案系統是由系統核心支援的,並不需要我們在java程式中重複造輪子,直接呼叫系統的核心介面就可以了。
小師妹:F師兄,這個我懂,我們不重複造輪子,我們只是輪子的搬運工。那麼java是怎麼呼叫系統核心來建立檔案的呢?
建立檔案最常用的方法就是呼叫File類中的createNewFile方法,我們看下這個方法的實現:
public boolean createNewFile() throws IOException {
SecurityManager security = System.getSecurityManager();
if (security != null) security.checkWrite(path);
if (isInvalid()) {
throw new IOException("Invalid file path");
}
return fs.createFileExclusively(path);
}
方法內部先進行了安全性檢測,如果通過了安全性檢測就會呼叫FileSystem的createFileExclusively方法來建立檔案。
在我的mac環境中,FileSystem的實現類是UnixFileSystem:
public native boolean createFileExclusively(String path)
throws IOException;
看到了嗎?UnixFileSystem中的createFileExclusively是一個native方法,它會去呼叫底層的系統介面。
小師妹:哇,檔案建立好了,我們就可以給檔案賦許可權了,但是windows和linux的許可權是一樣的嗎?
這個問題問得好,java程式碼是跨平臺的,我們的程式碼需要同時在windows和linux上的JVM執行,所以必須找到他們許可權的共同點。
我們先看一下windows檔案的許可權:
可以看到一個windows檔案的許可權可以有修改,讀取和執行三種,特殊許可權我們先不用考慮,因為我們需要找到windows和linux的共同點。
再看下linux檔案的許可權:
ls -al www.flydean.com
-rw-r--r-- 1 flydean staff 15 May 14 15:43 www.flydean.com
上面我使用了一個ll命令列出了www.flydean.com這個檔案的詳細資訊。 其中第一列就是檔案的許可權了。
linux的基本檔案許可權可以分為三部分,分別是owner,group,others,每部分和windows一樣都有讀,寫和執行的許可權,分別用rwx來表示。
三部分的許可權連起來就成了rwxrwxrwx,對比上面我們的輸出結果,我們可以看到www.flydean.com這個檔案對owner自己是可讀寫的,對Group使用者是隻讀的,對other使用者也是隻讀的。
你要想把檔案只對自己可讀,那麼可以執行下面的命令:
chmod 600 www.flydean.com
小師妹立馬激動起來:F師兄,這個我懂,6用二進位制表示就是110,600用二進位制表示就是110000000,剛剛好對應rw-------。
對於小師妹的領悟能力,我感到非常滿意。
檔案的建立
雖然我們已經不是孔乙己時代了,不需要知道茴字的四種寫法,但是多一條知識多一條路,做些充足的準備還是非常有必要的。
小師妹,那你知道在java中有哪幾種檔案的建立方法呢?
小師妹小聲道:F師兄,我只知道一種new File的方法。
我滿意的撫摸著我的鬍子,顯示一下自己高人的氣場。
之前我們講過了,IO有三大類,一種是Reader/Writer,一種是InputStream/OutputStream,最後一種是ObjectReader/ObjectWriter。
除了使用第一種new File之外,我們還可以使用OutputStream來實現,當然我們還要用到之前講到try with resource特性,讓程式碼更加簡潔。
先看第一種方式:
public void createFileWithFile() throws IOException {
File file = new File("file/src/main/resources/www.flydean.com");
//Create the file
if (file.createNewFile()){
log.info("恭喜,檔案建立成功");
}else{
log.info("不好意思,檔案建立失敗");
}
//Write Content
try(FileWriter writer = new FileWriter(file)){
writer.write("www.flydean.com");
}
}
再看第二種方式:
public void createFileWithStream() throws IOException
{
String data = "www.flydean.com";
try(FileOutputStream out = new FileOutputStream("file/src/main/resources/www.flydean.com")){
out.write(data.getBytes());
}
}
第二種方式看起來比第一種方式更加簡介。
小師妹:慢著,F師兄,JDK7中NIO就已經出現了,能不能使用NIO來建立檔案呢?
這個問題當然難不到我:
public void createFileWithNIO() throws IOException
{
String data = "www.flydean.com";
Files.write(Paths.get("file/src/main/resources/www.flydean.com"), data.getBytes());
List<String> lines = Arrays.asList("程式那些事", "www.flydean.com");
Files.write(Paths.get("file/src/main/resources/www.flydean.com"),
lines,
StandardCharsets.UTF_8,
StandardOpenOption.CREATE,
StandardOpenOption.APPEND);
}
NIO中提供了Files工具類來實現對檔案的寫操作,寫的時候我們還可以帶點引數,比如字元編碼,是替換檔案還是在append到檔案後面等等。
程式碼中檔案的許可權
小師妹又有問題了:F師兄,講了半天,還沒有給我講許可權的事情啦。
別急,現在就講許可權:
public void fileWithPromission() throws IOException {
File file = File.createTempFile("file/src/main/resources/www.flydean.com","");
log.info("{}",file.exists());
file.setExecutable(true);
file.setReadable(true,true);
file.setWritable(true);
log.info("{}",file.canExecute());
log.info("{}",file.canRead());
log.info("{}",file.canWrite());
Path path = Files.createTempFile("file/src/main/resources/www.flydean.com", "");
log.info("{}",Files.exists(path));
log.info("{}",Files.isReadable(path));
log.info("{}",Files.isWritable(path));
log.info("{}",Files.isExecutable(path));
}
上面我們講過了,JVM為了通用,只能取windows和linux都有的功能,那就是說許可權只有讀寫和執行許可權,因為windows裡面也可以區分本使用者或者其他使用者,所以是否是本使用者的許可權也保留了。
上面的例子我們使用了傳統的File和NIO中的Files來更新檔案的許可權。
總結
好了,檔案的許可權就先講到這裡了。
第四章 檔案讀取那些事
簡介
小師妹最新對java IO中的reader和stream產生了一點點困惑,不知道到底該用哪一個才對,怎麼讀取檔案才是正確的姿勢呢?今天F師兄現場為她解答。
字元和位元組
小師妹最近很迷糊:F師兄,上次你講到IO的讀取分為兩大類,分別是Reader,InputStream,這兩大類有什麼區別嗎?為什麼我看到有些類即是Reader又是Stream?比如:InputStreamReader?
小師妹,你知道哲學家的終極三問嗎?你是誰?從哪裡來?到哪裡去?
F師兄,你是不是迷糊了,我在問你java,你扯什麼哲學。
小師妹,其實吧,哲學是一切學問的基礎,你知道科學原理的英文怎麼翻譯嗎?the philosophy of science,科學的原理就是哲學。
你看計算機中程式碼的本質是什麼?程式碼的本質就是0和1組成的一串長長的二進位制數,這麼多二進位制數組合起來就成了計算機中的程式碼,也就是JVM可以識別可以執行的二進位制程式碼。
更多內容請訪問www.flydean.com
小師妹一臉崇拜:F師兄說的好像很有道理,但是這和Reader,InputStream有什麼關係呢?
別急,冥冥中自有定數,先問你一個問題,java中儲存的最小單位是什麼?
小師妹:容我想想,java中最小的應該是boolean,true和false正好和二進位制1,0對應。
對了一半,雖然boolean也是java中儲存的最小單位,但是它需要佔用一個位元組Byte的空間。java中最小的儲存單位其實是位元組Byte。不信的話可以用之前我介紹的JOL工具來驗證一下:
[main] INFO com.flydean.JolUsage - java.lang.Boolean object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 12 (object header) N/A
12 1 boolean Boolean.value N/A
13 3 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total
上面是裝箱過後的Boolean,可以看到雖然Boolean最後佔用16bytes,但是裡面的boolean只有1byte。
byte翻譯成中文就是位元組,位元組是java中儲存的基本單位。
有了位元組,我們就可以解釋字元了,字元就是由位元組組成的,根據編碼方式的不同,字元可以有1個,2個或者多個位元組組成。我們人類可以肉眼識別的漢字呀,英文什麼的都可以看做是字元。
而Reader就是按照一定編碼格式讀取的字元,而InputStream就是直接讀取的更加底層的位元組。
小師妹:我懂了,如果是文字檔案我們就可以用Reader,非文字檔案我們就可以用InputStream。
孺子可教,小師妹進步的很快。
按字元讀取的方式
小師妹,接下來F師兄給你講下按字元讀取檔案的幾種方式,第一種就是使用FileReader來讀取File,但是FileReader本身並沒有提供任何讀取資料的方法,想要真正的讀取資料,我們還是要用到BufferedReader來連線FileReader,BufferedReader提供了讀取的快取,可以一次讀取一行:
public void withFileReader() throws IOException {
File file = new File("src/main/resources/www.flydean.com");
try (FileReader fr = new FileReader(file); BufferedReader br = new BufferedReader(fr)) {
String line;
while ((line = br.readLine()) != null) {
if (line.contains("www.flydean.com")) {
log.info(line);
}
}
}
}
每次讀取一行,可以把這些行連起來就組成了stream,通過Files.lines,我們獲取到了一個stream,在stream中我們就可以使用lambda表示式來讀取檔案了,這是謂第二種方式:
public void withStream() throws IOException {
Path filePath = Paths.get("src/main/resources", "www.flydean.com");
try (Stream<String> lines = Files.lines(filePath))
{
List<String> filteredLines = lines.filter(s -> s.contains("www.flydean.com"))
.collect(Collectors.toList());
filteredLines.forEach(log::info);
}
}
第三種其實並不常用,但是師兄也想教給你。這一種方式就是用工具類中的Scanner。通過Scanner可以通過換行符來分割檔案,用起來也不錯:
public void withScanner() throws FileNotFoundException {
FileInputStream fin = new FileInputStream(new File("src/main/resources/www.flydean.com"));
Scanner scanner = new Scanner(fin,"UTF-8").useDelimiter("\n");
String theString = scanner.hasNext() ? scanner.next() : "";
log.info(theString);
scanner.close();
}
按位元組讀取的方式
小師妹聽得很滿足,連忙催促我:F師兄,字元讀取方式我都懂了,快將位元組讀取吧。
我點了點頭,小師妹,哲學的本質還記得嗎?位元組就是java儲存的本質。掌握到本質才能勘破一切虛偽。
還記得之前講過的Files工具類嗎?這個工具類提供了很多檔案操作相關的方法,其中就有讀取所有bytes的方法,小師妹要注意了,這裡是一次性讀取所有的位元組!一定要慎用,只可用於檔案較少的場景,切記切記。
public void readBytes() throws IOException {
Path path = Paths.get("src/main/resources/www.flydean.com");
byte[] data = Files.readAllBytes(path);
log.info("{}",data);
}
如果是比較大的檔案,那麼可以使用FileInputStream來一次讀取一定數量的bytes:
public void readWithStream() throws IOException {
File file = new File("src/main/resources/www.flydean.com");
byte[] bFile = new byte[(int) file.length()];
try(FileInputStream fileInputStream = new FileInputStream(file))
{
fileInputStream.read(bFile);
for (int i = 0; i < bFile.length; i++) {
log.info("{}",bFile[i]);
}
}
}
Stream讀取都是一個位元組一個位元組來讀的,這樣做會比較慢,我們使用NIO中的FileChannel和ByteBuffer來加快一些讀取速度:
public void readWithBlock() throws IOException {
try (RandomAccessFile aFile = new RandomAccessFile("src/main/resources/www.flydean.com", "r");
FileChannel inChannel = aFile.getChannel();) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (inChannel.read(buffer) > 0) {
buffer.flip();
for (int i = 0; i < buffer.limit(); i++) {
log.info("{}", buffer.get());
}
buffer.clear();
}
}
}
小師妹:如果是非常非常大的檔案的讀取,有沒有更快的方法呢?
當然有,記得上次我們講過的虛擬地址空間的對映吧:
我們可以直接將使用者的地址空間和系統的地址空間同時map到同一個虛擬地址記憶體中,這樣就免除了拷貝帶來的效能開銷:
public void copyWithMap() throws IOException{
try (RandomAccessFile aFile = new RandomAccessFile("src/main/resources/www.flydean.com", "r");
FileChannel inChannel = aFile.getChannel()) {
MappedByteBuffer buffer = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, inChannel.size());
buffer.load();
for (int i = 0; i < buffer.limit(); i++)
{
log.info("{}", buffer.get());
}
buffer.clear();
}
}
尋找出錯的行數
小師妹:好贊!F師兄你講得真好,小師妹我還有一個問題:最近在做檔案解析,有些檔案格式不規範,解析到一半就解析失敗了,但是也沒有個錯誤提示到底錯在哪一行,很難定位問題呀,有沒有什麼好的解決辦法?
看看天色已經不早了,師兄就再教你一個方法,java中有一個類叫做LineNumberReader,使用它來讀取檔案可以打印出行號,是不是就滿足了你的需求:
public void useLineNumberReader() throws IOException {
try(LineNumberReader lineNumberReader = new LineNumberReader(new FileReader("src/main/resources/www.flydean.com")))
{
//輸出初始行數
log.info("Line {}" , lineNumberReader.getLineNumber());
//重置行數
lineNumberReader.setLineNumber(2);
//獲取現有行數
log.info("Line {} ", lineNumberReader.getLineNumber());
//讀取所有檔案內容
String line = null;
while ((line = lineNumberReader.readLine()) != null)
{
log.info("Line {} is : {}" , lineNumberReader.getLineNumber() , line);
}
}
}
總結
今天給小師妹講解了字元流和位元組流,還講解了檔案讀取的基本方法,不虛此行。
第五章 檔案寫入那些事
簡介
小師妹又對F師兄提了一大堆奇奇怪怪的需求,要格式化輸出,要特定的編碼輸出,要自己定位輸出,什麼?還要閱後即焚?大家看F師兄怎麼一一接招吧。
字元輸出和位元組輸出
小師妹:F師兄,上次你的IO講到了一半,檔案讀取是基本上講完了,但是檔案的寫入還沒有講,什麼時候給小師妹我再科普科普?
小師妹:F師兄,你知道我這個人一直以來都是勤奮好學的典範,是老師們眼中的好學生,同學們心中的好榜樣,父母身邊乖巧的好孩子。在我永攀科學高峰的時候,居然發現還有一半的知識沒有獲取,真是讓我扼腕嘆息,F師兄,快快把知識傳給我吧。
小師妹你的請求,師兄我自當盡力辦到,但是我怎麼記得上次講IO檔案讀取已經過了好幾天了,怎麼今天你才來找我。
小師妹紅著臉:F師兄,這不是使用的時候遇到了點問題,才想找你把知識再複習一遍。
那先把輸出類的結構再過一遍:
上面就是輸出的兩大系統了:Writer和OutputStream。
Writer主要針對於字元,而Stream主要針對Bytes。
Writer中最最常用的就是FileWriter和BufferedWriter,我們看下一個最基本寫入的例子:
public void useBufferedWriter() throws IOException {
String content = "www.flydean.com";
File file = new File("src/main/resources/www.flydean.com");
FileWriter fw = new FileWriter(file);
try(BufferedWriter bw = new BufferedWriter(fw)){
bw.write(content);
}
}
BufferedWriter是對FileWriter的封裝,它提供了一定的buffer機制,可以提高寫入的效率。
其實BufferedWriter提供了三種寫入的方式:
public void write(int c)
public void write(char cbuf[], int off, int len)
public void write(String s, int off, int len)
第一個方法傳入一個int,第二個方法傳入字元陣列和開始讀取的位置和長度,第三個方法傳入字串和開始讀取的位置和長度。是不是很簡單,完全可以理解?
小師妹:不對呀,F師兄,後面兩個方法的引數,不管是char和String都是字元我可以理解,第一個方法傳入int是什麼鬼?
小師妹,之前跟你講的道理是不是都忘記的差不多了,int的底層儲存是bytes,char和String的底層儲存也是bytes,我們把int和char做個強制轉換就行了。我們看下是怎麼轉換的:
public void write(int c) throws IOException {
synchronized (lock) {
ensureOpen();
if (nextChar >= nChars)
flushBuffer();
cb[nextChar++] = (char) c;
}
}
還記得int需要佔用多少個位元組嗎?4個,char需要佔用2個位元組。這樣強制從int轉換到char會有精度丟失的問題,只會保留低位的2個位元組的資料,高位的兩個位元組的資料會被丟棄,這個需要在使用中注意。
看完Writer,我們再來看看Stream:
public void useFileOutputStream() throws IOException {
String str = "www.flydean.com";
try(FileOutputStream outputStream = new FileOutputStream("src/main/resources/www.flydean.com");
BufferedOutputStream bufferedOutputStream= new BufferedOutputStream(outputStream)){
byte[] strToBytes = str.getBytes();
bufferedOutputStream.write(strToBytes);
}
}
跟Writer一樣,BufferedOutputStream也是對FileOutputStream的封裝,我們看下BufferedOutputStream中提供的write方法:
public synchronized void write(int b)
public synchronized void write(byte b[], int off, int len)
比較一下和Writer的區別,BufferedOutputStream的方法是synchronized的,並且BufferedOutputStream是直接對byte進行操作的。
第一個write方法傳入int引數也是需要進行擷取的,不過這次是從int轉換成byte。
格式化輸出
小師妹:F師兄,我們經常用的System.out.println可以直接向標準輸出中輸出格式化過後的字串,檔案的寫入是不是也有類似的功能呢?
肯定有,PrintWriter就是做格式化輸出用的:
public void usePrintWriter() throws IOException {
FileWriter fileWriter = new FileWriter("src/main/resources/www.flydean.com");
try(PrintWriter printWriter = new PrintWriter(fileWriter)){
printWriter.print("www.flydean.com");
printWriter.printf("程式那些事 %s ", "非常棒");
}
}
輸出其他物件
小師妹:F師兄,我們看到可以輸出String,char還有Byte,那可不可以輸出Integer,Long等基礎型別呢?
可以的,使用DataOutputStream就可以做到:
public void useDataOutPutStream()
throws IOException {
String value = "www.flydean.com";
try(FileOutputStream fos = new FileOutputStream("src/main/resources/www.flydean.com")){
DataOutputStream outStream = new DataOutputStream(new BufferedOutputStream(fos));
outStream.writeUTF(value);
}
}
DataOutputStream提供了writeLong,writeDouble,writeFloat等等方法,還可以writeUTF!
在特定的位置寫入
小師妹:F師兄,有時候我們不需要每次都從頭開始寫入到檔案,能不能自定義在什麼位置寫入呢?
使用RandomAccessFile就可以了:
public void useRandomAccess() throws IOException {
try(RandomAccessFile writer = new RandomAccessFile("src/main/resources/www.flydean.com", "rw")){
writer.seek(100);
writer.writeInt(50);
}
}
RandomAccessFile可以通過seek來定位,然後通過write方法從指定的位置寫入。
給檔案加鎖
小師妹:F師兄,最後還有一個問題,怎麼保證我在進行檔案寫的時候別人不會覆蓋我寫的內容,不會產生衝突呢?
FileChannel可以呼叫tryLock方法來獲得一個FileLock鎖,通過這個鎖,我們可以控制檔案的訪問。
public void useFileLock()
throws IOException {
try(RandomAccessFile stream = new RandomAccessFile("src/main/resources/www.flydean.com", "rw");
FileChannel channel = stream.getChannel()){
FileLock lock = null;
try {
lock = channel.tryLock();
} catch (final OverlappingFileLockException e) {
stream.close();
channel.close();
}
stream.writeChars("www.flydean.com");
lock.release();
}
}
總結
今天給小師妹將了好多種檔案的寫的方法,夠她學習一陣子了。
第六章 目錄還是檔案
簡介
目錄和檔案傻傻分不清楚,目錄和檔案的本質到底是什麼?在java中怎麼操縱目錄,怎麼遍歷目錄。本文F師兄會為大家一一講述。
linux中的檔案和目錄
小師妹:F師兄,我最近有一個疑惑,java程式碼中好像只有檔案沒有目錄呀,是不是當初發明java的大神,一步小心走了神?
F師兄:小師妹真勇氣可嘉呀,敢於質疑權威是從小工到專家的最重要的一步。想想F師兄我,從小沒人提點,老師講什麼我就信什麼,專家說什麼我就聽什麼:股市必上一萬點,房子是給人住的不是給人炒的,原油寶當然是小白理財必備產品....然後,就沒有然後了。
更多內容請訪問www.flydean.com
雖然java中沒有目錄的概念只有File檔案,而File其實是可以表示目錄的:
public boolean isDirectory()
File中有個isDirectory方法,可以判斷該File是否是目錄。
File和目錄傻傻分不清楚,小師妹,有沒有聯想到點什麼?
小師妹:F師兄,我記得你上次講到Linux下面所有的資源都可以看做是檔案,在linux下面檔案和目錄的本質是不是一樣的?
對的,在linux下面檔案是一等公民,所有的資源都是以檔案的形式來區分的。
什麼扇區,邏輯塊,頁之類的底層結構我們就不講了。我們先考慮一下一個檔案到底應該包含哪些內容。除了檔案本身的資料之外,還有很多元資料的東西,比如檔案許可權,所有者,group,建立時間等資訊。
在linux系統中,這兩個部分是分開儲存的。存放資料本身的叫做block,存放元資料的叫做inode。
inode中儲存了block的地址,可以通過inode找到檔案實際資料儲存的block地址,從而進行檔案訪問。考慮一下大檔案可能佔用很多個block,所以一個inode中可以儲存多個block的地址,而一個檔案通常來說使用一個inode就夠了。
為了顯示層級關係和方便檔案的管理,目錄的資料檔案中存放的是該目錄下的檔案和檔案的inode地址,從而形成了一種一環套一環,圓環套圓環的鏈式關係。
上圖列出了一個通過目錄查詢其下檔案的環中環佈局。
我想java中目錄沒有單獨列出來一個類的原因可能是參考了linux底層的檔案佈局吧。
目錄的基本操作
因為在java中目錄和檔案是公用File這個類的,所以File的基本操作目錄它全都會。
基本上,目錄和檔案相比要多注意下面三類方法:
public boolean isDirectory()
public File[] listFiles()
public boolean mkdir()
為什麼說是三類呢?因為還有幾個和他們比較接近的方法,這裡就不一一列舉了。
isDirectory判斷該檔案是不是目錄。listFiles列出該目錄下面的所有檔案。mkdir建立一個檔案目錄。
小師妹:F師兄,之前我們還以目錄的遍歷要耗費比較長的時間,經過你一講解目錄的資料結構,感覺listFiles並不是一個耗時操作呀,所有的資料都已經準備好了,直接讀取出來就行。
對,看問題不要看表面,要看到隱藏在表面的本質內涵。你看師兄我平時不顯山露水,其實是真正的中流砥柱,堪稱公司優秀員工模範。
小師妹:F師兄,那平時也沒看上頭表彰你啥的?哦,我懂了,一定是老闆怕表彰了你引起別人的嫉妒,會讓你的好好大師兄的形象崩塌吧,看來老闆真的懂你呀。
目錄的進階操作
好了小師妹,你懂了就行,下面F師兄給你講一下目錄的進階操作,比如我們怎麼拷貝一個目錄呀?
小師妹,拷貝目錄簡單的F師兄,上次你就教我了:
cp -rf
一個命令的事情不就解決了嗎?難道里面還隱藏了點祕密?
咳咳咳,祕密倒是沒有,小師妹,我記得你上次說要對java從一而終的,今天師兄給你介紹一個在java中拷貝檔案目錄的方法。
其實Files工具類裡已經為我們提供了一個拷貝檔案的優秀方法:
public static Path copy(Path source, Path target, CopyOption... options)
使用這個方法,我們就可以進行檔案的拷貝了。
如果想要拷貝目錄,就遍歷目錄中的檔案,迴圈呼叫這個copy方法就夠了。
小師妹:且慢,F師兄,如果目錄下面還有目錄的,目錄下還套目錄的情況該怎麼處理?
這就是圈套呀,看我用個遞迴的方法解決它:
public void useCopyFolder() throws IOException {
File sourceFolder = new File("src/main/resources/flydean-source");
File destinationFolder = new File("src/main/resources/flydean-dest");
copyFolder(sourceFolder, destinationFolder);
}
private static void copyFolder(File sourceFolder, File destinationFolder) throws IOException
{
//如果是dir則遞迴遍歷建立dir,如果是檔案則直接拷貝
if (sourceFolder.isDirectory())
{
//檢視目標dir是否存在
if (!destinationFolder.exists())
{
destinationFolder.mkdir();
log.info("目標dir已經建立: {}",destinationFolder);
}
for (String file : sourceFolder.list())
{
File srcFile = new File(sourceFolder, file);
File destFile = new File(destinationFolder, file);
copyFolder(srcFile, destFile);
}
}
else
{
//使用Files.copy來拷貝具體的檔案
Files.copy(sourceFolder.toPath(), destinationFolder.toPath(), StandardCopyOption.REPLACE_EXISTING);
log.info("拷貝目標檔案: {}",destinationFolder);
}
}
基本思想就是遇到目錄我就遍歷,遇到檔案我就拷貝。
目錄的腰疼操作
小師妹:F師兄,假如我想刪除一個目錄中的檔案,或者我們想統計一下這個目錄下面到底有多少個檔案該怎麼做呢?
雖然這些操作有點腰疼,還是可以解決的,Files工具類中有個方法叫做walk,返回一個Stream物件,我們可以使用Stream的API來對檔案進行處理。
刪除檔案:
public void useFileWalkToDelete() throws IOException {
Path dir = Paths.get("src/main/resources/flydean");
Files.walk(dir)
.sorted(Comparator.reverseOrder())
.map(Path::toFile)
.forEach(File::delete);
}
統計檔案:
public void useFileWalkToSumSize() throws IOException {
Path folder = Paths.get("src/test/resources");
long size = Files.walk(folder)
.filter(p -> p.toFile().isFile())
.mapToLong(p -> p.toFile().length())
.sum();
log.info("dir size is: {}",size);
}
總結
本文介紹了目錄的一些非常常見和有用的操作。
第七章 檔案系統和WatchService
簡介
小師妹這次遇到了監控檔案變化的問題,F師兄給小師妹介紹了JDK7 nio中引入的WatchService,沒想到又順道普及了一下檔案系統的概念,萬萬沒想到。
監控的痛點
小師妹:F師兄最近你有沒有感覺到呼吸有點困難,後領有點涼颼颼的,說話有點不順暢的那種?
沒有啊小師妹,你是不是秋衣穿反了?
小師妹:不是的F師兄,我講的是心裡的感覺,那種莫須有的壓力,還有一絲悸動纏繞在心。
別繞彎子了小師妹,是不是又遇到問題了。
更多內容請訪問www.flydean.com
小師妹:還是F師兄懂我,這不上次的Properties檔案用得非常上手,每次修改Properties檔案都要重啟java應用程式,真的是很痛苦。有沒有什麼其他的辦法呢?
辦法當然有,最基礎的辦法就是開一個執行緒定時去監控屬性檔案的最後修改時間,如果修改了就重新載入,這樣不就行了。
小師妹:寫執行緒啊,這麼麻煩,有沒有什麼更簡單的辦法呢?
就知道你要這樣問,還好我準備的比較充分,今天給你介紹一個JDK7在nio中引入的類WatchService。
WatchService和檔案系統
WatchService是JDK7在nio中引入的介面:
監控的服務叫做WatchService,被監控的物件叫做Watchable:
WatchKey register(WatchService watcher,
WatchEvent.Kind<?>[] events,
WatchEvent.Modifier... modifiers)
throws IOException;
WatchKey register(WatchService watcher, WatchEvent.Kind<?>... events)
throws IOException;
Watchable通過register將該物件的WatchEvent註冊到WatchService上。從此只要有WatchEvent發生在Watchable物件上,就會通知WatchService。
WatchEvent有四種類型:
- ENTRY_CREATE 目標被建立
- ENTRY_DELETE 目標被刪除
- ENTRY_MODIFY 目標被修改
- OVERFLOW 一個特殊的Event,表示Event被放棄或者丟失
register返回的WatchKey就是監聽到的WatchEvent的集合。
現在來看WatchService的4個方法:
- close 關閉watchService
- poll 獲取下一個watchKey,如果沒有則返回null
- 帶時間引數的poll 在等待的一定時間內獲取下一個watchKey
- take 獲取下一個watchKey,如果沒有則一直等待
小師妹:F師兄,那怎麼才能構建一個WatchService呢?
上次文章中說的檔案系統,小師妹還記得吧,FileSystem中就有一個獲取WatchService的方法:
public abstract WatchService newWatchService() throws IOException;
我們看下FileSystem的結構圖:
在我的mac系統上,FileSystem可以分為三大類,UnixFileSystem,JrtFileSystem和ZipFileSystem。我猜在windows上面應該還有對應的windows相關的檔案系統。小師妹你要是有興趣可以去看一下。
小師妹:UnixFileSystem用來處理Unix下面的檔案,ZipFileSystem用來處理zip檔案。那JrtFileSystem是用來做什麼的?
哎呀,這就又要扯遠了,為什麼每次問問題都要扯到天邊....
從前當JDK還是9的時候,做了一個非常大的改動叫做模組化JPMS(Java Platform Module System),這個Jrt就是為了給模組化系統用的,我們來舉個例子:
public void useJRTFileSystem(){
String resource = "java/lang/Object.class";
URL url = ClassLoader.getSystemResource(resource);
log.info("{}",url);
}
上面一段程式碼我們獲取到了Object這個class的url,我們看下如果是在JDK8中,輸出是什麼:
jar:file:/Library/Java/JavaVirtualMachines/jdk1.8.0_171.jdk/Contents/Home/jre/lib/rt.jar!/java/lang/Object.class
輸出結果是jar:file表示這個Object class是放在jar檔案中的,後面是jar檔案的路徑。
如果是在JDK9之後:
jrt:/java.base/java/lang/Object.class
結果是jrt開頭的,java.base是模組的名字,後面是Object的路徑。看起來是不是比傳統的jar路徑更加簡潔明瞭。
有了檔案系統,我們就可以在獲取系統預設的檔案系統的同時,獲取到相應的WatchService:
WatchService watchService = FileSystems.getDefault().newWatchService();
WatchSerice的使用和實現本質
小師妹:F師兄,WatchSerice是咋實現的呀?這麼神奇,為我們省了這麼多工作。
其實JDK提供了這麼多類的目的就是為了不讓我們重複造輪子,之前跟你講監控檔案的最簡單辦法就是開一個獨立的執行緒來監控檔案變化嗎?其實.....WatchService就是這樣做的!
PollingWatchService() {
// TBD: Make the number of threads configurable
scheduledExecutor = Executors
.newSingleThreadScheduledExecutor(new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(null, r, "FileSystemWatcher", 0, false);
t.setDaemon(true);
return t;
}});
}
上面的方法就是生成WatchService的方法,小師妹看到沒有,它的本質就是開啟了一個daemon的執行緒,用來接收監控任務。
下面看下怎麼把一個檔案註冊到WatchService上面:
private void startWatcher(String dirPath, String file) throws IOException {
WatchService watchService = FileSystems.getDefault().newWatchService();
Path path = Paths.get(dirPath);
path.register(watchService, ENTRY_MODIFY);
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
watchService.close();
} catch (IOException e) {
log.error(e.getMessage());
}
}));
WatchKey key = null;
while (true) {
try {
key = watchService.take();
for (WatchEvent<?> event : key.pollEvents()) {
if (event.context().toString().equals(fileName)) {
loadConfig(dirPath + file);
}
}
boolean reset = key.reset();
if (!reset) {
log.info("該檔案無法重置");
break;
}
} catch (Exception e) {
log.error(e.getMessage());
}
}
}
上面的關鍵方法就是path.register,其中Path是一個Watchable物件。
然後使用watchService.take來獲取生成的WatchEvent,最後根據WatchEvent來處理檔案。
總結
道生一,一生二,二生三,三生萬物。一個簡簡單單的功能其實背後隱藏著...道德經,哦,不對,背後隱藏著道的哲學。
第八章 檔案File和路徑Path
簡介
檔案和路徑有什麼關係?檔案和路徑又隱藏了什麼祕密?在檔案系統的管理下,建立路徑的方式又有哪些?今天F師兄帶小師妹再給大家來一場精彩的表演。
檔案和路徑
小師妹:F師兄我有一個問題,java中的檔案File是一個類可以理解,因為檔案裡面包含了很多其他的資訊,但是路徑Path為什麼也要單獨一個類出來?只用一個String表示不是更簡單?
更多內容請訪問www.flydean.com
萬物皆有因,沒有無緣無故的愛,也沒有無緣無故的恨。一切真的是妙不可言啊。
我們來看下File和path的定義:
public class File
implements Serializable, Comparable<File>
public interface Path
extends Comparable<Path>, Iterable<Path>, Watchable
首先,File是一個類,它表示的是所有的檔案系統都擁有的屬性和功能,不管你是windows還是linux,他們中的File物件都應該是一樣的。
File中包含了Path,小師妹你且看,Path是一個interface,為什麼是一個interface呢?因為Path根據不同的情況可以分為JrtPath,UnixPath和ZipPath。三個Path所對應的FileSystem我們在上一篇文章中已經討論過了。所以Path的實現是不同的,但是包含Path的File是相同的。
小師妹:F師兄,這個怎麼這麼拗口,給我來一個直白通俗的解釋吧。
既然這樣,且聽我解釋:愛國版的,或許我們屬於不同的民族,但是我們都是中國人。通俗版的,大家都是文化人兒,為啥就你這麼拽。文化版的,同九年,汝何秀?
再看兩者的實現介面,File實現了Serializable表示可以被序列化,實現了Comparable,表示可以被排序。
Path繼承Comparable,表示可以被排序。繼承Iterable表示可以被遍歷,可以被遍歷是因為Path可以表示目錄。繼承Watchable,表示可以被註冊到WatchService中,進行監控。
檔案中的不同路徑
小師妹:F師兄,File中有好幾個關於Path的get方法,能講一下他們的不同之處嗎?
直接上程式碼:
public void getFilePath() throws IOException {
File file= new File("../../www.flydean.com.txt");
log.info("name is : {}",file.getName());
log.info("path is : {}",file.getPath());
log.info("absolutePath is : {}",file.getAbsolutePath());
log.info("canonicalPath is : {}",file.getCanonicalPath());
}
File中有三個跟Path有關的方法,分別是getPath,getAbsolutePath和getCanonicalPath。
getPath返回的結果就是new File的時候傳入的路徑,輸入什麼返回什麼。
getAbsolutePath返回的是絕對路徑,就是在getPath前面加上了當前的路徑。
getCanonicalPath返回的是精簡後的AbsolutePath,就是去掉了.或者..之類的指代符號。
看下輸出結果:
INFO com.flydean.FilePathUsage - name is : www.flydean.com.txt
INFO com.flydean.FilePathUsage - path is : ../../www.flydean.com.txt
INFO com.flydean.FilePathUsage - absolutePath is : /Users/flydean/learn-java-io-nio/file-path/../../www.flydean.com.txt
INFO com.flydean.FilePathUsage - canonicalPath is : /Users/flydean/www.flydean.com.txt
構建不同的Path
小師妹:F師兄,我記得路徑有相對路徑,絕對路徑等,是不是也有相應的建立Path的方法呢?
當然有的,先看下絕對路徑的建立:
public void getAbsolutePath(){
Path absolutePath = Paths.get("/data/flydean/learn-java-io-nio/file-path", "src/resource","www.flydean.com.txt");
log.info("absolutePath {}",absolutePath );
}
我們可以使用Paths.get方法傳入絕對路徑的地址來構建絕對路徑。
同樣使用Paths.get方法,傳入非絕對路徑可以構建相對路徑。
public void getRelativePath(){
Path RelativePath = Paths.get("src", "resource","www.flydean.com.txt");
log.info("absolutePath {}",RelativePath.toAbsolutePath() );
}
我們還可以從URI中構建Path:
public void getPathfromURI(){
URI uri = URI.create("file:///data/flydean/learn-java-io-nio/file-path/src/resource/www.flydean.com.txt");
log.info("schema {}",uri.getScheme());
log.info("default provider absolutePath {}",FileSystems.getDefault().provider().getPath(uri).toAbsolutePath().toString());
}
也可以從FileSystem構建Path:
public void getPathWithFileSystem(){
Path path1 = FileSystems.getDefault().getPath(System.getProperty("user.home"), "flydean", "flydean.txt");
log.info(path1.toAbsolutePath().toString());
Path path2 = FileSystems.getDefault().getPath("/Users", "flydean", "flydean.txt");
log.info(path2.toAbsolutePath().toString());
}
總結
好多好多Path的建立方法,總有一款適合你。快來挑選吧。
第九章 Buffer和Buff
簡介
小師妹在學習NIO的路上越走越遠,唯一能夠幫到她的就是在她需要的時候給她以全力的支援。什麼都不說了,今天介紹的是NIO的基礎Buffer。老鐵給我上個Buff。
Buffer是什麼
小師妹:F師兄,這個Buffer是我們縱橫王者峽谷中那句:老鐵給我加個Buff的意思嗎?
當然不是了,此Buffer非彼Buff,Buffer是NIO的基礎,沒有Buffer就沒有NIO,沒有Buffer就沒有今天的java。
因為NIO是按Block來讀取資料的,這個一個Block就可以看做是一個Buffer。我們在Buffer中儲存要讀取的資料和要寫入的資料,通過Buffer來提高讀取和寫入的效率。
更多內容請訪問www.flydean.com
還記得java物件的底層儲存單位是什麼嗎?
小師妹:這個我知道,java物件的底層儲存單位是位元組Byte。
對,我們看下Buffer的繼承圖:
Buffer是一個介面,它下面有諸多實現,包括最基本的ByteBuffer和其他的基本型別封裝的其他Buffer。
小師妹:F師兄,有ByteBuffer不就夠了嗎?還要其他的型別Buffer做什麼?
小師妹,山珍再好,也有吃膩的時候,偶爾也要換個蘿蔔白菜啥的,你以為乾隆下江南都幹了些啥?
ByteBuffer雖然好用,但是它畢竟是最小的單位,在它之上我們還有Char,int,Double,Short等等基礎型別,為了簡單起見,我們也給他們都搞一套Buffer。
Buffer進階
小師妹:F師兄,既然Buffer是這些基礎型別的集合,為什麼不直接用結合來表示呢?給他們封裝成一個物件,好像有點多餘。
我們既然在面向物件的世界,從表面來看自然是使用Object比較合乎情理,從底層的本質上看,這些封裝的Buffer包含了一些額外的元資料資訊,並且還提供了一些意想不到的功能。
上圖列出了Buffer中的幾個關鍵的概念,分別是Capacity,Limit,Position和Mark。Buffer底層的本質是陣列,我們以ByteBuffer為例,它的底層是:
final byte[] hb;
- Capacity表示的是該Buffer能夠承載元素的最大數目,這個是在Buffer建立初期就設定的,不可以被改變。
- Limit表示的Buffer中可以被訪問的元素個數,也就是說Buffer中存活的元素個數。
- Position表示的是下一個可以被訪問元素的index,可以通過put和get方法進行自動更新。
- Mark表示的是歷史index,當我們呼叫mark方法的時候,會把設定Mark為當前的position,通過呼叫reset方法把Mark的值恢復到position中。
建立Buffer
小師妹:F師兄呀,這麼多Buffer建立起來是不是很麻煩?有沒有什麼快捷的使用辦法?
一般來說建立Buffer有兩種方法,一種叫做allocate,一種叫做wrap。
public void createBuffer(){
IntBuffer intBuffer= IntBuffer.allocate(10);
log.info("{}",intBuffer);
log.info("{}",intBuffer.hasArray());
int[] intArray=new int[10];
IntBuffer intBuffer2= IntBuffer.wrap(intArray);
log.info("{}",intBuffer2);
IntBuffer intBuffer3= IntBuffer.wrap(intArray,2,5);
log.info("{}",intBuffer3);
intBuffer3.clear();
log.info("{}",intBuffer3);
log.info("{}",intBuffer3.hasArray());
}
allocate可以為Buffer分配一個空間,wrap同樣為Buffer分配一個空間,不同的是這個空間背後的陣列是自定義的,wrap還支援三個引數的方法,後面兩個引數分別是offset和length。
INFO com.flydean.BufferUsage - java.nio.HeapIntBuffer[pos=0 lim=10 cap=10]
INFO com.flydean.BufferUsage - true
INFO com.flydean.BufferUsage - java.nio.HeapIntBuffer[pos=0 lim=10 cap=10]
INFO com.flydean.BufferUsage - java.nio.HeapIntBuffer[pos=2 lim=7 cap=10]
INFO com.flydean.BufferUsage - java.nio.HeapIntBuffer[pos=0 lim=10 cap=10]
INFO com.flydean.BufferUsage - true
hasArray用來判斷該Buffer的底層是不是陣列實現的,可以看到,不管是wrap還是allocate,其底層都是陣列。
需要注意的一點,最後,我們呼叫了clear方法,clear方法呼叫之後,我們發現Buffer的position和limit都被重置了。這說明wrap的三個引數方法設定的只是初始值,可以被重置。
Direct VS non-Direct
小師妹:F師兄,你說了兩種建立Buffer的方法,但是兩種Buffer的後臺都是陣列,難道還有非陣列的Buffer嗎?
自然是有的,但是隻有ByteBuffer有。ByteBuffer有一個allocateDirect方法,可以分配Direct Buffer。
小師妹:Direct和非Direct有什麼區別呢?
Direct Buffer就是說,不需要在使用者空間再複製拷貝一份資料,直接在虛擬地址對映空間中進行操作。這叫Direct。這樣做的好處就是快。缺點就是在分配和銷燬的時候會佔用更多的資源,並且因為Direct Buffer不在使用者空間之內,所以也不受垃圾回收機制的管轄。
所以通常來說只有在資料量比較大,生命週期比較長的資料來使用Direct Buffer。
看下程式碼:
public void createByteBuffer() throws IOException {
ByteBuffer byteBuffer= ByteBuffer.allocateDirect(10);
log.info("{}",byteBuffer);
log.info("{}",byteBuffer.hasArray());
log.info("{}",byteBuffer.isDirect());
try (RandomAccessFile aFile = new RandomAccessFile("src/main/resources/www.flydean.com", "r");
FileChannel inChannel = aFile.getChannel()) {
MappedByteBuffer buffer = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, inChannel.size());
log.info("{}",buffer);
log.info("{}",buffer.hasArray());
log.info("{}",buffer.isDirect());
}
}
除了allocateDirect,使用FileChannel的map方法也可以得到一個Direct的MappedByteBuffer。
上面的例子輸出結果:
INFO com.flydean.BufferUsage - java.nio.DirectByteBuffer[pos=0 lim=10 cap=10]
INFO com.flydean.BufferUsage - false
INFO com.flydean.BufferUsage - true
INFO com.flydean.BufferUsage - java.nio.DirectByteBufferR[pos=0 lim=0 cap=0]
INFO com.flydean.BufferUsage - false
INFO com.flydean.BufferUsage - true
Buffer的日常操作
小師妹:F師兄,看起來Buffer確實有那麼一點複雜,那麼Buffer都有哪些操作呢?
Buffer的操作有很多,下面我們一一來講解。
向Buffer寫資料
向Buffer寫資料可以呼叫Buffer的put方法:
public void putBuffer(){
IntBuffer intBuffer= IntBuffer.allocate(10);
intBuffer.put(1).put(2).put(3);
log.info("{}",intBuffer.array());
intBuffer.put(0,4);
log.info("{}",intBuffer.array());
}
因為put方法返回的還是一個IntBuffer類,所以Buffer的put方法可以像Stream那樣連寫。
同時,我們還可以指定put在什麼位置。上面的程式碼輸出:
INFO com.flydean.BufferUsage - [1, 2, 3, 0, 0, 0, 0, 0, 0, 0]
INFO com.flydean.BufferUsage - [4, 2, 3, 0, 0, 0, 0, 0, 0, 0]
從Buffer讀資料
讀資料使用get方法,但是在get方法之前我們需要呼叫flip方法。
flip方法是做什麼用的呢?上面講到Buffer有個position和limit欄位,position會隨著get或者put的方法自動指向後面一個元素,而limit表示的是該Buffer中有多少可用元素。
如果我們要讀取Buffer的值則會從positon開始到limit結束:
public void getBuffer(){
IntBuffer intBuffer= IntBuffer.allocate(10);
intBuffer.put(1).put(2).put(3);
intBuffer.flip();
while (intBuffer.hasRemaining()) {
log.info("{}",intBuffer.get());
}
intBuffer.clear();
}
可以通過hasRemaining來判斷是否還有下一個元素。通過呼叫clear來清除Buffer,以供下次使用。
rewind Buffer
rewind和flip很類似,不同之處在於rewind不會改變limit的值,只會將position重置為0。
public void rewindBuffer(){
IntBuffer intBuffer= IntBuffer.allocate(10);
intBuffer.put(1).put(2).put(3);
log.info("{}",intBuffer);
intBuffer.rewind();
log.info("{}",intBuffer);
}
上面的結果輸出:
INFO com.flydean.BufferUsage - java.nio.HeapIntBuffer[pos=3 lim=10 cap=10]
INFO com.flydean.BufferUsage - java.nio.HeapIntBuffer[pos=0 lim=10 cap=10]
Compact Buffer
Buffer還有一個compact方法,顧名思義compact就是壓縮的意思,就是把Buffer從當前position到limit的值賦值到position為0的位置:
public void useCompact(){
IntBuffer intBuffer= IntBuffer.allocate(10);
intBuffer.put(1).put(2).put(3);
intBuffer.flip();
log.info("{}",intBuffer);
intBuffer.get();
intBuffer.compact();
log.info("{}",intBuffer);
log.info("{}",intBuffer.array());
}
上面程式碼輸出:
INFO com.flydean.BufferUsage - java.nio.HeapIntBuffer[pos=0 lim=3 cap=10]
INFO com.flydean.BufferUsage - java.nio.HeapIntBuffer[pos=2 lim=10 cap=10]
INFO com.flydean.BufferUsage - [2, 3, 3, 0, 0, 0, 0, 0, 0, 0]
duplicate Buffer
最後我們講一下複製Buffer,有三種方法,duplicate,asReadOnlyBuffer,和slice。
duplicate就是拷貝原Buffer的position,limit和mark,它和原Buffer是共享原始資料的。所以修改了duplicate之後的Buffer也會同時修改原Buffer。
如果用asReadOnlyBuffer就不允許拷貝之後的Buffer進行修改。
slice也是readOnly的,不過它拷貝的是從原Buffer的position到limit-position之間的部分。
public void duplicateBuffer(){
IntBuffer intBuffer= IntBuffer.allocate(10);
intBuffer.put(1).put(2).put(3);
log.info("{}",intBuffer);
IntBuffer duplicateBuffer=intBuffer.duplicate();
log.info("{}",duplicateBuffer);
IntBuffer readOnlyBuffer=intBuffer.asReadOnlyBuffer();
log.info("{}",readOnlyBuffer);
IntBuffer sliceBuffer=intBuffer.slice();
log.info("{}",sliceBuffer);
}
輸出結果:
INFO com.flydean.BufferUsage - java.nio.HeapIntBuffer[pos=3 lim=10 cap=10]
INFO com.flydean.BufferUsage - java.nio.HeapIntBuffer[pos=3 lim=10 cap=10]
INFO com.flydean.BufferUsage - java.nio.HeapIntBufferR[pos=3 lim=10 cap=10]
INFO com.flydean.BufferUsage - java.nio.HeapIntBuffer[pos=0 lim=7 cap=7]
總結
今天給小師妹介紹了Buffer的原理和基本操作。
第十章 File copy和File filter
簡介
一個linux命令的事情,小師妹非要讓我教她怎麼用java來實現,哎,攤上個這麼槓精的小師妹,我也是深感無力,做一個師兄真的好難。
使用java拷貝檔案
今天小師妹找到我了:F師兄,能告訴怎麼拷貝檔案嗎?
拷貝檔案?不是很簡單的事情嗎?如果你有了檔案的讀許可權,只需要這樣就可以了。
cp www.flydean.com www.flydean.com.back
當然,如果是目錄的話還可以加兩個引數遍歷和強制拷貝:
cp -rf srcDir distDir
這麼簡單的linux命令,不要告訴我你不會。
小師妹笑了:F師兄,我不要用linux命令,我就想用java來實現,我不正在學java嗎?學一門當然要找準機會來練習啦,快快教教我吧。
既然這樣,那我就開講了。java中檔案的拷貝其實也有三種方法,可以使用傳統的檔案讀寫的方法,也可以使用最新的NIO中提供的拷貝方法。
使用傳統方法當然沒有NIO快,也沒有NIO簡潔,我們先來看看怎麼使用傳統的檔案讀寫的方法來拷貝檔案:
public void copyWithFileStreams() throws IOException
{
File fileToCopy = new File("src/main/resources/www.flydean.com");
File newFile = new File("src/main/resources/www.flydean.com.back");
newFile.createNewFile();
try(FileOutputStream output = new FileOutputStream(newFile);FileInputStream input = new FileInputStream(fileToCopy)){
byte[] buf = new byte[1024];
int bytesRead;
while ((bytesRead = input.read(buf)) > 0)
{
output.write(buf, 0, bytesRead);
}
}
}
上面的例子中,我們首先定義了兩個檔案,然後從兩個檔案中生成了OutputStream和InputStream,最後以位元組流的形式從input中讀出資料到outputStream中,最終完成了檔案的拷貝。
傳統的File IO拷貝比較繁瑣,速度也比較慢。我們接下來看看怎麼使用NIO來完成這個過程:
public void copyWithNIOChannel() throws IOException
{
File fileToCopy = new File("src/main/resources/www.flydean.com");
File newFile = new File("src/main/resources/www.flydean.com.back");
try(FileInputStream inputStream = new FileInputStream(fileToCopy);FileOutputStream outputStream = new FileOutputStream(newFile)){
FileChannel inChannel = inputStream.getChannel();
FileChannel outChannel = outputStream.getChannel();
inChannel.transferTo(0, fileToCopy.length(), outChannel);
}
}
之前我們講到NIO中一個非常重要的概念就是channel,通過構建原始檔和目標檔案的channel通道,可以直接在channel層面進行拷貝,如上面的例子所示,我們呼叫了inChannel.transferTo完成了拷貝。
最後,還有一個更簡單的NIO檔案拷貝的方法:
public void copyWithNIOFiles() throws IOException
{
Path source = Paths.get("src/main/resources/www.flydean.com");
Path destination = Paths.get("src/main/resources/www.flydean.com.back");
Files.copy(source, destination, StandardCopyOption.REPLACE_EXISTING);
}
直接使用工具類Files提供的copy方法即可。
使用File filter
太棒了,小師妹一臉崇拜:F師兄,我還有一個需求,就是想刪除某個目錄裡面的以.log結尾的日誌檔案,這個需求是不是很常見?F師兄一般是怎麼操作的?
一般這種操作我都是一個linux命令就搞定了,如果搞不定那就用兩個:
rm -rf *.log
當然,如果需要,我們也是可以用java來實現的。
java中提供了兩個Filter都可以用來實現這個功能。
這兩個Filter是java.io.FilenameFilter和java.io.FileFilter:
@FunctionalInterface
public interface FilenameFilter {
boolean accept(File dir, String name);
}
@FunctionalInterface
public interface FileFilter {
boolean accept(File pathname);
}
這兩個介面都是函式式介面,所以他們的實現可以直接用lambda表示式來代替。
兩者的區別在於,FilenameFilter進行過濾的是檔名和檔案所在的目錄。而FileFilter進行過濾的直接就是目標檔案。
在java中是沒有目錄的概念的,一個目錄也是用File的表示的。
上面的兩個使用起來非常類似,我們就以FilenameFilter為例,看下怎麼刪除.log檔案:
public void useFileNameFilter()
{
String targetDirectory = "src/main/resources/";
File directory = new File(targetDirectory);
//Filter out all log files
String[] logFiles = directory.list( (dir, fileName)-> fileName.endsWith(".log"));
//If no log file found; no need to go further
if (logFiles.length == 0)
return;
//This code will delete all log files one by one
for (String logfile : logFiles)
{
String tempLogFile = targetDirectory + File.separator + logfile;
File fileDelete = new File(tempLogFile);
boolean isdeleted = fileDelete.delete();
log.info("file : {} is deleted : {} ", tempLogFile , isdeleted);
}
}
上面的例子中,我們通過directory.list方法,傳入lambda表示式建立的Filter,實現了過濾的效果。
最後,我們將過濾之後的檔案刪除。實現了目標。
總結
小師妹的兩個問題解決了,希望今天可以不要再見到她。
第十一章 NIO中Channel的妙用
簡介
小師妹,你還記得我們使用IO和NIO的初心嗎?
小師妹:F師兄,使用IO和NIO不就是為了讓生活更美好,世界充滿愛嗎?讓我等程式設計師可以優雅的將資料從一個地方搬運到另外一個地方。利其器,善其事,才有更多的時間去享受生活呀。
善,如果將資料比做人,IO,NIO的目的就是把人運到美國。
小師妹:F師兄,為什麼要運到美國呀,美國現在新冠太嚴重了,還是待在中國吧。中國是世界上最安全的國家!
好吧,為了保險起見,我們要把人運到上海。人就是資料,怎麼運過去呢?可以坐飛機,坐汽車,坐火車,這些什麼飛機,汽車,火車就可以看做是一個一個的Buffer。
最後飛機的航線,汽車的公路和火車的軌道就可以看做是一個個的channel。
簡單點講,channel就是負責運送Buffer的通道。
IO按源頭來分,可以分為兩種,從檔案來的File IO,從Stream來的Stream IO。不管哪種IO,都可以通過channel來運送資料。
Channel的分類
雖然資料的來源只有兩種,但是JDK中Channel的分類可不少,如下圖所示:
先來看看最基本的,也是最頂層的介面Channel:
public interface Channel extends Closeable {
public boolean isOpen();
public void close() throws IOException;
}
最頂層的Channel很簡單,繼承了Closeable介面,需要實現兩個方法isOpen和close。
一個用來判斷channel是否開啟,一個用來關閉channel。
小師妹:F師兄,頂層的Channel怎麼這麼簡單,完全不符合Channel很複雜的人設啊。
別急,JDK這麼做其實也是有道理的,因為是頂層的介面,必須要更加抽象更加通用,結果,一通用就發現還真的就只有這麼兩個方法是通用的。
所以為了應對這個問題,Channel中定義了很多種不同的型別。
最最底層的Channel有5大型別,分別是:
FileChannel
這5大channel中,和檔案File有關的就是這個FileChannel了。
FileChannel可以從RandomAccessFile, FileInputStream或者FileOutputStream中通過呼叫getChannel()來得到。
也可以直接呼叫FileChannel中的open方法傳入Path建立。
public abstract class FileChannel
extends AbstractInterruptibleChannel
implements SeekableByteChannel, GatheringByteChannel, ScatteringByteChannel
我們看下FileChannel繼承或者實現的介面和類。
AbstractInterruptibleChannel實現了InterruptibleChannel介面,interrupt大家都知道吧,用來中斷執行緒執行的利器。來看一下下面一段非常玄妙的程式碼:
protected final void begin() {
if (interruptor == null) {
interruptor = new Interruptible() {
public void interrupt(Thread target) {
synchronized (closeLock) {
if (closed)
return;
closed = true;
interrupted = target;
try {
AbstractInterruptibleChannel.this.implCloseChannel();
} catch (IOException x) { }
}
}};
}
blockedOn(interruptor);
Thread me = Thread.currentThread();
if (me.isInterrupted())
interruptor.interrupt(me);
}
上面這段程式碼就是AbstractInterruptibleChannel的核心所在。
首先定義了一個Interruptible的例項,這個例項中有一個interrupt方法,用來關閉Channel。
然後獲得當前執行緒的例項,判斷當前執行緒是否Interrupted,如果是的話,就呼叫Interruptible的interrupt方法將當前channel關閉。
SeekableByteChannel用來連線Entry或者File。它有一個獨特的屬性叫做position,表示當前讀取的位置。可以被修改。
GatheringByteChannel和ScatteringByteChannel表示可以一次讀寫一個Buffer序列結合(Buffer Array):
public long write(ByteBuffer[] srcs, int offset, int length)
throws IOException;
public long read(ByteBuffer[] dsts, int offset, int length)
throws IOException;
Selector和Channel
在講其他幾個Channel之前,我們看一個和下面幾個channel相關的Selector:
這裡要介紹一個新的Channel型別叫做SelectableChannel,之前的FileChannel的連線是一對一的,也就是說一個channel要對應一個處理的執行緒。而SelectableChannel則是一對多的,也就是說一個處理執行緒可以通過Selector來對應處理多個channel。
SelectableChannel通過註冊不同的SelectionKey,實現對多個Channel的監聽。後面我們會具體的講解Selector的使用,敬請期待。
DatagramChannel
DatagramChannel是用來處理UDP的Channel。它自帶了Open方法來建立例項。
來看看DatagramChannel的定義:
public abstract class DatagramChannel
extends AbstractSelectableChannel
implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, MulticastChannel
ByteChannel表示它同時是ReadableByteChannel也是WritableByteChannel,可以同時寫入和讀取。
MulticastChannel代表的是一種多播協議。正好和UDP對應。
SocketChannel
SocketChannel是用來處理TCP的channel。它也是通過Open方法來建立的。
public abstract class SocketChannel
extends AbstractSelectableChannel
implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, NetworkChannel
SocketChannel跟DatagramChannel的唯一不同之處就是實現的是NetworkChannel藉口。
NetworkChannel提供了一些network socket的操作,比如繫結地址等。
ServerSocketChannel
ServerSocketChannel也是一個NetworkChannel,它主要用在伺服器端的監聽。
public abstract class ServerSocketChannel
extends AbstractSelectableChannel
implements NetworkChannel
AsynchronousSocketChannel
最後AsynchronousSocketChannel是一種非同步的Channel:
public abstract class AsynchronousSocketChannel
implements AsynchronousByteChannel, NetworkChannel
為什麼是非同步呢?我們看一個方法:
public abstract Future<Integer> read(ByteBuffer dst);
可以看到返回值是一個Future,所以read方法可以立刻返回,只在我們需要的時候從Future中取值即可。
使用Channel
小師妹:F師兄,講了這麼多種類的Channel,看得我眼花繚亂,能不能講一個Channel的具體例子呢?
好的小師妹,我們現在講一個使用Channel進行檔案拷貝的例子,雖然Channel提供了transferTo的方法可以非常簡單的進行拷貝,但是為了能夠看清楚Channel的通用使用,我們選擇一個更加常規的例子:
public void useChannelCopy() throws IOException {
FileInputStream input = new FileInputStream ("src/main/resources/www.flydean.com");
FileOutputStream output = new FileOutputStream ("src/main/resources/www.flydean.com.txt");
try(ReadableByteChannel source = input.getChannel(); WritableByteChannel dest = output.getChannel()){
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
while (source.read(buffer) != -1)
{
// flip buffer,準備寫入
buffer.flip();
// 檢視是否有更多的內容
while (buffer.hasRemaining())
{
dest.write(buffer);
}
// clear buffer,供下一次使用
buffer.clear();
}
}
}
上面的例子中我們從InputStream中讀取Buffer,然後寫入到FileOutputStream。
總結
今天講解了Channel的具體分類,和一個簡單的例子,後面我們會再體驗一下Channel的其他例子,敬請期待。
第十二章 MappedByteBuffer多大的檔案我都裝得下
簡介
大大大,我要大!小師妹要讀取的檔案越來越大,該怎麼幫幫她,讓程式在效能和速度上面得到平衡呢?快來跟F師兄一起看看吧。
虛擬地址空間
小師妹:F師兄,你有沒有發現,最近硬碟的價格真的是好便宜好便宜,1T的硬碟大概要500塊,平均1M五毛錢。現在下個電影都1G起步,這是不是意味著我們買入了大資料時代?
沒錯,小師妹,硬體技術的進步也帶來了軟體技術的進步,兩者相輔相成,缺一不可。
小師妹:F師兄,如果要是去讀取G級的檔案,有沒有什麼快捷簡單的方法?
還記得上次我們講的虛擬地址空間嗎?
再把上次講的圖搬過來:
通常來說我們的應用程式呼叫系統的介面從磁碟空間獲取Buffer資料,我們把自己的應用程式稱之為使用者空間,把系統的底層稱之為系統空間。
傳統的IO操作,是作業系統講磁碟中的檔案讀入到系統空間裡面,然後再拷貝到使用者空間中,供使用者使用。
這中間多了一個Buffer拷貝的過程,如果這個量夠大的話,其實還是挺浪費時間的。
於是有人在想了,拷貝太麻煩太耗時了,我們單獨劃出一塊記憶體區域,讓系統空間和使用者空間同時對映到同一塊地址不就省略了拷貝的步驟嗎?
這個被劃出來的單獨的記憶體區域叫做虛擬地址空間,而不同空間到虛擬地址的對映就叫做Buffer Map。 Java中是有一個專門的MappedByteBuffer來代表這種操作。
小師妹:F師兄,那這個虛擬地址空間和記憶體有什麼區別呢?有了記憶體還要啥虛擬地址空間?
虛擬地址空間有兩個好處。
第一個好處就是虛擬地址空間對於應用程式本身而言是獨立的,從而保證了程式的互相隔離和程式中地址的確定性。比如說一個程式如果執行在虛擬地址空間中,那麼它的空間地址是固定的,不管他執行多少次。如果直接使用記憶體地址,那麼可能這次執行的時候記憶體地址可用,下次執行的時候記憶體地址不可用,就會導致潛在的程式出錯。
第二個好處就是虛擬空間地址可以比真實的記憶體地址大,這個大其實是對記憶體的使用做了優化,比如說會把很少使用的記憶體寫如磁碟,從而釋放出更多的記憶體來做更有意義的事情,而之前儲存到磁碟的資料,當真正需要的時候,再從磁碟中載入到記憶體中。
這樣實體記憶體實際上可以看做虛擬空間地址的快取。
詳解MappedByteBuffer
小師妹:MappedByteBuffer聽起來好神奇,怎麼使用它呢?
我們先來看看MappedByteBuffer的定義:
public abstract class MappedByteBuffer
extends ByteBuffer
它實際上是一個抽象類,具體的實現有兩個:
class DirectByteBuffer extends MappedByteBuffer implements DirectBuffer
class DirectByteBufferR extends DirectByteBuffer
implements DirectBuffer
分別是DirectByteBuffer和DirectByteBufferR。
小師妹:F師兄,這兩個ByteBuffer有什麼區別呢?這個R是什麼意思?
R代表的是ReadOnly的意思,可能是因為本身是個類的名字就夠長了,所以搞了個縮寫。但是也不寫個註解,讓人看起來十分費解....
我們可以從RandomAccessFile的FilChannel中呼叫map方法獲得它的例項。
我們看下map方法的定義:
public abstract MappedByteBuffer map(MapMode mode, long position, long size)
throws IOException;
MapMode代表的是對映的模式,position表示是map開始的地址,size表示是ByteBuffer的大小。
MapMode
小師妹:F師兄,檔案有隻讀,讀寫兩種模式,是不是MapMode也包含這兩類?
對的,其實NIO中的MapMode除了這兩個之外,還有一些其他很有趣的用法。
- FileChannel.MapMode.READ_ONLY 表示只讀模式
- FileChannel.MapMode.READ_WRITE 表示讀寫模式
- FileChannel.MapMode.PRIVATE 表示copy-on-write模式,這個模式和READ_ONLY有點相似,它的操作是先對原資料進行拷貝,然後可以在拷貝之後的Buffer中進行讀寫。但是這個寫入並不會影響原資料。可以看做是資料的本地拷貝,所以叫做Private。
基本的MapMode就這三種了,其實除了基礎的MapMode,還有兩種擴充套件的MapMode:
- ExtendedMapMode.READ_ONLY_SYNC 同步的讀
- ExtendedMapMode.READ_WRITE_SYNC 同步的讀寫
MappedByteBuffer的最大值
小師妹:F師兄,既然可以對映到虛擬記憶體空間,那麼這個MappedByteBuffer是不是可以無限大?
當然不是了,首先虛擬地址空間的大小是有限制的,如果是32位的CPU,那麼一個指標佔用的地址就是4個位元組,那麼能夠表示的最大值是0xFFFFFFFF,也就是4G。
另外我們看下map方法中size的型別是long,在java中long能夠表示的最大值是0x7fffffff,也就是2147483647位元組,換算一下大概是2G。也就是說MappedByteBuffer的最大值是2G,一次最多隻能map 2G的資料。
MappedByteBuffer的使用
小師妹,F師兄我們來舉兩個使用MappedByteBuffer讀寫的例子吧。
善!
先看一下怎麼使用MappedByteBuffer來讀資料:
public void readWithMap() throws IOException {
try (RandomAccessFile file = new RandomAccessFile(new File("src/main/resources/big.www.flydean.com"), "r"))
{
//get Channel
FileChannel fileChannel = file.getChannel();
//get mappedByteBuffer from fileChannel
MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());
// check buffer
log.info("is Loaded in physical memory: {}",buffer.isLoaded()); //只是一個提醒而不是guarantee
log.info("capacity {}",buffer.capacity());
//read the buffer
for (int i = 0; i < buffer.limit(); i++)
{
log.info("get {}", buffer.get());
}
}
}
然後再看一個使用MappedByteBuffer來寫資料的例子:
public void writeWithMap() throws IOException {
try (RandomAccessFile file = new RandomAccessFile(new File("src/main/resources/big.www.flydean.com"), "rw"))
{
//get Channel
FileChannel fileChannel = file.getChannel();
//get mappedByteBuffer from fileChannel
MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 4096 * 8 );
// check buffer
log.info("is Loaded in physical memory: {}",buffer.isLoaded()); //只是一個提醒而不是guarantee
log.info("capacity {}",buffer.capacity());
//write the content
buffer.put("www.flydean.com".getBytes());
}
}
MappedByteBuffer要注意的事項
小師妹:F師兄,MappedByteBuffer因為使用了記憶體對映,所以讀寫的速度都會有所提升。那麼我們在使用中應該注意哪些問題呢?
MappedByteBuffer是沒有close方法的,即使它的FileChannel被close了,MappedByteBuffer仍然處於開啟狀態,只有JVM進行垃圾回收的時候才會被關閉。而這個時間是不確定的。
總結
本文再次介紹了虛擬地址空間和MappedByteBuffer的使用。
第十三章 NIO中那些奇怪的Buffer
簡介
妖魔鬼怪快快顯形,今天F師兄幫助小師妹來斬妖除魔啦,什麼BufferB,BufferL,BufferRB,BufferRL,BufferS,BufferU,BufferRS,BufferRU統統給你剖析個清清楚楚明明白白。
Buffer的分類
小師妹:F師兄不都說JDK原始碼是最好的java老師嗎?為程不識原始碼,就稱牛人也枉然。但是我最近在學習NIO的時候竟然發現有些Buffer類居然沒有註釋,就那麼突兀的寫在哪裡,讓人好生心煩。
更多內容請訪問www.flydean.com
居然還有這樣的事情?快帶F師兄去看看。
小師妹:F師兄你看,以ShortBuffer為例,它的子類怎麼後面都帶一些奇奇怪怪的字元:
什麼什麼BufferB,BufferL,BufferRB,BufferRL,BufferS,BufferU,BufferRS,BufferRU都來了,點進去看他們的原始碼也沒有說明這些類到底是做什麼的。
還真有這種事情,給我一個小時,讓我仔細研究研究。
一個小時後,小師妹,經過我一個小時的辛苦勘察,結果發現,確實沒有官方文件介紹這幾個類到底是什麼含義,但是師兄我掐指一算,好像發現了這些類之間的小祕密,且聽為兄娓娓道來。
之前的文章,我們講到Buffer根據型別可以分為ShortBuffer,LongBuffer,DoubleBuffer等等。
但是根據本質和使用習慣,我們又可以分為三類,分別是:ByteBufferAsXXXBuffer,DirectXXXBuffer和HeapXXXBuffer。
ByteBufferAsXXXBuffer主要將ByteBuffer轉換成為特定型別的Buffer,比如CharBuffer,IntBuffer等等。
而DirectXXXBuffer則是和虛擬記憶體對映打交道的Buffer。
最後HeapXXXBuffer是在堆空間上面建立的Buffer。
Big Endian 和 Little Endian
小師妹,F師兄,你剛剛講的都不重要,我就想知道類後面的B,L,R,S,U是做什麼的。
好吧,在給你講解這些內容之前,師兄我給你講一個故事。
話說在明末浙江才女吳絳雪寫過一首詩:《春 景 詩》
鶯啼岸柳弄春晴,
柳弄春晴夜月明。
明月夜晴春弄柳,
晴春弄柳岸啼鶯。
小師妹,可有看出什麼特異之處?最好是多讀幾遍,讀出聲來。
小師妹:哇,F師兄,這首詩從頭到尾和從尾到頭讀起來是一樣的呀,又對稱又有意境!
不錯,這就是中文的魅力啦,根據讀的方式不同,得出的結果也不同,其實在計算機世界也存在這樣的問題。
我們知道在java中底層的最小儲存單元是Byte,一個Byte是8bits,用16進製表示就是Ox00-OxFF。
java中除了byte,boolean是佔一個位元組以外,好像其他的型別都會佔用多個位元組。
如果以int來舉例,int佔用4個位元組,其範圍是從Ox00000000-OxFFFFFFFF,假如我們有一個int=Ox12345678,存到記憶體地址裡面就有這樣兩種方式。
第一種Big Endian將高位的位元組儲存在起始地址
第二種Little Endian將地位的位元組儲存在起始地址
其實Big Endian更加符合人類的讀寫習慣,而Little Endian更加符合機器的讀寫習慣。
目前主流的兩大CPU陣營中,PowerPC系列採用big endian方式儲存資料,而x86系列則採用little endian方式儲存資料。
如果不同的CPU架構直接進行通訊,就由可能因為讀取順序的不同而產生問題。
java的設計初衷就是一次編寫處處執行,所以自然也做了設計。
所以BufferB表示的是Big Endian的buffer,BufferL表示的是Little endian的Buffer。
而BufferRB,BufferRL表示的是兩種只讀Buffer。
aligned記憶體對齊
小師妹:F師兄,那這幾個又是做什麼用的呢? BufferS,BufferU,BufferRS,BufferRU。
在講解這幾個類之前,我們先要回顧一下JVM中物件的儲存方式。
還記得我們是怎麼使用JOL來分析JVM的資訊的嗎?程式碼非常非常簡單:
log.info("{}", VM.current().details());
輸出結果:
## Running 64-bit HotSpot VM.
## Using compressed oop with 3-bit shift.
## Using compressed klass with 3-bit shift.
## WARNING | Compressed references base/shifts are guessed by the experiment!
## WARNING | Therefore, computed addresses are just guesses, and ARE NOT RELIABLE.
## WARNING | Make sure to attach Serviceability Agent to get the reliable addresses.
## Objects are 8 bytes aligned.
## Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
## Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
上面的輸出中,我們可以看到:Objects are 8 bytes aligned,這意味著所有的物件分配的位元組都是8的整數倍。
再注意上面輸出的一個關鍵字aligned,確認過眼神,是對的那個人。
aligned對齊的意思,表示JVM中的物件都是以8位元組對齊的,如果物件本身佔用的空間不足8位元組或者不是8位元組的倍數,則補齊。
還是用JOL來分析String物件:
[main] INFO com.flydean.JolUsage - java.lang.String object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 12 (object header) N/A
12 4 byte[] String.value N/A
16 4 int String.hash N/A
20 1 byte String.coder N/A
21 1 boolean String.hashIsZero N/A
22 2 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 2 bytes external = 2 bytes total
可以看到一個String物件佔用24位元組,但是真正有意義的是22位元組,有兩個2位元組是補齊用的。
對齊的好處顯而易見,就是CPU在讀取資料的時候更加方便和快捷,因為CPU設定是一次讀取多少位元組來的,如果你儲存是沒有對齊的,則CPU讀取起來效率會比較低。
現在可以回答部分問題:BufferU表示是unaligned,BufferRU表示是隻讀的unaligned。
小師妹:那BufferS和BufferRS呢?
這個問題其實還是很難回答的,但是經過師兄我的不斷研究和探索,終於找到了答案:
先看下DirectShortBufferRU和DirectShortBufferRS的區別,兩者的區別在兩個地方,先看第一個Order:
DirectShortBufferRU:
public ByteOrder order() {
return ((ByteOrder.nativeOrder() != ByteOrder.BIG_ENDIAN)
? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN);
}
DirectShortBufferRS:
public ByteOrder order() {
return ((ByteOrder.nativeOrder() == ByteOrder.BIG_ENDIAN)
? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN);
}
可以看到DirectShortBufferRU的Order是跟nativeOrder是一致的。而DirectShortBufferRS的Order跟nativeOrder是相反的。
為什麼相反?再看兩者get方法的不同:
DirectShortBufferU:
public short get() {
try {
checkSegment();
return ((UNSAFE.getShort(ix(nextGetIndex()))));
} finally {
Reference.reachabilityFence(this);
}
}
DirectShortBufferS:
public short get() {
try {
checkSegment();
return (Bits.swap(UNSAFE.getShort(ix(nextGetIndex()))));
} finally {
Reference.reachabilityFence(this);
}
}
區別出來了,DirectShortBufferS在返回的時候做了一個bits的swap操作。
所以BufferS表示的是swap過後的Buffer,和BufferRS表示的是隻讀的swap過後的Buffer。
總結
不寫註釋實在是害死人啊!尤其是JDK自己也不寫註釋的情況下!
第十四章 用Selector來說再見
簡介
NIO有三寶:Buffer,Channel,Selector少不了。本文將會介紹NIO三件套中的最後一套Selector,並在理解Selector的基礎上,協助小師妹發一張好人卡。我們開始吧。
Selector介紹
小師妹:F師兄,最近我的桃花有點旺,好幾個師兄莫名其妙的跟我打招呼,可是我一心向著工作,不想談論這些事情。畢竟先有事業才有家嘛。我又不好直接拒絕,有沒有什麼比較隱晦的方法來讓他們放棄這個想法?
更多內容請訪問www.flydean.com
這個問題,我沉思了大約0.001秒,於是給出了答案:給他們發張好人卡吧,應該就不會再來糾纏你了。
小師妹:F師兄,如果給他們發完好人卡還沒有用呢?
那就只能切斷跟他們的聯絡了,來個一刀兩斷。哈哈。
這樣吧,小師妹你最近不是在學NIO嗎?剛好我們可以用Selector來模擬一下發好人卡的過程。
假如你的志偉師兄和子丹師兄想跟你建立聯絡,每個人都想跟你建立一個溝通通道,那麼你就需要建立兩個channel。
兩個channel其實還好,如果有多個人都想同時跟你建立聯絡通道,那麼要維持這些通道就需要保持連線,從而浪費了資源。
但是建立的這些連線並不是時時刻刻都有訊息在傳輸,所以其實大多數時間這些建立聯絡的通道其實是浪費的。
如果使用Selector就可以只啟用一個執行緒來監聽通道的訊息變動,這就是Selector。
從上面的圖可以看出,Selector監聽三個不同的channel,然後交給一個processor來處理,從而節約了資源。
建立Selector
先看下selector的定義:
public abstract class Selector implements Closeable
Selector是一個abstract類,並且實現了Closeable,表示Selector是可以被關閉的。
雖然Selector是一個abstract類,但是可以通過open來簡單的建立:
Selector selector = Selector.open();
如果細看open的實現可以發現一個很有趣的現象:
public static Selector open() throws IOException {
return SelectorProvider.provider().openSelector();
}
open方法呼叫的是SelectorProvider中的openSelector方法。
再看下provider的實現:
public SelectorProvider run() {
if (loadProviderFromProperty())
return provider;
if (loadProviderAsService())
return provider;
provider = sun.nio.ch.DefaultSelectorProvider.create();
return provider;
}
});
有三種情況可以載入一個SelectorProvider,如果系統屬性指定了java.nio.channels.spi.SelectorProvider,那麼從指定的屬性載入。
如果沒有直接指定屬性,則從ServiceLoader來載入。
最後如果都找不到的情況下,使用預設的DefaultSelectorProvider。
關於ServiceLoader的用法,我們後面會有專門的文章來講述。這裡先不做多的解釋。
註冊Selector到Channel中
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress("localhost", 9527));
serverSocketChannel.configureBlocking(false);
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
如果是在伺服器端,我們需要先建立一個ServerSocketChannel,繫結Server的地址和埠,然後將Blocking設定為false。因為我們使用了Selector,它實際上是一個非阻塞的IO。
注意FileChannels是不能使用Selector的,因為它是一個阻塞型IO。
小師妹:F師兄,為啥FileChannel是阻塞型的呀?做成非阻塞型的不是更快?
小師妹,我們使用FileChannel的目的是什麼?就是為了讀檔案呀,讀取檔案肯定是一直讀一直讀,沒有可能讀一會這個channel再讀另外一個channel吧,因為對於每個channel自己來講,在檔案沒讀取完之前,都是繁忙狀態,沒有必要在channel中切換。
最後我們將建立好的Selector註冊到channel中去。
SelectionKey
SelectionKey表示的是我們希望監聽到的事件。
總的來說,有4種Event:
- SelectionKey.OP_READ 表示伺服器準備好,可以從channel中讀取資料。
- SelectionKey.OP_WRITE 表示伺服器準備好,可以向channel中寫入資料。
- SelectionKey.OP_CONNECT 表示客戶端嘗試去連線服務端
- SelectionKey.OP_ACCEPT 表示伺服器accept一個客戶端的請求
public static final int OP_READ = 1 << 0;
public static final int OP_WRITE = 1 << 2;
public static final int OP_CONNECT = 1 << 3;
public static final int OP_ACCEPT = 1 << 4;
我們可以看到上面的4個Event是用位運算來定義的,如果將這個四個event使用或運算合併起來,就得到了SelectionKey中的interestOps。
和interestOps類似,SelectionKey還有一個readyOps。
一個表示感興趣的操作,一個表示ready的操作。
最後,SelectionKey在註冊的時候,還可以attach一個Object,比如我們可以在這個物件中儲存這個channel的id:
SelectionKey key = channel.register(
selector, SelectionKey.OP_ACCEPT, object);
key.attach(Object);
Object object = key.attachment();
object可以在register的時候傳入,也可以呼叫attach方法。
最後,我們可以通過key的attachment方法,獲得該物件。
selector 和 SelectionKey
我們通過selector.select()這個一個blocking操作,來獲取一個ready的channel。
然後我們通過呼叫selector.selectedKeys()來獲取到SelectionKey物件。
在SelectionKey物件中,我們通過判斷ready的event來處理相應的訊息。
總的例子
接下來,我們把之前將的串聯起來,先建立一個小師妹的ChatServer:
public class ChatServer {
private static String BYE_BYE="再見";
public static void main(String[] args) throws IOException, InterruptedException {
Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress("localhost", 9527));
serverSocketChannel.configureBlocking(false);
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
ByteBuffer byteBuffer = ByteBuffer.allocate(512);
while (true) {
selector.select();
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iter = selectedKeys.iterator();
while (iter.hasNext()) {
SelectionKey selectionKey = iter.next();
if (selectionKey.isAcceptable()) {
register(selector, serverSocketChannel);
}
if (selectionKey.isReadable()) {
serverResonse(byteBuffer, selectionKey);
}
iter.remove();
}
Thread.sleep(1000);
}
}
private static void serverResonse(ByteBuffer byteBuffer, SelectionKey selectionKey)
throws IOException {
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
socketChannel.read(byteBuffer);
byteBuffer.flip();
byte[] bytes= new byte[byteBuffer.limit()];
byteBuffer.get(bytes);
log.info(new String(bytes).trim());
if(new String(bytes).trim().equals(BYE_BYE)){
log.info("說再見不如不見!");
socketChannel.write(ByteBuffer.wrap("再見".getBytes()));
socketChannel.close();
}else {
socketChannel.write(ByteBuffer.wrap("你是個好人".getBytes()));
}
byteBuffer.clear();
}
private static void register(Selector selector, ServerSocketChannel serverSocketChannel)
throws IOException {
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
}
}
上面例子有兩點需要注意,我們在迴圈遍歷中,當selectionKey.isAcceptable時,表示伺服器收到了一個新的客戶端連線,這個時候我們需要呼叫register方法,再註冊一個OP_READ事件到這個新的SocketChannel中,然後繼續遍歷。
第二,我們定義了一個stop word,當收到這個stop word的時候,會直接關閉這個client channel。
再看看客戶端的程式碼:
public class ChatClient {
private static SocketChannel socketChannel;
private static ByteBuffer byteBuffer;
public static void main(String[] args) throws IOException {
ChatClient chatClient = new ChatClient();
String response = chatClient.sendMessage("hello 小師妹!");
log.info("response is {}", response);
response = chatClient.sendMessage("能不能?");
log.info("response is {}", response);
chatClient.stop();
}
public void stop() throws IOException {
socketChannel.close();
byteBuffer = null;
}
public ChatClient() throws IOException {
socketChannel = SocketChannel.open(new InetSocketAddress("localhost", 9527));
byteBuffer = ByteBuffer.allocate(512);
}
public String sendMessage(String msg) throws IOException {
byteBuffer = ByteBuffer.wrap(msg.getBytes());
String response = null;
socketChannel.write(byteBuffer);
byteBuffer.clear();
socketChannel.read(byteBuffer);
byteBuffer.flip();
byte[] bytes= new byte[byteBuffer.limit()];
byteBuffer.get(bytes);
response =new String(bytes).trim();
byteBuffer.clear();
return response;
}
}
客戶端程式碼沒什麼特別的,需要注意的是Buffer的讀取。
最後輸出結果:
server收到: INFO com.flydean.ChatServer - hello 小師妹!
client收到: INFO com.flydean.ChatClient - response is 你是個好人
server收到: INFO com.flydean.ChatServer - 能不能?
client收到: INFO com.flydean.ChatClient - response is 再見
解釋一下整個流程:志偉跟小師妹建立了一個連線,志偉向小師妹打了一個招呼,小師妹給志偉發了一張好人卡。志偉不死心,想繼續糾纏,小師妹回覆再見,然後自己關閉了通道。
總結
本文介紹了Selector和channel在發好人卡的過程中的作用。
第十五章 檔案編碼和字符集Unicode
簡介
小師妹一時興起,使用了一項從來都沒用過的新技能,沒想卻出現了一個無法解決的問題。把大象裝進冰箱到底有幾步?亂碼的問題又是怎麼解決的?快來跟F師兄一起看看吧。
使用Properties讀取檔案
這天,小師妹心情很愉悅,吹著口哨唱著歌,標準的45度俯視讓人好不自在。
小師妹呀,什麼事情這麼高興,說出來讓師兄也沾點喜慶?
小師妹:F師兄,最新我發現了一種新型的讀取檔案的方法,很好用的,就跟map一樣:
public void usePropertiesFile() throws IOException {
Properties configProp = new Properties();
InputStream in = this.getClass().getClassLoader().getResourceAsStream("www.flydean.com.properties");
configProp.load(in);
log.info(configProp.getProperty("name"));
configProp.setProperty("name", "www.flydean.com");
log.info(configProp.getProperty("name"));
}
F師兄你看,我使用了Properties來讀取檔案,檔案裡面的內容是key=value形式的,在做配置檔案使用的時候非常恰當。我是從Spring專案中的properties配置檔案中得到的靈感,才發現原來java還有一個專門讀取屬性檔案的類Properties。
小師妹現在都會搶答了,果然青出於藍。
亂碼初現
小師妹你做得非常好,就這樣觸類旁通,很快java就要盡歸你手了,後面的什麼scala,go,JS等估計也統統不在話下。再過幾年你就可以升任架構師,公司技術在你的帶領之下一定會蒸蒸日上。
做為師兄,最大的責任就是給小師妹以鼓勵和信心,給她描繪美好的未來,什麼出任CEO,贏取高富帥等全都不在話下。聽說有個專業的詞彙來描述這個過程叫做:畫餅。
小師妹有點心虛:可是F師兄,我還有點小小的問題沒有解決,有點中文的小小亂碼....
我深有體會的點點頭:馬賽克是阻礙人類進步的絆腳石...哦,不是馬賽克,是檔案亂碼,要想弄清楚這個問題,還要從那個字符集和檔案編碼講起。
字符集和檔案編碼
在很久很久以前,師兄我都還沒有出生的時候,西方世界出現了一種叫做計算機的高科技產品。
初代計算機只能做些簡單的算數運算,還要使用人工打孔的程式才能執行,不過隨著時間的推移,計算機的體積越來越小,計算能力越來越強,打孔已經不存在了,變成了人工編寫的計算機語言。
一切都在變化,唯有一件事情沒有變化。這件事件就是計算機和程式語言只流傳在西方。而西方日常交流使用26個字母加有限的標點符號就夠了。
最初的計算機儲存可以是非常昂貴的,我們用一個位元組也就是8bit來儲存所有能夠用到的字元,除了最開始的1bit不用以外,總共有128中選擇,裝26個小寫+26個大寫字母和其他的一些標點符號之類的完全夠用了。
這就是最初的ASCII編碼,也叫做美國資訊交換標準程式碼(American Standard Code for Information Interchange)。
後面計算機傳到了全球,人們才發現好像之前的ASCII編碼不夠用了,比如中文中常用的漢字就有4千多個,怎麼辦呢?
沒關係,將ASCII編碼本地化,叫做ANSI編碼。1個位元組不夠用就用2個位元組嘛,路是人走出來的,編碼也是為人來服務的。於是產生了各種如GB2312, BIG5, JIS等各自的編碼標準。這些編碼雖然與ASCII編碼相容,但是相互之間卻並不相容。
這嚴重的影響了國際化的程序,這樣還怎麼去實現同一個地球,同一片家園的夢想?
於是國際組織出手了,制定了UNICODE字符集,為所有語言的所有字元都定義了一個唯一的編碼,unicode的字符集是從U+0000到U+10FFFF這麼多個編碼。
小師妹:F師兄,那麼unicode和我平時聽說的UTF-8,UTF-16,UTF-32有什麼關係呢?
我笑著問小師妹:小師妹,把大象裝進冰箱有幾步?
小師妹:F師兄,腦筋急轉彎的故事,已經不適合我了,大象裝進冰箱有三步,第一開啟冰箱,第二把大象裝進去,第三關上冰箱,完事了。
小師妹呀,作為一個有文化的中國人,要真正的承擔起民族復興,科技進步的大任,你的想法是很錯誤的,不能光想口號,要有實際的可操作性的方案才行,要不然我們什麼時候才能夠打造秦芯,唐芯和明芯呢?
師兄說的對,可是這跟unicode有什麼關係呢?
unicode字符集最後是要儲存到檔案或者記憶體裡面的,那怎麼存呢?使用固定的1個位元組,2個位元組還是用變長的位元組呢?根據編碼方式的不同,可以分為UTF-8,UTF-16,UTF-32等多種編碼方式。
其中UTF-8是一種變長的編碼方案,它使用1-4個位元組來儲存。UTF-16使用2個或者4個位元組來儲存,JDK9之後的String的底層編碼方式變成了兩種:LATIN1和UTF16。
而UTF-32是使用4個位元組來儲存。這三種編碼方式中,只有UTF-8是相容ASCII的,這也是為什麼國際上UTF-8編碼方式比較通用的原因(畢竟計算機技術都是西方人搞出來的)。
解決Properties中的亂碼
小師妹,要解決你Properties中的亂碼問題很簡單,Reader基本上都有一個Charsets的引數,通過這個引數可以傳入要讀取的編碼方式,我們把UTF-8傳進去就行了:
public void usePropertiesWithUTF8() throws IOException{
Properties configProp = new Properties();
InputStream in = this.getClass().getClassLoader().getResourceAsStream("www.flydean.com.properties");
InputStreamReader inputStreamReader= new InputStreamReader(in, StandardCharsets.UTF_8);
configProp.load(inputStreamReader);
log.info(configProp.getProperty("name"));
configProp.setProperty("name", "www.flydean.com");
log.info(configProp.getProperty("name"));
}
上面的程式碼中,我們使用InputStreamReader封裝了InputStream,最終解決了中文亂碼的問題。
真.終極解決辦法
小師妹又有問題了:F師兄,這樣做是因為我們知道檔案的編碼方式是UTF-8,如果不知道該怎麼辦呢?是選UTF-8,UTF-16還是UTF-32呢?
小師妹問的問題越來越刁鑽了,還好這個問題我也有準備。
接下來介紹我們的終極解決辦法,我們將各種編碼的字元最後都轉換成unicode字符集存到properties檔案中,再讀取的時候是不是就沒有編碼的問題了?
轉換需要用到JDK自帶的工具:
native2ascii -encoding utf-8 file/src/main/resources/www.flydean.com.properties.utf8 file/src/main/resources/www.flydean.com.properties.cn
上面的命令將utf-8的編碼轉成了unicode。
轉換前:
site=www.flydean.com
name=程式那些事
轉換後:
site=www.flydean.com
name=\u7a0b\u5e8f\u90a3\u4e9b\u4e8b
再執行下測試程式碼:
public void usePropertiesFileWithTransfer() throws IOException {
Properties configProp = new Properties();
InputStream in = this.getClass().getClassLoader().getResourceAsStream("www.flydean.com.properties.cn");
configProp.load(in);
log.info(configProp.getProperty("name"));
configProp.setProperty("name", "www.flydean.com");
log.info(configProp.getProperty("name"));
}
輸出正確的結果。
如果要做國際化支援,也是這樣做的。
總結
千辛萬苦終於解決了小師妹的問題,F師兄要休息一下。
本文的例子https://github.com/ddean2009/learn-java-io-nio
本文PDF下載連結java-io-all-in-one.pdf
本文作者:flydean程式那些事