小師妹學JavaIO之:文件讀取那些事

簡介

小師妹最新對java IO中的reader和stream產生了一點點困惑,不知道到底該用哪一個纔對,怎麼讀取文件纔是正確的姿勢呢?今天F師兄現場爲她解答。

更多精彩內容:
區塊鏈從入門到放棄系列教程-涵蓋密碼學,超級賬本,以太坊,Libra,比特幣等持續更新
Spring Boot 2.X系列教程:七天從無到有掌握Spring Boot-持續更新
Spring 5.X系列教程:滿足你對Spring5的一切想象-持續更新
java程序員從小工到專家成神之路(2020版)-持續更新中,附詳細文章教程

字符和字節

小師妹最近很迷糊: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);
            }
        }
    }

總結

今天給小師妹講解了字符流和字節流,還講解了文件讀取的基本方法,不虛此行。

本文的例子https://github.com/ddean2009/learn-java-io-nio

本文作者:flydean程序那些事

本文鏈接:http://www.flydean.com/io-file-reader/

本文來源:flydean的博客

歡迎關注我的公衆號:程序那些事,更多精彩等着您!

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