前提
參考資料:
- 《Java I/O》 -- 這本書沒有翻譯版,需要自己啃一下。
《Java I/O》這本書主要介紹了IO和NIO的相關(guān)API使用,但是NIO部分并不是太專業(yè),同系列的動物書《Java NIO》相對比較詳細(xì)并且有譯本,因此看本書的時候,我直接跳過了NIO部分。
IO概述
IO實(shí)際上是INPUT/OUTPUT(輸入/輸出)的簡寫,IO是任何計算機(jī)操作系統(tǒng)或編程語言的基礎(chǔ)。Java中,IO相關(guān)的類庫主要分布在java.io和java.nio兩個包中。
IO分類
從目前來看,IO主要分為BIO、NIO和AIO。
BIO,一般又稱為OIO(Old IO),意為Blocking-IO(阻塞IO),在此參考書中稱為Basic-IO(基礎(chǔ)IO)。這篇文章主要正是針對BIO進(jìn)行總結(jié)。BIO的核心API都是圍繞InputStream(輸入流)和OutputStream(輸出流),Reader和Writer,輸入流和輸出流都是面向字節(jié)的,而Reader和Writer可以說是面向字符的。
NIO,一般又叫New IO,意為Non-Blocking IO,即非阻塞IO,核心組件是Buffer、Channel、Selector。
AIO,意為Asynchronous IO,即異步IO,基于NIO引入了新的異步通道的概念,主要提供了異步文件通道和異步套接字通道的實(shí)現(xiàn)。
當(dāng)然,這里只是簡單說一下三種IO的概念,遲點(diǎn)讀完了《Unix網(wǎng)絡(luò)編程》后再做一次詳細(xì)的總結(jié)。
什么是流(Stream)
流(Stream)是一個不定長度的有序的字節(jié)序列(個人認(rèn)為,這個是最準(zhǔn)確和最精煉的流的定義)。Java中對流進(jìn)行了抽象,輸入流的抽象父類是java.io.InputStream(下面叫輸入流),輸出流的抽象父類是java.io.OutputStream(下面叫輸出流),這兩個父類定義了從字節(jié)來源讀取字節(jié)的方法以及向目標(biāo)源輸出字節(jié)的統(tǒng)一方法,這樣的好處是我們不需要刻意去知道流的輸入源或者流的輸出目的地到底是什么。而具體的輸入來源或者輸出目的地分別由InputStream或者OutputStream的具體子類確定。
另外,在BIO里面,是沒有"字符流"的概念(說實(shí)話,至少從我目前看到的資料來看沒有出現(xiàn)過相關(guān)字眼,除了一些博客文章之外),但是有提供了java.io.Reader(下面叫Reader)用于讀取字符,java.io.Writer(下面叫Writer)用于寫入字符。實(shí)際上在需要從外部來源讀取字符或者輸出字符到外部目標(biāo)的時候,Reader是從字節(jié)源讀取字節(jié),再把讀取到的字節(jié)數(shù)組轉(zhuǎn)換為字符數(shù)組;Writer是把字符數(shù)組轉(zhuǎn)換為字節(jié)數(shù)組,再輸出到外部目標(biāo)里面,最常見的是FileReader和FileWriter。在上述這種情況下,Reader和Writer都是使用了十分典型的裝飾器模式,下面就以Reader和Writer對文件的操作為例,貼點(diǎn)源碼再畫個圖說明這個問題。
先看一下FileReader:
public class FileReader extends InputStreamReader {
public FileReader(String fileName) throws FileNotFoundException {
super(new FileInputStream(fileName));
}
//暫時忽略其他代碼
}
public class InputStreamReader extends Reader {
private final StreamDecoder sd;
public InputStreamReader(InputStream in, String charsetName) throws UnsupportedEncodingException {
super(in);
if (charsetName == null)
throw new NullPointerException("charsetName");
sd = StreamDecoder.forInputStreamReader(in, this, charsetName);
}
}
見InputStreamReader里面實(shí)例化的StreamDecoder實(shí)例,就是用于把byte數(shù)組轉(zhuǎn)換為char數(shù)組,而byte數(shù)組來源于FileInputStream實(shí)例對應(yīng)的文件中。
再看一下FileWriter:
public class FileWriter extends OutputStreamWriter {
public FileWriter(String fileName) throws IOException {
super(new FileOutputStream(fileName));
}
//暫時忽略其他代碼
}
public class OutputStreamWriter extends Writer {
private final StreamEncoder se;
public OutputStreamWriter(OutputStream out, String charsetName) throws UnsupportedEncodingException {
super(out);
if (charsetName == null)
throw new NullPointerException("charsetName");
se = StreamEncoder.forOutputStreamWriter(out, this, charsetName);
}
}
見OutputStreamWriter里面實(shí)例化的StreamEncoder實(shí)例,就是用于把char數(shù)組轉(zhuǎn)換為byte數(shù)組,而byte數(shù)組存放在FileOutputStream實(shí)例中最終用于輸出到目標(biāo)文件。
簡單來看,大致就是下圖的流程:

當(dāng)然,像一般只用于內(nèi)存態(tài)的CharArrayWriter或者CharArrayReader等等,它們內(nèi)部就維護(hù)著一個char數(shù)組,寫入和輸出的數(shù)據(jù)都是基于char數(shù)組。
輸入流和輸出流
InputStream
java.io.InputStream是所有輸入流的抽象父類,提供三個基本方法用于從流中數(shù)據(jù)字節(jié)。另外,它還提供關(guān)閉流、檢查剩余可以讀取的字節(jié)序列長度、跳過指定長度、標(biāo)記流中的位置并重新設(shè)置當(dāng)前讀取位置、檢查是否支持標(biāo)記和重置。
//讀取單字節(jié)數(shù)據(jù),返回的是"無符號的byte"類型值[0,255],因?yàn)椴淮嬖跓o符號byte類型,所以返回值是int
public abstract int read( ) throws IOException
//讀取字節(jié)數(shù)組到指定的byte數(shù)組中
public int read(byte b[]) throws IOException
//讀取字節(jié)數(shù)組到指定的byte數(shù)組中,可以指定偏移量和總長度
public int read(byte b[], int off, int len) throws IOException
//跳過指定長度的字節(jié)序列
public long skip(long n) throws IOException
//返回剩余可讀取的字節(jié)序列的長度
public int available() throws IOException
//關(guān)閉輸入流
public void close() throws IOException
//標(biāo)記
public synchronized void mark(int readlimit)
//重置
public synchronized void reset() throws IOException
//是否支持標(biāo)記和重置
public boolean markSupported()
讀取
public abstract int read( )是抽象方法,必須由子類實(shí)現(xiàn),它用于讀取單字節(jié)數(shù)據(jù),返回的是"無符號的byte"類型值[0,255](讀取到的字節(jié)數(shù)值),因?yàn)镴ava中不存在無符號byte類型,所以返回值是int。當(dāng)讀取到流的尾部,返回-1。相關(guān)的轉(zhuǎn)換公式偽代碼大致是:
byte b = xxxx;
int i = (b >= 0) ? b : 256 + b;
public int read(byte b[], int off, int len)用于讀取連續(xù)的數(shù)據(jù)塊到一個指定的字節(jié)數(shù)組中,off是用于指定數(shù)據(jù)寫入目標(biāo)byte數(shù)組的起始偏移量,len是用于指定讀取字節(jié)的最大長度(數(shù)量),返回值是當(dāng)前讀取到的字節(jié)數(shù)值(注意范圍是[0,255])。當(dāng)讀取到流的尾部,返回-1。注意此方法的返回值是讀取到的字節(jié)序列的長度,有可能是目標(biāo)字節(jié)數(shù)組的總長度,也有可能是來源的字節(jié)序列的總長度,這是因?yàn)轭A(yù)先建立的目標(biāo)字節(jié)數(shù)組有可能不能容納來源中的所有字節(jié)。變體方法public int read(byte b[])實(shí)際上是:
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}
read方法都是阻塞方法,直到有可讀字節(jié)、到達(dá)流的尾部返回-1或者拋出異常。
這里舉個栗子:
public static void main(String[] args) throws Exception {
ByteArrayInputStream inputStream = new ByteArrayInputStream("abcdefg".getBytes("UTF-8"));
byte[] array = new byte[10];
inputStream.read(array, 4, 2);
System.out.println(new String(array, "UTF-8"));
}
過程如下圖:

這里為了說明,偏移量off是指(將會被寫入)目標(biāo)byte數(shù)組寫入數(shù)據(jù)時候的數(shù)組下標(biāo),讀取字節(jié)序列的最大長度len是指將會讀取到的字節(jié)序列的總長度,但是需要注意,讀取字節(jié)序列的時候是從字節(jié)序列的首位開始讀取(很容易誤認(rèn)為從off開始讀取,其實(shí)off是控制寫入的偏移量)。因此上面程序執(zhí)行后控制臺打印字符串:ab。
值得注意的是InputStream的三個read方法變體適用于它的所有子類。
跳過
public long skip(long bytesToSkip)用于指定跳過的字節(jié)序列長度,返回值是真正跳過的字節(jié)序列長度,有可能比指定的bytesToSkip小。遇到流的尾部,會直接返回-1。這個方法的作用是可以選擇跳過一些不需要讀取到內(nèi)存的字節(jié)序列,以減少內(nèi)存消耗。
public static void main(String[] args) throws Exception{
ByteArrayInputStream inputStream = new ByteArrayInputStream("abcdefg".getBytes("UTF-8"));
byte[] array = new byte[10];
inputStream.skip(5);
inputStream.read(array,0, array.length);
System.out.println(new String(array,"UTF-8"));
}
最終輸出:fg,也就是跳過了前面的5個字節(jié)。
計算剩余可讀字節(jié)序列長度
public int available()方法用于返回在不阻塞的情況下可以讀取的總字節(jié)序列長度,如果沒有可讀字節(jié)則返回0。這個方法使用在本地文件(例如讀取磁盤文件數(shù)據(jù)的時候使用的FileInputStream)讀取的時候返回的值是真實(shí)可信的,但是在使用網(wǎng)絡(luò)流(例如套接字)則返回的值并不是真實(shí)的,因?yàn)榫W(wǎng)絡(luò)流是非阻塞的。
public static void main(String[] args) throws Exception {
ByteArrayInputStream inputStream = new ByteArrayInputStream("abcdefg".getBytes("UTF-8"));
int size = inputStream.available();
byte[] data = new byte[size];
inputStream.read(data);
System.out.println(new String(data, "UTF-8"));
}
最后控制臺輸出:abcdefg。使用available()方法可以提前預(yù)支需要寫入的字節(jié)數(shù)組的長度,但是需要警惕該方法返回值是否真實(shí),還有,是否有足夠的內(nèi)存初始化該長度的數(shù)組,否則有可能發(fā)生OOM。
標(biāo)記和重置
并不是所有的InputStream子類都支持標(biāo)記和重置,因此public boolean markSupported()方法就是用于判斷流實(shí)例是否支持標(biāo)記和重置。如果markSupported方法返回false而強(qiáng)制使用mark或者reset一般會拋出IOException。下面舉個栗子說明一下怎么使用:
public static void main(String[] args) throws Exception{
ByteArrayInputStream inputStream = new ByteArrayInputStream("abcdefg".getBytes("UTF-8"));
byte[] array = new byte[10];
//注意這里的readAheadLimit可以亂填,參考一下ByteArrayInputStream的源碼
inputStream.mark(10086);
inputStream.read(array,0, 4);
inputStream.reset();
inputStream.read(array,4, 4);
System.out.println(new String(array,"UTF-8"));
}
過程如下圖:

這里用ByteArrayInputStream為例,pos總是指向字節(jié)數(shù)組中下一個需要讀取的字節(jié)元素的下標(biāo),mark()方法調(diào)用時候,pos的值被記錄在mark變量中,而reset()方法調(diào)用的時候,pos的值被重置為mark的值。上面的例子最終輸出:abcdabcd。
關(guān)閉
輸入流的關(guān)閉可以顯式調(diào)用close()方法,一般需要把關(guān)閉方法置于finally塊中并且捕獲其異常不進(jìn)行異常拋出。關(guān)閉后的流不能再進(jìn)行讀取等操作,否則會拋出IOException。在Jdk1.7中引入了java.lang.AutoCloseable接口,實(shí)現(xiàn)了AutoCloseable接口的流可以使用try-resource的方式進(jìn)行編碼,這樣就不需要顯式關(guān)閉流。例如:
try (FileInputStream inputStream = new FileInputStream("xxxx")){
byte[] buffer = new byte[10];
inputStream.read(buffer);
//....
}
InputStream子類
InputStream的子類主要是用于區(qū)分不同的字節(jié)數(shù)據(jù)來源(或者直接叫數(shù)據(jù)源)。另外,InputStream的子類FilterInputStream使用了典型的裝飾器模式,一般稱這類流叫裝飾(輸入)流。常見的裝飾流主要是FilterInputStream的子類,包括BufferedInputStream、PushbackInputStream等等(還有很多隱藏在sun包下)。下面最要挑幾個常用的InputStream的子類來介紹一下使用方式。這里啰嗦再點(diǎn)一次:InputStream的三個read方法變體適用于它的所有子類。
下圖是InputStream的主要子類,不包含sun包下隱藏的類。

下面的分類只是按照個人的理解,并沒有科學(xué)的根據(jù)。
廢棄的子類:
- StringBufferInputStream,本來是設(shè)計用于讀取字符的,已過期,用StringReader替代。
- LineNumberInputStream,本來是設(shè)計用于讀取字符并且附帶行號記錄的功能,已過期,用LineNumberReader替代。
介質(zhì)流:
- ByteArrayInputStream,從byte數(shù)組中讀取數(shù)據(jù)。
- FileInputStream,從本地磁盤文件中讀取數(shù)據(jù)。
裝飾流:
ObjectInputStream和所有FilterInputStream的子類都是裝飾輸入流(裝飾器模式的主角)。
- ObjectInputStream,可以用于讀取對象,實(shí)際上是反序列化操作,但是對象類必須實(shí)現(xiàn)Serializable接口。
- PushbackInputStream,一般叫回退輸入流,這個比較特殊,可以把讀取進(jìn)來的某些數(shù)據(jù)重新回退到輸入流的緩沖區(qū)之中,也就是提供了回退機(jī)制。
- DataInputStream,提供從流中直接讀取具體數(shù)據(jù)類型的功能。
- BufferedInputStream,緩沖輸入流,為字節(jié)流讀取提供基于內(nèi)存的緩沖功能。
管道流:
- PipedInputStream,提供從與其它線程共用的管道中讀取數(shù)據(jù)的功能。
合并流:
- SequenceInputStream,用于合并多個輸入字節(jié)流。
內(nèi)存輸入流
ByteArrayInputStream(字節(jié)數(shù)組輸入流)主要是用于直接讀取byte數(shù)組(它內(nèi)部就維護(hù)了一個字節(jié)數(shù)組),操作都是基于內(nèi)存態(tài)的,它實(shí)現(xiàn)了InputStream的所有方法,也就是支持標(biāo)記和重置。例子見前面的分析,這里不做多余舉例。
文件輸入流
FileInputStream主要用于讀取文件內(nèi)容為字節(jié)數(shù)組,不支持標(biāo)記和重置,另外在JSR-51中它引入了FileChannel用于通過Channel讀取數(shù)據(jù)。
這里可能有疑惑,有時候可以用很小的byte數(shù)組用來做緩沖區(qū)完成文件的復(fù)制(文件的復(fù)制包括兩個步驟,分別是源文件內(nèi)容讀取到內(nèi)存中和內(nèi)存中的數(shù)據(jù)寫到目標(biāo)文件中),下面畫個圖解釋一下整個復(fù)制過程。假設(shè)場景:源文件1.log里面有10字節(jié)數(shù)據(jù),使用的緩沖字節(jié)數(shù)組的長度是6,目標(biāo)是把1.log的內(nèi)容拷貝到目標(biāo)文件2.log中。先在磁盤D建立一個文件1.log,文件內(nèi)容是:
abcdefghij
文件拷貝的代碼如下:
public static void main(String[] args) throws Exception {
byte[] buffer = new byte[6];
try (FileInputStream inputStream = new FileInputStream("D:\\1.log");
FileOutputStream outputStream = new FileOutputStream("D:\\2.log")) {
while (true) {
int len = inputStream.read(buffer);
if (len < 0) {
break;
}
outputStream.write(buffer, 0, len);
}
}
}
控制臺輸出:
Loop:1,len:6
Loop:2,len:4
Loop:3,len:-1
同時可以看到磁盤D中新建了一個2.log文件,內(nèi)容和1.log中完全一樣,這里暫時先忽略O(shè)utputStream的使用方式,畫圖理解整個過程。

每次進(jìn)行寫入的時候是根據(jù)read()方法返回的當(dāng)前讀取到的字節(jié)序列的長度并且指定寫時偏移量為0,總長度為讀取到的字節(jié)序列的長度實(shí)現(xiàn)的。所以緩沖字節(jié)數(shù)組中即使有上一次循環(huán)殘余的臟字節(jié),也不會影響此次循環(huán)的數(shù)據(jù)寫入。
回退輸入流
Java中輸入流都是采用順序的讀取方式,即對于一個輸入流來講都是采用從字節(jié)序列頭到字節(jié)序列尾的順序讀取的,如果在輸入流讀取到實(shí)際不需要的字節(jié),則只能通過程序?qū)⑦@些不需要的字節(jié)忽略,為了解決這樣的處理問題,在Java中提供了一種回退輸入流PushbackInputStream,可以把讀取到的字節(jié)重新回退到輸入流的緩沖字節(jié)數(shù)組之中。
public static void main(String[] args) throws Exception {
String message = "doge";
try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(message.getBytes());
PushbackInputStream pushbackInputStream = new PushbackInputStream(byteArrayInputStream)) {
int temp;
while ((temp = pushbackInputStream.read()) != -1) {
//如果讀取到'o'
if (temp == 'o') {
//把'o'放回去緩沖字節(jié)數(shù)組
pushbackInputStream.unread(temp);
//再讀一次
temp = pushbackInputStream.read();
System.out.print("<這是回退>" + (char) temp + "</這是回退>");
} else {
System.out.print((char) temp);
}
}
}
}
控制臺輸出:
d<這是回退>o</這是回退>ge
unread()方法主要是把讀取到的字節(jié)放回去(pos-1)的字節(jié)數(shù)組中的位置,很好理解,因?yàn)閜os總是指向下一個讀取到的字節(jié)元素。這里引用一篇文章的截圖說明一下其處理機(jī)制:

緩沖輸入流
BufferedInputStream繼承自FilterInputStream,它是典型的裝飾輸入流,它內(nèi)部提供了一個緩沖字節(jié)數(shù)組,默認(rèn)長度是8192(也就是總?cè)萘渴?KB),當(dāng)然也可以通過構(gòu)造函數(shù)指定。讀取數(shù)據(jù)的時候,源字節(jié)序列先填充到其內(nèi)部的緩沖字節(jié)數(shù)組,然后在調(diào)用read()等相關(guān)方法的時候,實(shí)際上是從緩沖字節(jié)數(shù)組中拷貝字節(jié)數(shù)據(jù)到目標(biāo)字節(jié)數(shù)組中。當(dāng)緩沖字節(jié)數(shù)組中的字節(jié)讀取(拷貝)完畢之后,如果被讀取的字節(jié)序列還有剩余,則再次調(diào)用底層輸入流填充緩沖字節(jié)數(shù)組。這種做法等于從直接內(nèi)存中讀取數(shù)據(jù),其效率每次都要訪問磁盤文件高很多。當(dāng)需要讀取的字節(jié)序列的長度十分小或者本身使用的目標(biāo)字節(jié)容量比BufferedInputStream提供的緩沖字節(jié)數(shù)組容量大的時候,使用BufferedInputStream的優(yōu)勢是不明顯的。
public static void main(String[] args) throws Exception {
try (FileInputStream inputStream = new FileInputStream("D:\\1.log");
BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream)) {
byte[] data = new byte[20];
bufferedInputStream.read(data);
System.out.println(new String(data, "UTF-8"));
}
}
數(shù)據(jù)輸入流
DataInputStream繼承自FilterInputStream,它也是典型的裝飾輸入流,它允許應(yīng)用程序以與機(jī)器無關(guān)方式從底層輸入流中讀取基本Java數(shù)據(jù)類型。它的方法列表如下:
final int read(byte[] buffer, int offset, int length)
final int read(byte[] buffer)
final boolean readBoolean()
final byte readByte()
final char readChar()
final double readDouble()
final float readFloat()
final void readFully(byte[] dst)
final void readFully(byte[] dst, int offset, int byteCount)
final int readInt()
final String readLine()
final long readLong()
final short readShort()
final static String readUTF(DataInput in)
final String readUTF()
final int readUnsignedByte()
final int readUnsignedShort()
final int skipBytes(int count)
注意到所有的不需要緩沖字節(jié)數(shù)組的方法都是用于讀取字節(jié)序列中的下一個字節(jié)或者下一個字節(jié)塊(如果需要轉(zhuǎn)換的話,則轉(zhuǎn)換為相應(yīng)的類型)。
public static void main(String[] args) throws Exception {
ByteArrayOutputStream bo = new ByteArrayOutputStream();
DataOutputStream outputStream = new DataOutputStream(bo);
outputStream.writeInt(24);
outputStream.writeBytes("doge");
ByteArrayInputStream inputStream = new ByteArrayInputStream(bo.toByteArray());
DataInputStream dataInputStream = new DataInputStream(inputStream);
//這里注意,因?yàn)閷懭氲臅r候順序是先int然后是byte,所以讀取的時候必須先讀取int再讀取byte
int age = dataInputStream.readInt();
byte[] nameChars = new byte[4];
for (int i = 0; i < 4; i++) {
nameChars[i] = dataInputStream.readByte();
}
System.out.println("age int ->" + age);
System.out.println("name byte->" + new String(nameChars));
}
管道輸入流
Java的管道輸入與輸出實(shí)際上使用的是一個循環(huán)緩沖數(shù)組來實(shí)現(xiàn),這個數(shù)組默認(rèn)大小為1024字節(jié)。輸入流PipedInputStream從這個循環(huán)緩沖數(shù)組中讀數(shù)據(jù),輸出流PipedOutputStream往這個循環(huán)緩沖數(shù)組中寫入數(shù)據(jù)。當(dāng)這個緩沖數(shù)組已滿的時候,輸出流PipedOutputStream所在的線程將阻塞;當(dāng)這個緩沖數(shù)組首次為空的時候,輸入流PipedInputStream所在的線程將阻塞。但是在實(shí)際用起來的時候,卻會發(fā)現(xiàn)并不是那么好用。一般PipedInputStream和PipedOutputStream是成對出現(xiàn)的,否則沒有意義。
public static void main(String[] args) throws Exception {
PipedInputStream pipedInputStream = new PipedInputStream();
PipedOutputStream pipedOutputStream = new PipedOutputStream(pipedInputStream);
Thread sender = new Thread(() -> {
try {
pipedOutputStream.write("hello,doge".getBytes("UTF-8"));
} catch (IOException e) {
e.printStackTrace();
}
});
Thread receiver = new Thread(() -> {
byte[] data = new byte[10];
try {
//這里會阻塞
pipedInputStream.read(data);
System.out.println("Receive message -> " + new String(data,"UTF-8"));
} catch (IOException e) {
e.printStackTrace();
}
});
receiver.start();
sender.start();
Thread.sleep(Integer.MAX_VALUE);
}
上面只是舉例所以顯式創(chuàng)建線程,如果在實(shí)際使用中最好別這樣做。
合并輸入流
SequenceInputStream會將與之相連接的流集組合成一個輸入流并從第一個輸入流開始讀取,直到到達(dá)文件末尾,接著從第二個輸入流讀取,依次類推,直到到達(dá)包含的最后一個輸入流的文件末尾為止。合并流的作用是將多個源合并合一個源,也就是說它是一個字節(jié)流的合并工具。
public static void main(String[] args) throws Exception{
ByteArrayInputStream inputStream1 = new ByteArrayInputStream("hello,".getBytes("UTF-8"));
ByteArrayInputStream inputStream2 = new ByteArrayInputStream("doge".getBytes("UTF-8"));
Vector vector = new Vector<InputStream>();
vector.add(inputStream1);
vector.add(inputStream2);
SequenceInputStream sequenceInputStream = new SequenceInputStream(vector.elements());
byte[] data = new byte[10];
//注意這里先讀取"hello,",長度為6,起始位置為0
sequenceInputStream.read(data,0, 6);
//第二次讀取"doge",長度為4,起始位置為6(data數(shù)組下標(biāo))
sequenceInputStream.read(data,6, 4);
System.out.println(new String(data,"UTF-8"));
}
OutputStream
java.io.OutputStream是所有輸出流的抽象父類,提供三個基本方法用于寫入字節(jié)序列,另外還提供關(guān)閉流、flush(強(qiáng)制清空緩沖區(qū)的字節(jié)數(shù)組并且將之馬上寫入到目標(biāo)中)兩個方法。
//單字節(jié)數(shù)據(jù)寫入,實(shí)際上是"無符號的byte",范圍是[0,255]
public abstract void write(int b) throws IOException
//下面的write方法的變體
public void write(byte[] data) throws IOException
//寫入指定的字節(jié)數(shù)組,可以指定偏移量和總長度
public void write(byte[] data, int offset, int length) throws IOException
// 強(qiáng)制清空緩沖區(qū)的字節(jié)數(shù)組并且將之馬上寫入到目標(biāo)中
public void flush( ) throws IOException
//關(guān)閉流
public void close( ) throws IOException
寫入
public abstract void write(int b)是抽象方法,必須由子類實(shí)現(xiàn)。它用于寫入單字節(jié)數(shù)據(jù),寫入的字節(jié)是"無符號的byte"類型值[0,255],因?yàn)镴ava中不存在無符號byte類型,所以入?yún)⑹莍nt類型。
public void write(byte[] data, int offset, int length)用于寫入一個連續(xù)的數(shù)據(jù)塊(即一個連續(xù)的字節(jié)序列)到目標(biāo)中,offset是指寫入的目標(biāo)字節(jié)數(shù)組data的偏移量,可以理解為data這個字節(jié)數(shù)組寫入時候的起始索引,而length是需要寫入的字節(jié)序列的最大長度(這個長度有可能比data這個字節(jié)數(shù)組的總長度要小)。畫個圖說明一下:

public void write(byte[] data)方法只是write(byte[] data, int offset, int length)的變體,源碼如下:
public void write(byte b[]) throws IOException {
write(b, 0, b.length);
}
flush
很多時候我們需要使用緩沖流來提高寫入的性能,使用緩沖流的時候,并不是每次調(diào)用write方法就直接寫目標(biāo)中寫入字節(jié)序列,而是當(dāng)緩沖字節(jié)數(shù)組(下面稱為緩沖區(qū))填充滿了之后再一次性把緩沖區(qū)中的所有字節(jié)寫到目標(biāo)中。flush()方法被調(diào)用的時候,不管緩沖區(qū)是否已經(jīng)填充滿,直接把緩沖區(qū)的所有字節(jié)寫入到目標(biāo)中并且清空緩沖區(qū)。其實(shí)可以通過源碼查看,在FileOutputStream中,flush()方法是空實(shí)現(xiàn),也就是調(diào)用它不會產(chǎn)生任何效果,但是在BufferedOutputStream中它就起到前面說到的效果。當(dāng)然,如果調(diào)用了close()方法會強(qiáng)制把緩沖區(qū)的數(shù)據(jù)寫入到目標(biāo),表面上可以這樣理解,close()方法調(diào)用的時候必定會觸發(fā)flush()。
關(guān)閉
輸出流的關(guān)閉可以顯式調(diào)用close()方法,一般需要把關(guān)閉方法置于finally塊中并且捕獲其異常不進(jìn)行異常拋出。關(guān)閉后的流不能再進(jìn)行讀取等操作,否則會拋出IOException。在Jdk1.7中引入了java.lang.AutoCloseable接口,實(shí)現(xiàn)了AutoCloseable接口的流可以使用try-resource的方式進(jìn)行編碼,這樣就不需要顯式關(guān)閉流。例如:
try (FileOutputStream outputStream = new FileOutputStream("xxxx")){
byte[] buffer = new byte[10];
//填充buffer
outputStream.write(buffer);
//....
}
在上一節(jié)中提到過,調(diào)用了close()方法會強(qiáng)制把緩沖區(qū)的數(shù)據(jù)寫入到目標(biāo),所以一定要注意必須確保輸出流的關(guān)閉,一方面可以釋放相關(guān)的句柄以避免資源被大量占用導(dǎo)致OOM等,另一方面可以避免沒有顯式調(diào)用flush()下導(dǎo)致內(nèi)存數(shù)據(jù)沒有成功寫入到目標(biāo)中(發(fā)生了內(nèi)存數(shù)據(jù)的丟失)。
OutputStream子類
下圖是OutputStream的主要子類,不包含sun包下隱藏的類。

下面的分類只是按照個人的理解,并沒有科學(xué)的根據(jù)。
管道流:
- PipedOutputStream,提供從與其它線程共用的管道中寫入數(shù)據(jù)的功能。
介質(zhì)流:
- ByteArrayOutputStream,提供寫入數(shù)據(jù)到內(nèi)存中的字節(jié)數(shù)組的功能。
- FileOutputStream,提供寫入數(shù)據(jù)到磁盤文件的功能。
裝飾流:
主要包括ObjectOutputStream和FilterOutputStream的子類。
- ObjectOutputStream,提供對象序列化功能。
- PrintStream,打印流,提供打印各種類型的數(shù)據(jù)的功能,System.out是標(biāo)準(zhǔn)輸出,是PrintStream的實(shí)例。
- DataOutputStream,數(shù)據(jù)輸出流,提供直接寫入具體數(shù)據(jù)類型的功能。
- BufferedOutputStream,緩沖輸出流,為輸出數(shù)據(jù)提供字節(jié)緩沖區(qū)。
因?yàn)檩敵隽骱洼斎肓魇腔緦ΨQ的,下面只介紹一個特例:PrintStream。
打印流
PrintStream也使用了典型的裝飾器模式,它為其他輸出流添加了功能,使它們能夠方便地打印各種數(shù)據(jù)值表示形式。與其他輸出流不同,PrintStream永遠(yuǎn)不會拋出IOException,異常情況僅僅可通過checkError方法返回的布爾值進(jìn)行判斷。另外,PrintStream的構(gòu)造函數(shù)可以通過autoFlush這個布爾值參數(shù)設(shè)置是否自動flush,這意味著可在寫入byte數(shù)組之后自動調(diào)用flush方法(其實(shí)是任何寫入操作,因?yàn)榫唧w類型的寫入操作最終也會轉(zhuǎn)換為byte數(shù)組的寫入)。其實(shí),PrintStream的API設(shè)計是十分優(yōu)秀的,但是它吞下所有異常的這一設(shè)計有點(diǎn)不太友好。
public static void main(String[] args) throws Exception {
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
PrintStream printStream = new PrintStream(outputStream)) {
printStream.print("I am doge,");
printStream.print(25);
printStream.print(" years old!");
System.out.println(new String(outputStream.toByteArray(), "UTF-8"));
}
}
控制臺輸出:
I am doge,25 years old!
不得不說,打印流用起來真是十分爽。
關(guān)于InputStream和OutputStream的小結(jié)
這里小結(jié)一下InputStream和OutputStream,以FileInputStream和FileOutputStream,僅僅代表個人的見解。
先在假設(shè)Windows系統(tǒng)的D盤有一個文件,文件名稱是access.log,內(nèi)容是:
Hello,doge
我們可以通過文件的絕對路徑實(shí)例化FileInputStream:
FileInputStream fileInputStream = new FileInputStream(String.format("D:%saccess.log", File.separator));
然后,我們就可以通過FileInputStream讀取D:\access.log中的內(nèi)容,首先我們要先新建一個字節(jié)數(shù)組用于存放讀取的內(nèi)容,因?yàn)?Hello,doge"的長度為10,所以我們建立長度為10的字節(jié)數(shù)組即可:
byte[] data = new byte[10];
接著我們通過FileInputStream的read(byte b[])方法讀取文件內(nèi)容,寫入到新創(chuàng)建的空字節(jié)數(shù)組data中:
fileInputStream.read(data);
這里值得注意的是:FileInputStream的read(byte b[])當(dāng)讀取到文件的末尾的時候,會返回-1,這里讀取過程其實(shí)最好寫成一個循環(huán):
int len;
while ((len = fileInputStream.read(data)) != -1){
}
這樣之后,文件內(nèi)容就能夠讀取到data中,可以嘗試打印data的內(nèi)容:
System.out.println(new String(data,"UTF-8"));
當(dāng)然,這里僅僅是演示如何讀取文件內(nèi)容,但是實(shí)際生產(chǎn)環(huán)境中,文件的內(nèi)容有可能極大,如果要新建一個極大的byte數(shù)組,一次性讀完整個文件,有可能會導(dǎo)致大量內(nèi)存被占用導(dǎo)致內(nèi)存溢出。因此,在讀取大文件的時候,可以考慮分行、限制每次讀取長度或者分段多次讀取,在這里讀取大文件不做深入分析,后面會寫一篇實(shí)戰(zhàn)的文章。
對于OutputStream,寫入數(shù)據(jù)過程大致和InputStream的讀取數(shù)據(jù)過程相反,以FileOutputStream為例子,假設(shè)我們要向D:\access.log文件中寫入文件內(nèi)容,寫入的內(nèi)容為:
Today is Sunday.
類似,先通過文件絕對路徑新建FileOutputStream實(shí)例:
FileOutputStream fileOutputStream = new FileOutputStream(String.format("D:%saccess.log", File.separator));
然后,我們需要準(zhǔn)備寫入目標(biāo)文件的字節(jié)數(shù)組內(nèi)容:
byte[] outputData = "Today is Sunday.".getBytes("UTF-8");
最后調(diào)用FileOutputStream的write(byte b[])方法即可:
fileOutputStream.write(outputData);
調(diào)用完成的后,會發(fā)現(xiàn)文件中原來的"Hello,doge"內(nèi)容被抹掉,替換為"Today is Sunday.",這是因?yàn)?br> FileOutputStream實(shí)例沒有指定為追加模式,于是直接把字節(jié)數(shù)組的內(nèi)容直接寫進(jìn)去文件,覆蓋掉原來的內(nèi)容。如果想實(shí)現(xiàn)文件內(nèi)容追加,要使用FileOutputStream的另一個構(gòu)造函數(shù):
//如果指定append為true,則寫入的數(shù)據(jù)的方式是追加,而不是覆蓋,默認(rèn)是覆蓋
public FileOutputStream(String name, boolean append){
}
也就是:
FileOutputStream fileOutputStream = new FileOutputStream(String.format("D:%saccess.log", File.separator),true);
有時候要留多個心眼,觀察一下OutputStream的具體子類的構(gòu)造,可能會有驚喜。
輸入輸出流的分析大致到這里,其實(shí)它們的API還是挺容易使用的。
Reader和Writer
Reader
java.io.Reader是所有的字符讀取器(Reader暫時不知道怎么翻譯,先這樣叫)的抽象父類。Reader的核心功能是從來源中讀取字節(jié)序列并且轉(zhuǎn)換為char數(shù)組。它主要提供下面的方法:
public int read(java.nio.CharBuffer target) throws IOException
public int read() throws IOException
public int read(char cbuf[]) throws IOException
abstract public int read(char cbuf[], int off, int len) throws IOException
public long skip(long n) throws IOException
public boolean ready() throws IOException
public boolean markSupported()
public void mark(int readAheadLimit) throws IOException
public void reset() throws IOException
abstract public void close() throws IOException
大部分方法與InputStream中的方法相似,實(shí)際效果也是基本一致,這里主要分析一下ready()和read()方法。
ready
當(dāng)此方法返回true的時候,保證下一次調(diào)用read()方法的時候不會阻塞輸入,但是當(dāng)此方法返回false,并不能保證下一次調(diào)用read()方法的時候一定會阻塞輸入。這個方法的作用是用來判斷編碼轉(zhuǎn)換器是否已經(jīng)把字節(jié)序列轉(zhuǎn)換為char序列,如果有可用的char序列,此方法返回true,此時可以進(jìn)行char的讀取。其實(shí),個人更建議使用read()方法阻塞和返回值是否為-1來做相關(guān)判斷。
讀取
read方法都是阻塞方法,直到有可讀字節(jié)、到達(dá)流的尾部返回-1或者拋出異常。其實(shí)在最開始前已經(jīng)說過,實(shí)際上Reader讀取的還是字節(jié),中間通過編碼轉(zhuǎn)換把byte序列轉(zhuǎn)換成char序列,下面就直接描述為"讀取char序列"或者"讀取char數(shù)組"。
public int read(java.nio.CharBuffer target)把讀取到的char序列寫入到指定的CharBuffer實(shí)例中,返回寫入的char序列的長度,底層依賴到read(char cbuf[], int off, int len)方法。
public int read()方法是單字符讀取方法,返回值的范圍是[0,65535],當(dāng)方法返回-1說明已經(jīng)到達(dá)流的尾部,得到的int值可以直接強(qiáng)轉(zhuǎn)為char類型。
abstract public int read(char cbuf[], int off, int len)方法是讀取一段char序列或者一塊char數(shù)據(jù),可以指定寫入到目標(biāo)char數(shù)組cbuf的偏移量和寫入的總字符長度。注意到此方法是抽象方法,必須由子類實(shí)現(xiàn)。public int read(char cbuf[])方法只是read(char cbuf[], int off, int len)的變體,源碼如下:
public int read(char cbuf[]) throws IOException {
return read(cbuf, 0, cbuf.length);
}
Reader的主要子類
下圖是Reader的主要子類,不包含sun包下隱藏的類。

介質(zhì)Reader:
- CharArrayReader,從Char數(shù)組讀取數(shù)據(jù)。
- StringReader,從字符串中讀取數(shù)據(jù)。
裝飾Reader:
- BufferedReader,讀取數(shù)據(jù)時提供基于內(nèi)存的char數(shù)組緩沖區(qū),并且提供了基于行讀取的功能。
- LineNumberReader,繼承于BufferedReader,添加了設(shè)置行號和獲取行號的功能。
- PushbackReader,提供回退功能。
管道Reader:
- PipedReader,提供基于線程的管道數(shù)據(jù)讀取的功能。
轉(zhuǎn)換Reader:
- InputStreamReader,比較特殊,它的構(gòu)造函數(shù)入?yún)镮nputStream,也就是它是InputStream轉(zhuǎn)化為Reader的橋梁,也可以理解為讀取到的byte序列轉(zhuǎn)換為char序列的轉(zhuǎn)換器。
- FileReader,承于InputStreamReader,提供基文件數(shù)據(jù)讀取的更便捷的方法。
一般來說,如果來源中僅僅存在字符,可以優(yōu)先使用Reader進(jìn)行數(shù)據(jù)讀取,如果遇到像圖片一類的二進(jìn)制序列,只能考慮使用InputStream。
CharArrayReader
CharArrayReader提供從Char數(shù)組中讀取數(shù)據(jù)的功能。
public static void main(String[] args) throws Exception {
char[] input = "Hello,doge".toCharArray();
CharArrayReader charArrayReader = new CharArrayReader(input);
char[] readData = new char[10];
charArrayReader.read(readData);
System.out.println(new String(readData));
}
StringReader
StringReader提供從字符串讀取數(shù)據(jù)的功能。
public static void main(String[] args) throws Exception{
StringReader stringReader = new StringReader("Hello,doge");
char[] readData = new char[10];
stringReader.read(readData);
System.out.println(new String(readData));
}
BufferedReader
BufferedReader讀取數(shù)據(jù)時提供基于內(nèi)存的緩沖區(qū),并且提供了基于行讀取的功能,換行功能是換行符進(jìn)行判斷,例如"\n"或者"\r"。
public static void main(String[] args) throws Exception {
try (StringReader stringReader = new StringReader("Hello,doge!\nToday is Sunday!");
BufferedReader bufferedReader = new BufferedReader(stringReader)) {
String value;
while ((value = bufferedReader.readLine()) != null) {
System.out.println(value);
}
}
}
控制臺輸出:
Hello,doge!
Today is Sunday!
BufferedReader提供的readLine()方法返回被裝飾Reader下一行的字符串內(nèi)容,如果讀取到流的尾部,則返回null。
LineNumberReader
LineNumberReader繼承于BufferedReader,讀取數(shù)據(jù)時提供基于內(nèi)存的緩沖區(qū),并且提供了基于行讀取的功能,增加了設(shè)置行號和獲取行號的功能。
public static void main(String[] args) throws Exception {
try (StringReader stringReader = new StringReader("Hello,doge!\nToday is Sunday!");
LineNumberReader bufferedReader = new LineNumberReader(stringReader)) {
String value;
while ((value = bufferedReader.readLine()) != null) {
System.out.println(bufferedReader.getLineNumber() + ":" + value);
}
}
}
控制臺輸出:
1:Hello,doge!
2:Today is Sunday!
PushbackReader
PushbackReader類似于PushbackInputStream,提供數(shù)據(jù)回退的功能。
public static void main(String[] args) throws Exception {
StringReader stringReader = new StringReader("hello,doge");
PushbackReader pushbackReader = new PushbackReader(stringReader);
int len;
while ((len = pushbackReader.read()) != -1) {
if (len == 'o') {
//回退當(dāng)前char
pushbackReader.unread(len);
//再次讀取
len = pushbackReader.read();
System.out.print("<回退字符>" + (char) len + "</回退字符>");
} else {
System.out.print((char) len);
}
}
}
PipedReader
PipedReader和PipedWriter與PipedInputStream和PipedOutputStream一樣,都可以用于管道通信。PipedWriter是字符管道輸出流,繼承于Writer;PipedReader是字符管道輸入流,繼承于Reader,PipedWriter和PipedReader的作用是可以通過管道進(jìn)行線程間的通訊。兩者必須要配套使用,否則意義不大。
public static void main(String[] args) throws Exception{
PipedReader pipedReader = new PipedReader();
PipedWriter pipedWriter = new PipedWriter(pipedReader);
Thread sender = new Thread(() -> {
try {
pipedWriter.write("hello,doge");
} catch (IOException e) {
e.printStackTrace();
}
});
Thread receiver = new Thread(() -> {
char[] data = new char[10];
try {
pipedReader.read(data);
System.out.println("receive data -> " + new String(data));
} catch (IOException e) {
e.printStackTrace();
}
});
receiver.start();
sender.start();
Thread.sleep(Integer.MAX_VALUE);
}
InputStreamReader
InputStreamReader是InputStream轉(zhuǎn)化為Reader的橋梁,也可以理解為讀取到的byte序列轉(zhuǎn)換為char序列的轉(zhuǎn)換器,可以在構(gòu)造器中指定具體的編碼類型,如果不指定的話將采用底層操作系統(tǒng)的默認(rèn)編碼類型,例如GBK。它的實(shí)例化依賴于InputStream的實(shí)例,InputStream轉(zhuǎn)化為Reader,就可以使用裝飾流操作InputStreamReader實(shí)例,這樣的話能夠大大簡化讀取數(shù)據(jù)的操作。
public static void main(String[] args) throws Exception {
try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream("Hello,doge!\nToday is Sunday!".getBytes("UTF-8"));
InputStreamReader inputStreamReader = new InputStreamReader(byteArrayInputStream, "UTF-8");
BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) {
String value;
while ((value = bufferedReader.readLine()) != null) {
System.out.println(value);
}
}
}
FileReader
FileReader繼承于InputStreamReader,它內(nèi)部原理是通過FileInputStream轉(zhuǎn)化為Reader,作用是簡化了讀取文件字符的功能。
public static void main(String[] args) throws Exception{
try (FileReader fileReader = new FileReader(String.format("D:%saccess.log", File.separator));
BufferedReader bufferedReader = new BufferedReader(fileReader)){
String value;
while ((value = bufferedReader.readLine()) != null) {
System.out.println(value);
}
}
}
Writer
java.io.Writer是所有的字符寫入器(Writer暫時不知道怎么翻譯,先這樣叫)的抽象父類。Writer的核心功能是把char序列轉(zhuǎn)化為byte序列寫入到目標(biāo)中,char序列轉(zhuǎn)化為byte序列的過程對開發(fā)者來說是無感知的。它主要提供下面的方法:
public void write(int c) throws IOException
public void write(char cbuf[]) throws IOException
abstract public void write(char cbuf[], int off, int len) throws IOException
public void write(String str) throws IOException
public void write(String str, int off, int len)
public Writer append(CharSequence csq) throws IOException
public Writer append(CharSequence csq, int start, int end)
public Writer append(char c) throws IOException
abstract public void flush() throws IOException
abstract public void close() throws IOException
讀取或者追加
追加方法append()最終都會調(diào)用到write()方法,它們只是為了方便構(gòu)建鏈?zhǔn)骄幊?追加方法都返回this)。所有的append()和write()方法都是abstract public void write(char cbuf[], int off, int len)方法的變體而已,寫入char數(shù)組再轉(zhuǎn)換為字節(jié)序列到目標(biāo)中,可以指定char數(shù)組的偏移量和寫入的總長度。注意到,write(int c)方法,實(shí)際上char和int可以相互轉(zhuǎn)換,在此方法中,參數(shù)int會直接轉(zhuǎn)換為char類型。int轉(zhuǎn)換為char的時候,只會取低16位,高16位會被忽略,并且它是無符號的,也就是char的范圍是[0,65535],這正是Unicode編碼的碼點(diǎn)范圍。
flush
flush()方法調(diào)用之后會立即把char序列轉(zhuǎn)換為byte序列寫入到目標(biāo)中,類似于OutputStream的flush方法。
關(guān)閉
close()方法用于關(guān)閉流,此方法調(diào)用后必定先進(jìn)行flushing,也就是效果類似于首先先調(diào)用flush()方法,然后釋放流相關(guān)的資源和句柄等,類似于OutputStream的close方法。
Writer的主要子類
下圖是Writer的主要子類,不包含sun包下隱藏的類。

Writer的子類的和Reader的子類是基本對稱的,下面只介紹特例。
打印Writer:
- PrintWriter,提供基于多種數(shù)類型格式化的輸出功能,使用方式其實(shí)跟PrintStream大致相同。
這里再啰嗦一句,Writer的所有子類都實(shí)現(xiàn)了父類Writer中的方法,它們都是相當(dāng)有效和易用的API。
PrintWriter
PrintWriter提供基于多種數(shù)類型格式化的輸出功能,它使用了裝飾器模式。
public static void main(String[] args) throws Exception {
try (StringWriter stringWriter = new StringWriter();
PrintWriter printWriter = new PrintWriter(stringWriter)) {
printWriter.println("hello,doge");
printWriter.println(10086);
System.out.println(stringWriter.toString());
}
}
控制臺輸出:
hello,doge
10086
小結(jié)
IO是網(wǎng)絡(luò)編程的基礎(chǔ),如果想要寫出高性能的中間件,必須深入理解IO相關(guān)的知識。這篇文章僅僅說到了一些皮毛,主要是通過閱讀書中的內(nèi)容,對IO的一些基礎(chǔ)認(rèn)知進(jìn)行整理,對BIO相關(guān)的一些API進(jìn)行基于例子的使用講解。這本書中關(guān)于套接字相關(guān)的內(nèi)容并不詳細(xì),在《Java NIO》有更深入的分析(但是兩本書很多內(nèi)容是重疊的,有點(diǎn)蛋疼),后面會寫另一篇NIO的總結(jié),主要包括NIO的特性以及URL、URI、URLConnection、套接字相關(guān)的內(nèi)容等等。如果有更深入的收獲,后面再寫具體的文章分享。
(本文完)