小師妹學IO系列文章集合-附PDF下載


目錄

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有四種類型:

  1. ENTRY_CREATE 目標被創建
  2. ENTRY_DELETE 目標被刪除
  3. ENTRY_MODIFY 目標被修改
  4. OVERFLOW 一個特殊的Event,表示Event被放棄或者丟失

register返回的WatchKey就是監聽到的WatchEvent的集合。

現在來看WatchService的4個方法:

  1. close 關閉watchService
  2. poll 獲取下一個watchKey,如果沒有則返回null
  3. 帶時間參數的poll 在等待的一定時間內獲取下一個watchKey
  4. 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程序那些事

本文鏈接:http://www.flydean.com/java-io-all-in-one/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM