淺談一下Java為什么不能使用字符流讀取非文本的二進制文件
讀取文件
剛學Java的IO流部分時,書上說只能使用字節(jié)流去讀取圖片、視頻等非文本二進制文件,不能使用字符流,否則文件會損壞。所以我就一直記住這一點了,但是為什么不能使用,這一直是我的一個疑惑。今天,我又想到了這個問題,所以干脆就一鼓作氣把它解決了吧。
先來看一個關于圖片復制的代碼示例: 注意:我的電腦是存在 D:/DB這個路徑的,如果你沒有,DB這個文件夾,必須建立一個。
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é)復制執(zhí)行成功!");
characterRead(srcPath.toFile(), desPath2.toFile());
System.out.println("字符復制執(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);
}
}
}
}
運行結果: 可見,使用字符流確實無法讀取圖片這樣的二進制文件,必須使用字節(jié)流。

圖片大小變化: 可見,使用字符流后圖片大小變化了,使用字節(jié)流則不會。

為什么會這樣呢?
通過上面那個例子,我們可以看到確實是無法使用字符流復制文件,并且使用字符流復制文件后,文件的大小也會變化,這就引出我們今天要討論的標題了。
我們先來想一想,為什么文本文件打開可以顯示文字? 我們都知道計算機處理的文件無論是文本還是非文本的文件,最終在計算機內部都是以二進制的形式存儲的。
使用文本編輯器的16進制模式打開一個文本文件:

使用編輯器的16進制模式打開上面程序使用的圖片文件:

對比兩張圖片中的數(shù)據(jù),應該發(fā)現(xiàn)不了什么區(qū)別吧,但是為什么文本數(shù)據(jù)就可以顯示出文字呢?這是一個非?;A的問題,大學里面的基礎課都是講過這方面的內容–字符編碼表。 我最開始學習的是 C 語言,接觸最早的編碼表是 ASCII(美國信息交換標準代碼),后來學習java接觸的是 Unicode(萬國碼,這個名字和它的起源很契合。我們目前最常使用的是UTF-8,是針對Unicode的一種可變長度字符編碼。)
注意: 使用 UTF-8 也是分為含有 BOM(Byte Order Mark,字節(jié)順序標記) 和 沒有的兩種形式,而且混用會導致錯誤,感興趣的可以去了解一下。

字符編碼表的作用體現(xiàn)在編碼上,引述百科的一段話:
在顯示器上看見的文字、圖片等信息在電腦里面其實并不是我們看見的樣子,即使你知道所有信息都存儲在硬盤里,把它拆開也看不見里面有任何東西,只有些盤片。假設,你用顯微鏡把盤片放大,會看見盤片表面凹凸不平,凸起的地方被磁化,凹的地方是沒有被磁化;凸起的地方代表數(shù)字1,凹的地方代表數(shù)字0。硬盤只能用0和1來表示所有文字、圖片等信息。那么字母”A”在硬盤上是如何存儲的呢?可能小張計算機存儲字母”A”是1100001,而小王存儲字母”A”是11000010,這樣雙方交換信息時就會誤解。比如小張把1100001發(fā)送給小王,小王并不認為1100001是字母”A”,可能認為這是字母”X”,于是小王在用記事本訪問存儲在硬盤上的1100001時,在屏幕上顯示的就是字母”X”。也就是說,小張和小王使用了不同的編碼表。
所以字符編碼表就是二進制數(shù)字和字符之間的一個一一映射,例如 65 (數(shù)字)代表 A,所以下面這段代碼會在屏幕上輸出 A。
char c = 65; System.out.println(c);
我們使用一個循環(huán)來測試一下:
char c = 0;
for (int i = 9999; i < 10009; i++) {
c = (char) i;
System.out.print(c+" ");
}
測試結果:(當然了,這個取決于你的當前的字符編碼表,如果使用 ASCII,估計就有意思了。)

這樣就解釋了前面那個問題(為什么文本文件打開可以顯示文字?),我們之所以可以看見文本文件的字符是因為計算機按照我們文件的編碼(ASCII、UTF-8或者GBK等),從字符編碼表中找出來對應的字符。 所以,當我們使用記事本打開二進制文件會看到亂碼,這就是原因。文件的復制過程也是復制的二進制數(shù)據(jù),而不是真實的文字。
因此可以這樣理解文件復制的過程:
- 字符流:二進制數(shù)據(jù) --編碼-> 字符編碼表 --解碼-> 二進制數(shù)據(jù)
- 字節(jié)流:二進制數(shù)據(jù) —> 二進制數(shù)據(jù)
所以問題就是出現(xiàn)在編碼和解碼的過程中,既然是字符的編碼表,那它就是包含所有的字符,但是字符的數(shù)量是有限的,這就意味著它不能表示一些超過編碼表的字符,因為根本不存在表中。所以,JVM 會使用一些字符進行替換,基本上都是亂碼(所以大小會發(fā)生變化),而且如果有一個數(shù)據(jù)恰好是-1,那么讀取就會中斷,引起數(shù)據(jù)丟失。
例如如下代碼使用字符流讀取就會錯誤:
String filename = "D:/DB/fos.txt"; //文件名
byte[] b = new byte[] {-1, -1}; //兩個字節(jié),127的二進制就是 1111 1111
//數(shù)據(jù)寫入文件
try (FileOutputStream fos = new FileOutputStream(filename)) {
fos.write(b, 0, b.length); //將兩個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());
結果:

說明: 我將兩個1字節(jié)的-1寫入(字節(jié)流)了文本文件(注意是字節(jié):-1,不是字符:-1),然后再讀取(字符流),再寫入(字符流)就已經出現(xiàn)了問題。讀取出的字符顯示了一個奇怪的符號,而且它的值為:65533,這個值如果用字節(jié)表示的話,一個字節(jié)是不夠的,所以文件的大小就會變化。在非文本的二進制數(shù)據(jù)中,出現(xiàn)這種情況都是正常的,因為本來就不是按照字符編碼的。
因為字符都是正數(shù),而非字符編碼的話,字節(jié)數(shù)可能是負數(shù)(很可能),但是負數(shù)在字符看來就是正數(shù),這也是為什么-1,被讀成 65533的原因。可以看出來,讀取就已經錯誤了。
注意: 這里的重點是對于使用字符流讀取非文本文件,在讀取-寫入的過程中的問題。
總結
這個問題算是基本解決了,如果想要了解更多,估計需要閱讀一些專業(yè)的書籍才行了,不過到了這一步,我覺得已經可以了。它也要求我們掌握關于計算機的一些基本的入門知識了。雖然這個問題拖了很久才解決,但是也是因為我最近開始使用Java的IO流進行編程,以前的話,只是記住了那句話,但是動手實踐卻沒有去做,這也是應該多動手編程、多積累才能解決問題。
到此這篇關于淺談一下為什么不能使用字符流讀取非文本的二進制文件的文章就介紹到這了,更多相關不能使用字符流讀取非文本二進制文件內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
java實現(xiàn)pgsql自動更新創(chuàng)建時間與更新時間的兩種方式小結
本文主要介紹了java實現(xiàn)pgsql自動更新創(chuàng)建時間與更新時間的兩種方式小結,主要包括通過數(shù)據(jù)庫自身實現(xiàn)以及通過mybatisplus的TableField注解添加,具有一定的參考價值,感興趣的可以了解一下2024-01-01
Java8新特性之lambda(動力節(jié)點Java學院整理)
這篇文章主要介紹了Java8新特性之lambda(動力節(jié)點Java學院整理)表達式的相關知識,包括lambda語法方面的知識,非常不錯,具有參考借鑒價值,需要的朋友參考下吧2017-06-06
springboot2.0.0配置多數(shù)據(jù)源出現(xiàn)jdbcUrl is required with driverClassN
這篇文章主要介紹了springboot2.0.0配置多數(shù)據(jù)源出現(xiàn)jdbcUrl is required with driverClassName的錯誤,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-11-11

