本文對 Java 中的 IO 流的概念和操作進行了梳理總結,并給出了對中文亂碼問題的解決方法。
1. 什么是流
Java 中的流是對字節序列的抽象,我們可以想象有一個水管,只不過現在流動在水管中的不再是水,而是字節序列。和水流一樣,Java 中的流也具有一個 “流動的方向”,通常可以從中讀入一個字節序列的對象被稱為輸入流;能夠向其寫入一個字節序列的對象被稱為輸出流。
以下是 IO 相關類的總結圖
2. 字節流
Java 中的字節流處理的最基本單位為單個字節,它通常用來處理二進制數據。Java 中最基本的兩個字節流類是 InputStream 和 OutputStream,它們分別代表了組基本的輸入字節流和輸出字節流。InputStream 類與 OutputStream 類均為抽象類,我們在實際使用中通常使用 Java 類庫中提供的它們的一系列子類。下面我們以 InputStream 類為例,來介紹下 Java 中的字節流。
InputStream 類中定義了一個基本的用于從字節流中讀取字節的方法 read,這個方法的定義如下:
public abstract int read() throws IOException;
這是一個抽象方法,也就是說任何派生自 InputStream 的輸入字節流類都需要實現這一方法,這一方法的功能是從字節流中讀取一個字節,若到了末尾則返回 - 1,否則返回讀入的字節。關于這個方法我們需要注意的是,它會一直阻塞知道返回一個讀取到的字節或是 - 1。另外,字節流在默認情況下是不支持緩存的,這意味著每調用一次 read 方法都會請求操作系統來讀取一個字節,這往往會伴隨著一次磁盤 IO,因此效率會比較低。有的小伙伴可能認為 InputStream 類中 read 的以字節數組為參數的重載方法,能夠一次讀入多個字節而不用頻繁的進行磁盤 IO。那么究竟是不是這樣呢?我們來看一下這個方法的源碼:
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}
它調用了另一個版本的 read 重載方法,那我們就接著往下追:
public int read(byte b[], int off, int len) throws IOException {
if (b == null) {
throw new NullPointerException();
} else if (off < 0 || len < 0 || len > b.length - off) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return 0;
}
int c = read();
if (c == -1) {
return -1;
}
b[off] = (byte)c;
int i = 1;
try {
for (; i < len ; i++) {
c = read();
if (c == -1) {
break;
}
b[off + i] = (byte)c;
}
} catch (IOException ee) {
}
return i;
}
從以上的代碼我們可以看到,實際上 read(byte[])方法內部也是通過循環調用 read()方法來實現 “一次” 讀入一個字節數組的,因此本質來說這個方法也未使用內存緩沖區。要使用內存緩沖區以提高讀取的效率,我們應該使用 BufferedInputStream。
3. 字符流
Java 中的字符流處理的最基本的單元是 Unicode 碼元(大小 2 字節),它通常用來處理文本數據。所謂 Unicode 碼元,也就是一個 Unicode 代碼單元,范圍是 0x0000~0xFFFF。在以上范圍內的每個數字都與一個字符相對應,Java 中的 String 類型默認就把字符以 Unicode 規則編碼而后存儲在內存中。然而與存儲在內存中不同,存儲在磁盤上的數據通常有著各種各樣的編碼方式。使用不同的編碼方式,相同的字符會有不同的二進制表示。實際上字符流是這樣工作的:
- 輸出字符流:把要寫入文件的字符序列(實際上是 Unicode 碼元序列)轉為指定編碼方式下的字節序列,然后再寫入到文件中;
- 輸入字符流:把要讀取的字節序列按指定編碼方式解碼為相應字符序列(實際上是 Unicode 碼元序列從)從而可以存在內存中。
我們通過一個 demo 來加深對這一過程的理解,示例代碼如下:
import java.io.FileWriter;
import java.io.IOException;
public class FileWriterDemo {
public static void main(String[] args) {
FileWriter fileWriter = null;
try {
try {
fileWriter = new FileWriter("demo.txt");
fileWriter.write("demo");
} finally {
fileWriter.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
以上代碼中,我們使用 FileWriter 向 demo.txt 中寫入了 “demo” 這四個字符,我們用十六進制編輯器 WinHex 查看下 demo.txt 的內容:
從上圖可以看出,我們寫入的 “demo” 被編碼為了“64 65 6D 6F”,但是我們并沒有在上面的代碼中顯式指定編碼方式,實際上,在我們沒有指定時使用的是操作系統的默認字符編碼方式來對我們要寫入的字符進行編碼。
由于字符流在輸出前實際上是要完成 Unicode 碼元序列到相應編碼方式的字節序列的轉換,所以它會使用內存緩沖區來存放轉換后得到的字節序列,等待都轉換完畢再一同寫入磁盤文件中。
4. 字符流與字節流的區別
經過以上的描述,我們可以知道字節流與字符流之間主要的區別體現在以下幾個方面:
- 字節流操作的基本單元為字節;字符流操作的基本單元為 Unicode 碼元。
- 字節流通常用于處理二進制數據,實際上它可以處理任意類型的數據,但它不支持直接寫入或讀取 Unicode 碼元;字符流通常處理文本數據,它支持寫入及讀取 Unicode 碼元。
5. 緩沖流
緩沖流是處理流的一種, 它依賴于原始的輸入輸出流, 它令輸入輸出流具有1個緩沖區, 顯著減少與外部設備的IO次數, 而且提供一些額外的方法.
可見, 緩沖流最大的特點就是具有1個緩沖區! 而我們使用緩沖流無非兩個目的:
- 減少IO次數(提升performance)
- 使用一些緩沖流的額外的方法.
緩沖字節流:BufferedInputStream
,BufferedOutputStream
緩沖字符流:BufferedReader
,BufferedWriter
5. 字節流中文亂碼問題
在 Java 中 不同編碼方式中文所占字節數不同,詳見 https://www.cnblogs.com/lslk89/p/6898526.html
例如:"abc中國"
當我們以每四個字節讀取文件時,此時會讀到 "abc" + "中"的首字節,此時就會產生亂碼。
byte[] b = new btye[4];
inputStream.read(b);//出現亂碼
又如如下代碼,讀取字節流
/*
* 讀取文件字節流
*/
public String readerFile(File f) {
String str = "";
FileInputStream fis = null;
try {
fis = new FileInputStream(f);
byte[] b = new byte[512];
int n;
while ((n = fis.read(b)) != -1) {
str = str + new String(b, 0, n);
b = new byte[512];
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
fis.close();
} catch (Exception e) {
e.printStackTrace();
}
}
return str;
}
這個方法就是通過傳進的File對象,讀取里面的內容,返回一個字符串,如果你把這方法copy去讀取含有中文的文件,無意外的話就會出現中文亂碼,如果出現中文亂碼,該如何解決呢?其實String類有提供方法解決,只要把str = str + new String(b, 0, n);改成str = str + new String(b, 0, n, "gbk");就可以解決了。
上面的方法在一般情況下是可以讀取中文了,但是,仔細想想,畢竟上面的方法是以字節為單位的,而一個中文占多個字節,細心的同學應該已經想到了,上面的方法是一次讀取512個字節,如果,一個中文剛好就占了第512個字節和第513個字節,你一次讀512個字節,狠狠得把他們拆散了,重新new了一個新的字符串,你說亂碼不亂碼?
解決辦法
- 判斷讀取的字節是否是中文字節,這種方式比較麻煩
- 將字節流轉換成字符流,并指定編碼,詳見下文
6. 字節流和字符流的選擇
操作對象
- 字符流操作對象
- 純文本
2.需要查指定的編碼表,默認為GBK
- 純文本
- 字節流操作對象
- 圖像,音頻等多媒體文件
- 無需查詢指定編碼表
如何選擇合適的流
- 先明確源頭和目的:源頭使用的是輸入流,InputStream或者Reader。目的使用的是輸出流,OutputStream或者Writer
- 確定操作的對象是那些:純文本用字符流,否則用字節流
- 當明確后,再確定使用哪一個具體的對象:內存,硬盤(比如操作文件的話用FileWriter/FileReader,或者FileInputStream/FileOutputStream),控制臺(System)
7. 字節流和字符流的相互轉換
從字符流到字節流
可以從字符流中獲取char[]數組,轉換為String,然后調用String的API函數getBytes() 獲取到byte[],然后就可以通過ByteArrayInputStream、ByteArrayOutputStream來實現到字節流的轉換。
函數:new String(byte[] data, String encoding);
這個方法通常與String.getBytes(String encoding)一起使用.
用法:tring str = new String(formMsg.getBytes("ISO-8859-1"),"utf-8");
從字節流到字符流
如下,是一個字節流上傳文件到 hadoop hdfs 的工具方法。此處為了避免中文亂碼的,將字節流指定編碼轉換為字符流,然后再用 getBytes("UTF-8")
方法獲取相應編碼的字節,實現字節流輸出。
/**
* 文件流上傳文件
*
* @param iStream 輸入流
* @param pathStr HDFS 路徑 'test/out/' 最后要有 /
* @param fileName 文件名
* @return
*/
public static boolean upLoadFileToHdfs(InputStream iStream, String pathStr, String fileName) {
//FileSystem fs = FileSystem.get(conf);
Path path = new Path(pathStr + fileName);
//FSDataOutputStream outputStream = fs.create(path);
FileSystem fs = null;
FSDataOutputStream outputStream = null;
//InputStreamReader是字節流和字符流之間的橋梁,轉化時需要指定字符集,否則按照系統字符集轉換
InputStreamReader reader = null;
BufferedReader br = null;
try {
reader = new InputStreamReader(iStream,"UTF-8");
//創建緩沖字符輸入流
br = new BufferedReader(reader);
fs = FileSystem.get(conf);
outputStream = fs.create(path);
String line;
while ((line = br.readLine()) != null) {
outputStream.write(line.getBytes("UTF-8"));
outputStream.write("\r\n".getBytes("UTF-8"));
}
//IOUtils.copyBytes(, outputStream, 4096);
return true;
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
outputStream.hsync();
outputStream.close();
br.close();
reader.close();
iStream.close();
fs.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return false;
}
8. Java如何獲取文件編碼格式
若我們想知道一個文件的編碼格式,我們可以使用 cpdetector 這個開源的jar包可以自動判斷當前文件的內容編碼,從而在讀取的時候選擇正確的編碼讀取,避免亂碼問題。
地址: http://cpdetector.sourceforge.net/
使用方法可以參照博客: