簡介
小師妹最新對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的博客
歡迎關注我的公衆號:程序那些事,更多精彩等着您!