淺談一下Java為什么不能使用字符流讀取非文本的二進(jìn)制文件
讀取文件
剛學(xué)Java的IO流部分時(shí),書上說(shuō)只能使用字節(jié)流去讀取圖片、視頻等非文本二進(jìn)制文件,不能使用字符流,否則文件會(huì)損壞。所以我就一直記住這一點(diǎn)了,但是為什么不能使用,這一直是我的一個(gè)疑惑。今天,我又想到了這個(gè)問(wèn)題,所以干脆就一鼓作氣把它解決了吧。
先來(lái)看一個(gè)關(guān)于圖片復(fù)制的代碼示例: 注意:我的電腦是存在 D:/DB這個(gè)路徑的,如果你沒(méi)有,DB這個(gè)文件夾,必須建立一個(gè)。
package dragon; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FileWriter; import java.io.IOException; import java.io.InputStreamReader; import java.nio.file.Path; import java.nio.file.Paths; public class ReadImage { public static void main(String[] args) throws IOException { String imgPath = "D:/DB/husky/kkk.jpeg"; String byteImgCopyPath = "D:/DB/husky/byteCopykkk.jpeg"; String charImgCopyPath = "D:/DB/husky/charCopykkk.jpeg"; Path srcPath = Paths.get(imgPath); Path desPath1 = Paths.get(byteImgCopyPath); Path desPath2 = Paths.get(charImgCopyPath); byteRead(srcPath.toFile(), desPath1.toFile()); System.out.println("字節(jié)復(fù)制執(zhí)行成功!"); characterRead(srcPath.toFile(), desPath2.toFile()); System.out.println("字符復(fù)制執(zhí)行成功!"); } static void byteRead(File src, File des) throws IOException { try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(src)); BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(des))) { int hasRead = 0; byte[] b = new byte[1024]; while ((hasRead = bis.read(b)) != -1) { bos.write(b, 0, hasRead); } } } static void characterRead(File src, File des) throws IOException { try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(src), "UTF-8")); BufferedWriter writer = new BufferedWriter(new FileWriter(des))) { int hasRead = 0; char[] c = new char[1024]; while ((hasRead = reader.read(c)) != -1) { writer.write(c, 0, hasRead); } } } }
運(yùn)行結(jié)果: 可見(jiàn),使用字符流確實(shí)無(wú)法讀取圖片這樣的二進(jìn)制文件,必須使用字節(jié)流。
圖片大小變化: 可見(jiàn),使用字符流后圖片大小變化了,使用字節(jié)流則不會(huì)。
為什么會(huì)這樣呢?
通過(guò)上面那個(gè)例子,我們可以看到確實(shí)是無(wú)法使用字符流復(fù)制文件,并且使用字符流復(fù)制文件后,文件的大小也會(huì)變化,這就引出我們今天要討論的標(biāo)題了。
我們先來(lái)想一想,為什么文本文件打開(kāi)可以顯示文字? 我們都知道計(jì)算機(jī)處理的文件無(wú)論是文本還是非文本的文件,最終在計(jì)算機(jī)內(nèi)部都是以二進(jìn)制的形式存儲(chǔ)的。
使用文本編輯器的16進(jìn)制模式打開(kāi)一個(gè)文本文件:
使用編輯器的16進(jìn)制模式打開(kāi)上面程序使用的圖片文件:
對(duì)比兩張圖片中的數(shù)據(jù),應(yīng)該發(fā)現(xiàn)不了什么區(qū)別吧,但是為什么文本數(shù)據(jù)就可以顯示出文字呢?這是一個(gè)非?;A(chǔ)的問(wèn)題,大學(xué)里面的基礎(chǔ)課都是講過(guò)這方面的內(nèi)容–字符編碼表。 我最開(kāi)始學(xué)習(xí)的是 C 語(yǔ)言,接觸最早的編碼表是 ASCII(美國(guó)信息交換標(biāo)準(zhǔn)代碼),后來(lái)學(xué)習(xí)java接觸的是 Unicode(萬(wàn)國(guó)碼,這個(gè)名字和它的起源很契合。我們目前最常使用的是UTF-8,是針對(duì)Unicode的一種可變長(zhǎng)度字符編碼。)
注意: 使用 UTF-8 也是分為含有 BOM(Byte Order Mark,字節(jié)順序標(biāo)記) 和 沒(méi)有的兩種形式,而且混用會(huì)導(dǎo)致錯(cuò)誤,感興趣的可以去了解一下。
字符編碼表的作用體現(xiàn)在編碼上,引述百科的一段話:
在顯示器上看見(jiàn)的文字、圖片等信息在電腦里面其實(shí)并不是我們看見(jiàn)的樣子,即使你知道所有信息都存儲(chǔ)在硬盤里,把它拆開(kāi)也看不見(jiàn)里面有任何東西,只有些盤片。假設(shè),你用顯微鏡把盤片放大,會(huì)看見(jiàn)盤片表面凹凸不平,凸起的地方被磁化,凹的地方是沒(méi)有被磁化;凸起的地方代表數(shù)字1,凹的地方代表數(shù)字0。硬盤只能用0和1來(lái)表示所有文字、圖片等信息。那么字母”A”在硬盤上是如何存儲(chǔ)的呢?可能小張計(jì)算機(jī)存儲(chǔ)字母”A”是1100001,而小王存儲(chǔ)字母”A”是11000010,這樣雙方交換信息時(shí)就會(huì)誤解。比如小張把1100001發(fā)送給小王,小王并不認(rèn)為1100001是字母”A”,可能認(rèn)為這是字母”X”,于是小王在用記事本訪問(wèn)存儲(chǔ)在硬盤上的1100001時(shí),在屏幕上顯示的就是字母”X”。也就是說(shuō),小張和小王使用了不同的編碼表。
所以字符編碼表就是二進(jìn)制數(shù)字和字符之間的一個(gè)一一映射,例如 65 (數(shù)字)代表 A,所以下面這段代碼會(huì)在屏幕上輸出 A。
char c = 65; System.out.println(c);
我們使用一個(gè)循環(huán)來(lái)測(cè)試一下:
char c = 0; for (int i = 9999; i < 10009; i++) { c = (char) i; System.out.print(c+" "); }
測(cè)試結(jié)果:(當(dāng)然了,這個(gè)取決于你的當(dāng)前的字符編碼表,如果使用 ASCII,估計(jì)就有意思了。)
這樣就解釋了前面那個(gè)問(wèn)題(為什么文本文件打開(kāi)可以顯示文字?),我們之所以可以看見(jiàn)文本文件的字符是因?yàn)橛?jì)算機(jī)按照我們文件的編碼(ASCII、UTF-8或者GBK等),從字符編碼表中找出來(lái)對(duì)應(yīng)的字符。 所以,當(dāng)我們使用記事本打開(kāi)二進(jìn)制文件會(huì)看到亂碼,這就是原因。文件的復(fù)制過(guò)程也是復(fù)制的二進(jìn)制數(shù)據(jù),而不是真實(shí)的文字。
因此可以這樣理解文件復(fù)制的過(guò)程:
- 字符流:二進(jìn)制數(shù)據(jù) --編碼-> 字符編碼表 --解碼-> 二進(jìn)制數(shù)據(jù)
- 字節(jié)流:二進(jìn)制數(shù)據(jù) —> 二進(jìn)制數(shù)據(jù)
所以問(wèn)題就是出現(xiàn)在編碼和解碼的過(guò)程中,既然是字符的編碼表,那它就是包含所有的字符,但是字符的數(shù)量是有限的,這就意味著它不能表示一些超過(guò)編碼表的字符,因?yàn)楦静淮嬖诒碇?。所以,JVM 會(huì)使用一些字符進(jìn)行替換,基本上都是亂碼(所以大小會(huì)發(fā)生變化),而且如果有一個(gè)數(shù)據(jù)恰好是-1,那么讀取就會(huì)中斷,引起數(shù)據(jù)丟失。
例如如下代碼使用字符流讀取就會(huì)錯(cuò)誤:
String filename = "D:/DB/fos.txt"; //文件名 byte[] b = new byte[] {-1, -1}; //兩個(gè)字節(jié),127的二進(jìn)制就是 1111 1111 //數(shù)據(jù)寫入文件 try (FileOutputStream fos = new FileOutputStream(filename)) { fos.write(b, 0, b.length); //將兩個(gè)127連續(xù)寫入,就是 1111 1111 1111 1111 } File file = new File(filename); //輸出文件的大小 System.out.println("file length: " + file.length()); char[] c = new char[2]; //使用字符流讀取文件 try (FileReader reader = new FileReader(filename)) { int count = reader.read(c); //Java使用Unicode編碼,讀取的是從 0-65535 之間的數(shù)字。 System.out.println("以文本形式輸出:" + new String(c, 0, count)+" "+count); for (char d : c) { System.out.println("字符為:" + d); } } System.out.println("表示字符:" + c[0]); //再寫入文件 try (FileWriter writer = new FileWriter(filename)) { writer.write(c, 0, 2); } File f = new File(filename); System.out.println("file length: " + f.length());
結(jié)果:
說(shuō)明: 我將兩個(gè)1字節(jié)的-1寫入(字節(jié)流)了文本文件(注意是字節(jié):-1,不是字符:-1),然后再讀?。?strong>字符流),再寫入(字符流)就已經(jīng)出現(xiàn)了問(wèn)題。讀取出的字符顯示了一個(gè)奇怪的符號(hào),而且它的值為:65533,這個(gè)值如果用字節(jié)表示的話,一個(gè)字節(jié)是不夠的,所以文件的大小就會(huì)變化。在非文本的二進(jìn)制數(shù)據(jù)中,出現(xiàn)這種情況都是正常的,因?yàn)楸緛?lái)就不是按照字符編碼的。
因?yàn)樽址际钦龜?shù),而非字符編碼的話,字節(jié)數(shù)可能是負(fù)數(shù)(很可能),但是負(fù)數(shù)在字符看來(lái)就是正數(shù),這也是為什么-1,被讀成 65533的原因??梢钥闯鰜?lái),讀取就已經(jīng)錯(cuò)誤了。
注意: 這里的重點(diǎn)是對(duì)于使用字符流讀取非文本文件,在讀取-寫入的過(guò)程中的問(wèn)題。
總結(jié)
這個(gè)問(wèn)題算是基本解決了,如果想要了解更多,估計(jì)需要閱讀一些專業(yè)的書籍才行了,不過(guò)到了這一步,我覺(jué)得已經(jīng)可以了。它也要求我們掌握關(guān)于計(jì)算機(jī)的一些基本的入門知識(shí)了。雖然這個(gè)問(wèn)題拖了很久才解決,但是也是因?yàn)槲易罱_(kāi)始使用Java的IO流進(jìn)行編程,以前的話,只是記住了那句話,但是動(dòng)手實(shí)踐卻沒(méi)有去做,這也是應(yīng)該多動(dòng)手編程、多積累才能解決問(wèn)題。
到此這篇關(guān)于淺談一下為什么不能使用字符流讀取非文本的二進(jìn)制文件的文章就介紹到這了,更多相關(guān)不能使用字符流讀取非文本二進(jìn)制文件內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Nacos配置文件使用經(jīng)驗(yàn)及CAP原則詳解
這篇文章主要為大家介紹了Nacos配置文件使用經(jīng)驗(yàn)及CAP規(guī)則詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2024-02-02java 實(shí)現(xiàn)音樂(lè)播放器的簡(jiǎn)單實(shí)例
這篇文章主要介紹了java 實(shí)現(xiàn)音樂(lè)播放器的簡(jiǎn)單實(shí)例的相關(guān)資料,希望通過(guò)本文能幫助到大家,實(shí)現(xiàn)這樣的功能,需要的朋友可以參考下2017-09-09java實(shí)現(xiàn)pgsql自動(dòng)更新創(chuàng)建時(shí)間與更新時(shí)間的兩種方式小結(jié)
本文主要介紹了java實(shí)現(xiàn)pgsql自動(dòng)更新創(chuàng)建時(shí)間與更新時(shí)間的兩種方式小結(jié),主要包括通過(guò)數(shù)據(jù)庫(kù)自身實(shí)現(xiàn)以及通過(guò)mybatisplus的TableField注解添加,具有一定的參考價(jià)值,感興趣的可以了解一下2024-01-01Java8新特性之lambda(動(dòng)力節(jié)點(diǎn)Java學(xué)院整理)
這篇文章主要介紹了Java8新特性之lambda(動(dòng)力節(jié)點(diǎn)Java學(xué)院整理)表達(dá)式的相關(guān)知識(shí),包括lambda語(yǔ)法方面的知識(shí),非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友參考下吧2017-06-06java結(jié)束當(dāng)前循環(huán)常用代碼
在?Java中,當(dāng)我們要結(jié)束一個(gè)循環(huán)時(shí),通常會(huì)使用循環(huán)變量的實(shí)現(xiàn)類來(lái)結(jié)束,但在實(shí)際開(kāi)發(fā)中,我們經(jīng)常會(huì)遇到某個(gè)循環(huán)結(jié)束后需要進(jìn)行其他的操作的情況,在本文中給大家分享java結(jié)束當(dāng)前循環(huán)常用代碼,感興趣的朋友跟隨小編一起看看吧2023-06-06java 中 阻塞隊(duì)列BlockingQueue詳解及實(shí)例
這篇文章主要介紹了java 中 阻塞隊(duì)列BlockingQueue詳解及實(shí)例的相關(guān)資料,需要的朋友可以參考下2017-03-03springboot2.0.0配置多數(shù)據(jù)源出現(xiàn)jdbcUrl is required with driverClassN
這篇文章主要介紹了springboot2.0.0配置多數(shù)據(jù)源出現(xiàn)jdbcUrl is required with driverClassName的錯(cuò)誤,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-11-11springdata jpa單表操作crud的實(shí)例代碼詳解
這篇文章主要介紹了springdata jpa單表操作crud的實(shí)例代碼詳解,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-01-01