- tags:io
- categories:總結
- date: 2017-03-28 22:49:50
不僅僅在JAVA領域中,還有其他編程語言中都離不開對IO的操作。io是操作系統與機器硬件與外界進行交互的通道,計算機是個處理器,要處理數據,那當然要有信息數據流了。那么大多數據信息都是需要外界輸入,在進行特別處理,得到我們想要的數據。整個IPO(input-process-ouput)過程也就是io處理過程的抽象描述。
不僅僅是計算機中有IPO,整個現代社會不都是這種模式么,就像工廠流水線搬,將原材料(輸入信息流)通過指定機器材料(input通道地址)進行加工處理(cpu),最后得到產品(output輸出)。所以,io其實是程序處理非常非常重要的模塊,沒有數據,其他無論硬件資源,程序的意義都不大了。
所以,就像好好總結學習java中IO模板內容,想知道是如何查找資源的?資源路徑獲取途徑?在查找到資源,如何讀取,是字節流,還是字符流?讀取信息流,如何保存?如何處理資源文件的編碼問題等等問題,都是值得去思考思考的。對于我們java應用程序,舉個最簡單的例子,我們應用程序中創建java.lang.Class對象加載類的時候,就是要在路徑中查找對應class字節碼文件,這就是個輸入過程,當讀取完字節碼后,就加載到內存中進行處理;或者還有在使用框架時候,總會遇到xml和properties配置文件,在應用啟動后,重要通過io對這些配置文件進行讀取.......
IO流分類
在總體上可以知道,io就是數據流的傳遞過程,那必然是兩端節點的操作。一端讀取數據,另一端可以將處理好的數據回寫。那么先來看看對于java中的IO流是如何分類的呢?
A. 根據處理的流的數據單位不同,可以分為:字符流,字節流。
B. 根據數據流動方向不同,可以分為:輸入流,輸出流。(數據流的方向是相對而言的)
C. 根據讀取流的功能不同,可以分為:節點流,處理流。
字節流 字符流
輸入流 InputStream Reader
輸出流 OutputStream Writer
流的大致處理過程可以抽象成如下圖所示:(圖片來源于網絡)
[圖片上傳失敗...(image-9be6c6-1511063649569)]
另外關于節點流和處理流概念也是可以對應到具體的io操作過程中:[圖片來自java中的IO流介紹]
節點流: 該流指的是從一個特定的數據源讀寫數據。即節點流是直接操作實體資源的(eg:文件,網絡流等)。就如javaio中的FileInputStream和FileOutputStream兩個類這般,內部有個FileDescriptor文件句柄類,就是直接連接文件數據源,對文件字節流進行讀寫。
處理流: "連接"在已存在的流(字節流或處理流)之上,通過對流數據的處理,為程序提供更為強大的讀寫功能(eg:處理文件字節流,那么可以增加緩沖區,在讀寫固定長度的字節后,一次性全部轉換成字符數據,供程序使用)。過濾流是使用一個已經存在的輸入流或者輸出流連接創建的,過濾流就是對字節流進行一系列的包裝。
eg: BufferedInputStream和BufferedOutputStream,就是使用了已經存在的字節流來構造一個提供帶緩沖的讀寫流,大大提高的程序讀寫文件資源的效率。
那么,java中的io流除了知道是有字節字符io流幾大類,其實內部還有很多根據不同的場景實現的其他流,這都是因為抽象IO基類無法高效的完整資源數據流的輸入輸出,所以java又自定義了幾種用于不同場景的流。java-io流類體系如下:(圖片來自:IO流體系)
可以看到,針對不同的使用需求,訪問文件,數組,字符串,管道,緩沖流,轉換流等等,都有合適的類去完整任務。我們的目的就是在理解基礎IO操作流程,底層結構,常用api的情況下,可以因地制宜的選擇合適類去處理業務。
字節字符與編碼
因為java io在讀取或者寫入字節字符的時候,都會進行字符字節編碼解碼的工作。在學習IO之前,弄懂字節字符的編碼解碼是什么?在字節轉換成字符的時候,是如何根據字節解碼成字符串的?當字符轉換成字節輸出的時候,內部是如何對字符進行編碼成字節輸出的?編碼是開發中不可避免的部分。所以,這里可以參照自己寫得關于字符字節編碼解碼的文章: 字節字符編碼解析ASCII編碼漢字轉換
Input輸入
首先,可以通過javaio中input輸入流的類圖結構大致了解java中輸入流有哪些,并且可以從類圖中清楚的了解這些流的層級關系。當然了,輸入流中根據數據源讀取是字節還是字符又分為字節流(InputStream)和字符流(Reader),下面可以分別了解學習輸入流內容。
InputStream字節輸入流
InputStream對應的流,表示的是從程序外部資源(遠程/本地文件,網絡數據字節流..)讀取資源內部字節數據,保存到一個byte字節數組中,在程序中,就能對該字節數組內的數據進行讀取,修改等處理操作。那么,代表著字符輸入流的Inputstream抽象類中,針對資源文件和數據規定了哪些操作呢?比如如何讀取字節數據,保存在哪?如何一次讀取多個字節?在讀取流過程中,使用什么來記錄讀取狀態?如何標記字節流中讀取的位置等都是值得思考的。
//InputStream.java
public abstract int read() throws IOException;/*一次讀取一個字節,返回讀取字節值0~255*/
public int read(byte b[]) throws IOException;/*一次性將字節流中字節數據保存到b數組中*/
/*一次性讀取len(或者更少)個字節放置到b[off]~b[b.length-1]中*/
public int read(byte b[], int off, int len) throws IOException;
public long skip(long n);/*在讀取字節流中,跳過n個字節繼續讀取*/
public int available() throws IOException; /*返回字節流中可以讀取的字節數量*/
public void close() throws IOException;/*關閉輸入字節流*/
public synchronized void mark(int readlimit);/*標記讀取流字節位置標記*/
public synchronized void reset() throws IOException;/*重置字節流讀取位置*/
可以從方法中看到,頂層的字節數入流中是通過一個字節數組byte[]來保存流中數據信息的。當外部資源文件句柄與該流連接上之后,可以每次讀取文件中一個字節,也可以讀取文件中的指定字節數,最后都是保存在byte[]字節數組中。
當然還有提供一些在讀取字節數據過程中的標記,重置操作,可以讀取流中任意位置的字節數據。當讀取字節都保存在byte數組中后,還能通過available方法獲取已經讀取字節的數量。
之后,所有的字節輸入流InputStream的子類就可以再次基礎上進行重寫覆蓋或者定義一些其他的針對字節流的操作。目的當然是通過更加合理,合適的底層容器如字節數組,更加高效的讀取資源字節流。提高IO使用效率,節省資源占用時間。
其實,從InputStream類圖中可以發現,每個子類都會或多或少的自定義方法去實現或者重寫InputStream字節輸入流基本方法。
底層多數都是使用一個byte[]
字節數組字段去存儲從文件或者其他外部資源讀取到的字節數據。
然后所有read方法都是將字節數據在byte[]中進行處理操作,以及像mark,reset方法都是基于保存了字節數據的字節數組字段,然后移動posistion指針,修改byte[]中實際字節數量的count字段等等.....只是每種子類處理的場景是不同的,有些如PipedInputStream類中有Thread類型的readSide,WriteSide字段,是用于兩個線程進行管道通訊的輸入字節流;FileInputStream類內部則有一個FileDescriptor文件指針,是直接指向文件的輸入流。
總的來說,基于InputStream的實現子類都有這樣特性:該類字節流都是將外部資源讀取成字節數據流,通過read方法,將這些字節流保存到底層的byte[]數組中,然后就會有如pos,count,mark等變量用于保存讀取指針位置,字節數組中字節數量,標記位置等變量來維護字節流于字節數組的關系
。目的當然都是將輸入源中的字節根據需求讀取到某個容器進行保存,程序中可以根據容器獲取字節數據。
那么具體的這個字節數組長什么樣,里面的數據具體是什么樣子的呢?下面可以通過一個文件輸入流的例子,將桌面的一個text文件中的數據,以字節流方法讀取到程序中,在程序中的字節數組變量調試查看這些字節數組,然后再可以根據這些字節數組數據與text文件進行對比:(注意當使用不同的編碼方式保存文件信息,就會得到不同數量的字節數,當讀取文件成字節數組也就不同。同一個文件,分別使用gbk與utf8編碼后,就會得到不同的字節數)
@Test
public void InputStreamTest() throws Exception{
InputStream fis = new FileInputStream("C:/Users/_Fan/Desktop/demo.txt");
byte[] buf = new byte[fis.available()];
fis.read(buf,0,buf.length);
// System.out.println(new String(buf,"gbk"));
System.out.println("字節數量:>>"+buf.length);
fis.close();
}
demo.txt文件中保存的只是一些網站鏈接,使用的是系統gbk編碼。從上圖可以看到,我通過FileInputStream輸出流,將demo.txt中的數據以字節流的進行讀取出來,保存到buf字節數組中。
然后通過調試,可以看到buf內的所有字節數組,每個字節是8位,所以都是對應著一個-128~+127整數數值。
其實在計算機內存中,demo.txt中的所有字符串都是二進制字節,可以看到該文件一共有221個字節。
當我們通過文本工具打開文件時候,就會根據這些二進制字節數據解碼(因為都是英文字母,所以按照gbk解碼也不會出現亂碼),這些每個字節其實對應著一個英文字母。(若是文本中有負數,那么就不是對應一個字節碼了,就會通過補碼方式保存成多個字節)打開后,就可以看到這些由字節數據解碼成的英文字符串了。
反過來,該text文件就是字節數據集合,當我們通過FileInputStream讀取該文件時候,這些字節就會被讀取保存到字節數組buf中,我們就可以看到如上圖綠色框中的整數字節數組了。因為demo.txt文件中第一個英文字母是h,對應一個8位的二進制字節就是104(0110 1000)。
所以當我們讀取這個文件字節流完時候,buf中的第一個字節就是h(104)了。 ASCII編碼轉換
那么為什么java中的有符號的byte的范圍是-128~127呢,因為一個byte是8位,簡單的說就是2^8=256,那么正負數對半分,正數與負數應該都是128個,正數從0~+127就是128個數,那么負數呢,因為計算機在內存中存儲負數是用補碼表示,按照正常邏輯負數應該是-0~-127對吧,但是有個特殊的-0,對應的二進制表示為1000 0000,就把他當做一個負數,對他求補碼(取反+1),最后就得到了-128,所以,在負數這邊的128個取值是從-1~-128。(1111 1111 ~ 1000 0000)
,這樣就是128個對半分啦~~
在了解了字節輸入流如何讀取資源數據,并通過代碼調試看到讀取到字節流內的字節數據具體形態,那么這些不同的字節輸入流分別適合在什么情況下使用呢?
FileInputStream
: 該類適合在知道要讀取的數據源是文件,程序要獲取文件中的字節數據情況下,可以通過傳遞文件絕對路徑,或者文件File對象到FileInputStream構造器中與文件資源連接,創建字節輸入流通道。
[文件資源 --> FileInputStream輸入字節流 ---> 程序/內存(字節數組)]
FilterInputStream
: 過濾功能的字節輸入流,在該類中會有一個底層的InputStream類型的輸入流作為基礎數據流,FilterInputStream類就是在該基礎數據流上,提供額外的其他處理,給基礎流增加新的功能,進行封裝。最常用的該處理流就是BufferedInputStream類。
該類是在基礎字節流基礎上,提供了字節數據緩沖功能,字節流在操作的時候本身是不會用到緩沖區(內存)的,是與文件本身直接操作的。所以使用緩沖字節輸入流時候,會在內存中創建一個byte[]作為緩沖區,將基礎字節流中的字節數據分批讀取到這個緩沖區中,每當緩沖區數據讀取完之后,基礎輸入流會再次填充緩沖區,如此反復,直到我們讀取完輸入流所有的字節數據,
這樣,就能對該緩沖區進行mark標記,reset重置等方法,快速的獲取字節數據,增加了每次讀取的字節數量,大大提高了字節讀取效率,而不是每次讀取字節流都要與外部文件等資源進行IO交互,消耗額外時間與資源
。(該緩沖輸入流中最重要的兩個方法個人認為是fill()和read1()方法
前者是將基礎字節流數據讀取到緩沖區中,后者是說明了每次讀取字節都是從緩沖區中去取的....)
//BufferedInputStream.java
public
class BufferedInputStream extends FilterInputStream {
private static int defaultBufferSize = 8192;//,默認的緩沖區大小
protected volatile byte buf[];//緩沖數組
/*
* 當前緩沖區的有效字節數
* 注意,這里是指緩沖區的有效字節數,而不是輸入流中的有效字節數。
*/
protected int count;
// 當前緩沖區的位置索引
// 注意,這里是指緩沖區的位置索引,而不是輸入流中的位置索引。
protected int pos;
/*
* 當前緩沖區的標記位置
* markpos與reset()方法配合使用才有意義:
* (1) 通過mark()函數,保存當前pos位置索引到markpos中。
* (2) 通過reset()函數,會將pos的值重置為markpos。當再次使用read()讀取數據時候,
* 就會從上面的mark()標記的位置開始讀取數據。
*/
protected int markpos = -1;
/*
* marklimit是緩沖區可標記位置的最大值。
*/
protected int marklimit;
...
//得到基礎字節輸入流in
private InputStream getInIfOpen() throws IOException {
InputStream input = in;
if (input == null)
throw new IOException("Stream closed");
return input;
}
/**
* 得到緩沖區字節數組對象
*/
private byte[] getBufIfOpen() throws IOException {
byte[] buffer = buf;
if (buffer == null)
throw new IOException("Stream closed");
return buffer;
}
/**
* 從基礎字節輸入流中讀取一部分數據,更新到內存緩沖區中。
*/
private void fill() throws IOException {
byte[] buffer = getBufIfOpen();
if (markpos < 0)
pos = 0; /* no mark: throw away the buffer */
else if (pos >= buffer.length) /* no room left in buffer */
if (markpos > 0) { /* can throw away early part of the buffer */
int sz = pos - markpos;
System.arraycopy(buffer, markpos, buffer, 0, sz);
pos = sz;
markpos = 0;
} else if (buffer.length >= marklimit) {
markpos = -1; /* buffer got too big, invalidate mark */
pos = 0; /* drop buffer contents */
} else { /* grow buffer */
int nsz = pos * 2;
if (nsz > marklimit)
nsz = marklimit;
byte nbuf[] = new byte[nsz];
System.arraycopy(buffer, 0, nbuf, 0, pos);
if (!bufUpdater.compareAndSet(this, buffer, nbuf)) {
throw new IOException("Stream closed");
}
buffer = nbuf;
}
count = pos;
int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
if (n > 0)
count = n + pos;
}
/*
* 從緩沖區中讀取一個字節,因為字節范圍是-128+127,
* 所以可以經過 & 0xff得到一個字節對應的整數值。
*/
public synchronized int read() throws IOException {
if (pos >= count) {
fill();
if (pos >= count)
return -1;
}
//根據當前索引位置pos得到緩沖數組對應byte字節,并將pos向后移動一位
return getBufIfOpen()[pos++] & 0xff;
}
/*
* 從緩沖區中讀取len個字節,并將讀取的字節拷貝到入參b中
*/
private int read1(byte[] b, int off, int len) throws IOException {
int avail = count - pos; //得到緩沖區中還能讀取到的字節數量
if (avail <= 0) {
if (len >= getBufIfOpen().length && markpos < 0) {
return getInIfOpen().read(b, off, len);
}
//若已經讀完緩沖區中的數據,則調用fill()從輸入流讀取下一部分數據來填充緩沖區
fill();
avail = count - pos;
if (avail <= 0) return -1;
}
int cnt = (avail < len) ? avail : len;
//將buf[pos] ~ buf[pos + cnt]字節數組拷貝到b中
System.arraycopy(getBufIfOpen(), pos, b, off, cnt);
pos += cnt;
return cnt;
}
...
}
可以從源代碼大致看出,程序對字節的讀取都是從內存中的字節緩沖區中獲取數據的,在緩沖區中又會根據緩沖區狀態標記如pos
(當前讀取位置指針),count
(緩沖區中有效字節數),markpos
(pos標記)等等對緩沖區進行移動讀取,若是緩沖區數據讀取完后,又通過fill()方法,將基礎字節流的數據讀取部分更新到緩沖區中,直到基礎字節流中數據被讀取完。
簡單看看BufferedInputStream的使用,并使用JUNIT調試看看內部如何執行?根據代碼斷點查看程序執行流程:
@Test
public void BufferedInputStreamTest() throws Exception{
InputStream fis = new FileInputStream("C:/Users/_Fan/Desktop/tt.txt");
byte[] buf = new byte[fis.available()];//保存讀取到的文件字節數組
(斷點)BufferedInputStream bis = new BufferedInputStream(fis); //在文件字節流基礎上,添加字節緩沖功能
(斷點)bis.read(buf,0,buf.length); //將緩沖區中的數據拷貝到buf字節數組中
System.out.println(new String(buf,"utf8"));
System.out.println("字節數量:>>"+buf.length);
fis.close();
}
調試第一步:在BufferedInputStream構造器處后:
通過文件基礎輸入流available可知buf長度為6,此時buf = [0,0,0,0,0]。然后在此基礎流上,添加緩沖功能。此時,BufferedInputStream對象bis屬性值: buf=[8192個0],count =0, in = FileInputStream(fis),marklimit=0, markpos =-1,pos=0。向下,跳到第二個斷點。
第二個斷點:BufferedIntputStream對象bis創建成功,所有內部屬性都進行了初始化。進入read方法:
進入BufferedInptuStream.read(b,off,len)方法,做一些基礎判斷后,調用私有read1(b,off+n,len -n)方法,此時,bis對象狀態沒有改變。
進入read1方法,通過首次判斷int avail = count - pos;
,因為count=pos=0,所以avail<0,說明此刻緩沖區中沒有可讀字節數據。
之后,就會調用非常重要的方法fill(),目的將基礎流fis中字節數據更新到緩沖區中,進入fill方法:
重要的來了,調用getInIfOpen().read(buffer, pos, buffer.length - pos);方法,實質上getInIfOpen()方法就是返回FileInputStream類型fis對象,也就是bis對象的in屬性,底層調用的就是fis.read(byte[],int,int)方法,將文件字節流中的數據讀取到緩沖區buf中,
此刻,bis屬性中的buf緩沖區的數據有變動了,從buf=[8192個0],讀取數據之后,變成了buf=[-17, -69, -65, -28, -67, -96,(8192-6)個0]。因為fis文件中有6個字節。然后更新count=6。
之后又回調到前面的read1方法,后面的自己數組操作都是用bis中的buf緩沖區數據進行操作了。在操作中就不斷更新count,pos,markpos等屬性....
[read() ---> read1() ---> fill()[將基礎字節流數據轉移到緩沖區buf] ---> read1() ---> read()]
結束調試后輸出:(因為中文用UTF8編碼,有三個字節是用于標識UTF8編碼的字節:ef bb bf )
你
字節數量:>>6
那為什么不一次性將所有的基礎輸入字節流數據保存到內存中的緩沖區byte[]中呢?
首先,因為默認的緩沖區大小是8kb,若是要讀取的外部資源文件很大呢?幾兆?幾十兆?那這樣一次讀寫數據會占用IO資源,消耗時間。何不每次讀取部分,程序先使用緩沖區中字節數據,若是用完,每次再從基礎輸入流中讀取字節呢。
第二當然是希望盡可能不占用內存資源了,若是多線程,多個IO任務在執行,每個線程要讀取1M,20M的字節流,那么積累起來,也占用相當大的內存空間,因為緩沖區都是建立在內存中的。(這里有篇說BufferedInputStream挺好的文章,可學習參考:BufferedInputStream(緩沖輸入流)的認知、源碼和示例)
ByteArrayInputStream:
該類是字節數組輸入流,其實與BufferInputStream類似,該字節數組輸入流內部也有一個byte[]字節數組緩沖流,所以使用該類來讀取數據或者程序使用,都是通過字節數組來實現的。
那通常這些帶有緩沖區的字節輸入流適合什么時候用?當我們要處理一個內部或者外部的字節數組資源的時候(字節輸入流來源是字節數組),就可以通過構造器將這個字節數組傳入ByteArrayInptuStream中,內部底層就把這個字節數組作為緩沖區數組,后續的針對字節的操作就可以使用這個緩沖區來完成。
public
class ByteArrayInputStream extends InputStream {
//保存輸入流數據的字節數組緩沖區
protected byte buf[];
//下一個會被讀取的字節位置索引(緩沖區讀取指針)
protected int pos;
//緩沖區標記索引
protected int mark = 0;
//緩沖區字節流的長度
protected int count;
//通過構造器將輸入字節數組傳入內部,創建一個內容為buf的緩沖區字節流
public ByteArrayInputStream(byte buf[]) {
this.buf = buf;
this.pos = 0;
this.count = buf.length;
}
//通過構造器將輸入字節數組傳入內部,創建一個內容為buf的緩沖區字節流
//offset為讀取指針起始位置,length為讀取偏移量長度
public ByteArrayInputStream(byte buf[], int offset, int length) {
this.buf = buf;
this.pos = offset;
this.count = Math.min(offset + length, buf.length);
this.mark = offset;
}
...
}
有時候需要想想,在我們讀取一些小文件,字節數少的資源,其實不使用帶有緩沖功能的輸入流也是可以的。但是,當輸入源字節數量大的時候,為了提高IO效率,就可以使用緩沖區機制,將輸入源大量的字節數據,一批批,一次次的將數據拷貝到內存緩沖區,程序從內存緩沖區獲取速度那就當然快了,效率也高了。
也來看看字節數入流的簡單操作:主要目的就是處理傳入的字節數組。
@Test
public void byteArrayInputStreamTest() throws Exception{
InputStream fis = new FileInputStream("C:/Users/_Fan/Desktop/tt.txt");
byte[] buf = new byte[fis.available()];//保存讀取到的文件字節數組
fis.read(buf,0,buf.length);//通過文件字節流將內容保存到buf字節數組中
byte[] dest = new byte[buf.length];//創建一個被字節數組輸入流使用的數組
ByteArrayInputStream bais = new ByteArrayInputStream(buf);
bais.read(dest,0,dest.length);//通過bais將輸入的字節數組拷貝到dest中
System.out.println(new String(dest,"utf8"));
System.out.println("字節數量:>>"+dest.length);
fis.close();
}
- ObjectInputStream: 是對象序列化是讀取對象字節信息的類。序列化問題需要找時間在總結總結。
- PipedInputStream: 該類是用于兩個線程通訊的通道輸入流,一個線程的輸出作為另外一個線程的輸入,組成字節流通道。
- SequenceInputStream: 用于邏輯上關聯多個輸入流的讀取順序。從輸入流A-->B--->C。
這里呢,還多說一點,關于java系統中的標準輸入流:System.in。可以看到,這個流連接著鍵盤字節流輸入或者其他主機環境或者用戶指定的輸入源。(常用的場景就是我們要通過控制臺鍵盤輸入數據,就可以通過System.in流得到輸入數據)
//System.java
public final class System {
/**
* The "standard" input stream. This stream is already
* open and ready to supply input data. Typically this stream
* corresponds to keyboard input or another input source specified by
* the host environment or user.
*/
public final static InputStream in = null;
...
}
Reader字符輸入流
與InputStream都是輸入流,只是流中基礎數據類型不一樣。InputStream中的是字節數據(raw bytes),Reader則是字符數據。在java中,最簡單區別兩者方式可以從兩者存儲數據使用的字節數,byte類型是一個字節,字符char則是使用兩個字節存儲。因為我們也會經常想直接讀取某個外部資源內的字符數據,而不是更底層的字節。當從資源中讀取到的字符流,一般就是我們可以直接看懂的,就不用再次通過字節數據轉換。無論是英文字符串還是中文內容,都可以用char來存儲表示。這樣對于我們程序處理也更方便。通常字節流讀取是使用緩沖流進行保存字節和程序處理的。
下面通過FileReader來做個簡單例子,再通過源代碼來查看內部數據是如何流動的:
@Test
public void FileReaderTest() throws Exception{
Reader reader = new FileReader("C:/Users/_Fan/Desktop/tt.txt");
char[] cbuf = new char[10];
// reader.read(cbuf,0,cbuf.length);
int len = 0;
while((len = reader.read(cbuf)) != -1){
System.out.println(new String(cbuf,0,len));
}
System.out.println("java中的char字節大小:>>"+Character.SIZE/8);
byte[] bytes = getBytes(cbuf);
System.out.println("轉換成字節數大小:>>"+bytes.length);
reader.close();
}
================================
//輸出
hello哈
java中的char字節大小:>>2
轉換成字節數大小:>>23
(5個字母(5個字節)+ 1個中文(3個utf8標識字節+3個存儲中文漢字的字節)) = 5+6 = 11個字節,
剩下的12字節就是剩下(10 - 6) = 4個字符得到的字節數。
================================
/**
* getBytes : 將字符數組轉換成字節數組
* @param chars
* @return
*/
private byte[] getBytes(char[] chars){
Charset cs = Charset.forName("utf-8");
CharBuffer cb = CharBuffer.allocate(chars.length);
cb.put(chars);
cb.flip();
ByteBuffer bb = cs.encode(cb);//字符串-->字節(編碼)
return bb.array();
}
那么,需要通過斷點調試看看這個字符數組是什么樣的?將字符數組轉換成字節數組又會變成什么樣子?通過下面的程序調試截圖:
從上面可以看到,當使用字符輸入流Reader以及相關子類來獲取輸入源內的字符時候,會將字符保存到char[]字符數組中。每個數組索引位置處,都保存著輸入源中的一個字符,而不是字節。因為在java中,一個char在內存中最大可以開辟兩個字節來保存字符,大多數字符都可以在內存中保存,包括漢字(這里的"哈"對應Unicode編碼為"\u54c8")
因為文件是用utf編碼,按照utf存儲規則,漢字"哈"需要用三個字節來存儲
。[更進一步說明,當在磁盤文件中按照utf存儲漢字的規則,需要將54c8兩個字節進行拆分成三個字節(-27,-109,-120)保存在磁盤上,當程序讀取文件的時候,根據utf8編碼標識得知文件使用utf8編碼存儲,就會按照特定規則取出字節碼,又解碼拼裝成兩個字節的54c8存儲在char中,程序就能正確看到中文漢字]
char ha = '\u54c8';
System.out.println(new String("\u54c8"));//哈
System.out.println(ha);//哈
那么,FileReader內部是如何實現字符讀取的呢?與前面的直接讀取字節流有什么聯系么?為什么可以直接將字節文件讀取成字符數組呢,通過源碼來看看啦:
//通過構造器創建文件字符流輸入:
//Reader reader = new FileReader("C:/Users/_Fan/Desktop/tt.txt");
//FileReader.java
/*
* FileReader繼承自InputStreamReader類,
* 所以實際FileReader.read()調用的是InputStreamReader的read方法。
*/
public class FileReader extends InputStreamReader {
//調用父類InputStreamReader,在FileInputStream文件字節流基礎上處理
public FileReader(String fileName) throws FileNotFoundException {
super(new FileInputStream(fileName));
...
}
所以,當調用reader.read(cbuf,0,cbuf.length);
方法,實際上是通過委托給父類InputStreamReader類來處理。因為InputStreamReader是個處理流,也是轉換流把,作用是將字節轉換為字符。所以,FileReader文件字符輸入流,其實底層還是由FileInputStream文件字節輸入流來連接外部的文件資源,然后在字節流基礎上,InputStreamReader類在再將這些字節流轉換為字符流,最后可以保存到char[]字符數組中。
關于這個InputStream轉換流,放到下一節進行詳說。
由Reaer系列類圖也可以看出,這些常規的字節輸入流是每次進行一次數據讀取,都會進行一次物理IO操作,當然費時費資源了,所以,與InputStream系列中的BufferedInputStream帶有緩沖功能的字節輸入一樣,這里的帶有緩沖功能的字符輸入流BufferedReader也要來看看了,看看內部是不是與FileReader依賴FileInputStream一樣,這個BufferedReader底層也是依賴于帶有緩沖功能的字節輸入流BufferedInputStream,以及內部外部文件字節資源與緩沖區數據的更新機制是怎么的?
@Test
public void FileReaderTest() throws Exception{
Reader reader = new FileReader("C:/Users/_Fan/Desktop/tt.txt");
char[] cbuf = new char[10];
// reader.read(cbuf,0,cbuf.length);
/*當基礎流已經使用read讀取過后,再次讀取數據到緩沖區就沒有數據了
* eg: 若在br.read()之前,reader已經調用了read(cbuf,0,cbuf.length)
* 方法,一次性將文件中的字節流數據讀取完了,那么此時br.read()將會讀取不到數據,
* 因為原始基礎流已經沒有數據了。
*/
BufferedReader br = new BufferedReader(reader);//在FileReader流上添加字符緩沖
br.read(cbuf, 0, cbuf.length);
// String line = br.readLine();
System.out.println(new String(cbuf));
br.close();
reader.close();
}
可以從用法與源碼中可知:
字符緩沖流是在字符流Reader基礎上,底層通過在內存中創建char[]字符數組當做緩沖區來使用
。
而字符輸入流Reader又是在字節流InputStream基礎上來實現字符數組數據的讀取
。
所以,相當于BufferedReader字符緩沖流是在InputStream字節輸入流上套了兩層過濾與處理功能,才能達到字符緩沖區功能,他底層方法調用過程:
[BufferedReader.read() --->read1()--->fill(in.read(cb, dst, cb.length - dst)通過基礎字符流讀取字符,更新到緩沖區中) --->read1() --> read() ]
,差不多就是這么個過程,其中,基礎字符流讀取字符過程,也是有字節流支持的。至于如何將字節流轉換成字符流,就要看InputStreamReader如何實現的了。
所以,總的來說,無論是字節輸入流,字符輸入流都是需要先將外部資源文件通過字節輸入流讀取到程序中,然后在此基礎上,可以將字節流轉成字符流或者添加什么其他的緩沖等功能都是看需求與各自類中定義的實現。
BufferedReader對于讀取文件內容字符輸入流是個不錯的選擇,其內部還有一個readLine方法,可以按照文件每行字符數據進行讀取,底層是通過StringBuffer對象,通過循環讀取文件字符,通過判斷是否是換行符號來讀取每行字符。也是很有用的方法。結合char[]緩沖,按行讀取文件字符內容,效率也高,但是也要注意文件的編碼設置。
還有一個字符數組輸入流也是常見的常用的,就是CharArrayReader類。可以通過傳入一個char[]字符數組到構造器,之后,可以通過CharArrayReader的char[] buf緩沖區來對這些字符數據流進行操作,也是給某些字符數組添加緩沖,和一些其他的處理,更高效的使用這些字符數組。
@Test
public void CharArrayReaderTest() throws Exception{
Reader reader = new FileReader("C:/Users/_Fan/Desktop/tt.txt");
char[] cbuf = new char[10];
reader.read(cbuf,0,cbuf.length);//將文件中字符保存到cbuf字符數組中
//將cbuf傳入CharArrayReader,通過緩沖對字符數組處理
CharArrayReader car = new CharArrayReader(cbuf);
char[] temp = new char[cbuf.length];
car.read(temp,0,temp.length);//將字符緩沖區的字符拷貝到temp中
System.out.println(new String(temp));
reader.close();
}
根據類圖,字符輸入流系列的一些處理,數據源可以是文件,字符數組等等,目的就是程序要高效處理字符數組。還有一些其他的字符輸入流就做簡單說明,因為每個使用場景都是可以單獨來細說的。知道有這么個玩意就行啦..嘿嘿...
Reader系列的子類,多數都是可以與InputStream中分別對應,包括這些使用流處理場景。這里可以省略過。但是,有個非常重要的子類InputStreamReader這個處理輸入流,主要作用是將字節流轉換成字符流。Reader中挺多子類都是要依賴此類進行輸入流處理。更詳細說明,可以放到下一節中。
輸入字節流與字符流的轉換
InputStreamReader:輸入處理流,主要是包裝字節流,將這些字節流轉換成字符流。在將字節轉換成字符,需要StreamDecoder類來處理。所以,就主要來看看StreamDecoder內部實現,就能大致清除了解字節是如何轉換成字符的?數據流是如何流動傳輸的等等。就取上面的FileReader的列子進行說明:
- 看看FileReader構造器調用:
public FileReader(String fileName) throws FileNotFoundException {
super(new FileInputStream(fileName));//調用InputStreamReader()
}
- 可以看到,實際是通過調用父類InputStreamReader轉換流的構造器:
(重要的是,還新建了個FileInputStream文件字節流傳參,說明字符流的讀取與字節流密切相關,或者說是依賴字節輸入流)
private final StreamDecoder sd;
public InputStreamReader(InputStream in) {
super(in);
try {
sd = StreamDecoder.forInputStreamReader(in, this, (String)null); // ## check lock object
} catch (UnsupportedEncodingException e) {
// The default encoding should always be available
throw new Error(e);
}
}
- 可以看到,將FileInputStream作為in參數傳遞給內部的StreamDecoder類的構造器:
public static StreamDecoder forInputStreamReader(InputStream in,
Object lock,
String charsetName)
throws UnsupportedEncodingException
{
String csn = charsetName; //傳遞的charsetName == null
if (csn == null)
csn = Charset.defaultCharset().name(); //查詢系統默認字符編碼集
try {
if (Charset.isSupported(csn))//支持編碼
return new StreamDecoder(in, lock, Charset.forName(csn));//調用構造函數
} catch (IllegalCharsetNameException x) { }
throw new UnsupportedEncodingException (csn);
}
...
//字符集對象
private Charset cs;
//字符解碼:字節-->字符
private CharsetDecoder decoder;
private ByteBuffer bb;
// Exactly one of these is non-null
private InputStream in;//字節輸入流,這里通過構造器傳遞的是FileInputStream in.
private ReadableByteChannel ch;//nio,字節通道對象
StreamDecoder(InputStream in, Object lock, Charset cs) {
this(in, lock,
cs.newDecoder()
.onMalformedInput(CodingErrorAction.REPLACE)
.onUnmappableCharacter(CodingErrorAction.REPLACE));
}
/*
* 初始化好字節讀取通道,字節緩沖區,字符解碼器等等對象。
* 為后續將字節流轉換成字符流做鋪墊
*/
StreamDecoder(InputStream in, Object lock, CharsetDecoder dec) {
super(lock);
this.cs = dec.charset();
this.decoder = dec;
// This path disabled until direct buffers are faster
if (false && in instanceof FileInputStream) {
ch = getChannel((FileInputStream)in); //返回in.getChannel();默認支持通道
if (ch != null) //若是支持通道,那么直接從堆外內存開辟地址給緩沖字節流
bb = ByteBuffer.allocateDirect(DEFAULT_BYTE_BUFFER_SIZE);
}
if (ch == null) {
this.in = in;
this.ch = null;
//堆內內存
bb = ByteBuffer.allocate(DEFAULT_BYTE_BUFFER_SIZE);
}
bb.flip(); // So that bb is initially empty
}
- 在StreamDecoder對象等默認配置初始化好后,看看FileReader.read是如何讀取字符數組的:
Reader r = new FileReader("C:/Users/XianSky/Desktop/readme.txt");
char[] cbuf = new char[20];
r.read(cbuf, 0, cbuf.length);
- 然后,調用的是InputStreamReader.read方法:
public int read(char cbuf[], int offset, int length) throws IOException {
return sd.read(cbuf, offset, length);
}
- 可以看到是代理給StreamDecoder對象sd進行讀取:
public int read(char cbuf[], int offset, int length) throws IOException {
int off = offset;
int len = length;
synchronized (lock) {
ensureOpen();
...
return n + implRead(cbuf, off, off + len);
}
}
/*
* 可以看到,StreamDecoder實際上就是使用CharBuffer,ByteBuffer的互相轉換來對
* FileInputStream讀取到的字節流轉換成字符流。
*/
int implRead(char[] cbuf, int off, int end) throws IOException {
...
//先將cbuf使用緩沖區包裝起來
CharBuffer cb = CharBuffer.wrap(cbuf, off, end - off);
if (cb.position() != 0)
// Ensure that cb[0] == cbuf[off]
cb = cb.slice();
boolean eof = false;
for (;;) {
//將ByteBuffer bb字節緩沖區內字節進行解碼成字符
CoderResult cr = decoder.decode(bb, cb, eof);
if (cr.isUnderflow()) {
if (eof)
break;
if (!cb.hasRemaining())
break;
if ((cb.position() > 0) && !inReady())
break; // Block at most once
int n = readBytes();//實際將字節數組轉換成字符數組方法
if (n < 0) {
eof = true;
if ((cb.position() == 0) && (!bb.hasRemaining()))
break;
decoder.reset();
}
continue;
}
...
cr.throwException();
}
....
return cb.position();
}
-
當sd.read方法調用,返回的時候,char[] cbuf字符數組已經把文件的字符都讀取出來了。具體的StreamDecoder的read時序圖如下:
StreamDecoder.png
綜上呢,InputStreamReader轉換流呢,主要就是利用StreamDecoder將StreamDecoder內部的InputStream對象讀取到的字節流,利用CharBuffer,ByteBuffer,ReableByteChannel等NEW IO的算法來對字節流進行解碼處理,轉換成字符流數組返回到最開始讀取字符的方法處,并且,此時參數中的char[]也已經被StreamDecoder解碼的字節流填充完了。。。
參考:
JAVA IO - 白大蝦
BufferedInputStream(緩沖輸入流)的認知、源碼和示例
JAVA常用類庫/JAVA IO