Java中IO流分成兩大類,一種是輸入流。全部的輸入流都直接或間接繼承自InputStream抽象類,輸入流作為數據的來源。我們能夠通過輸入流的read方法讀取字節數據。還有一種是輸出流,全部的輸出流都直接或間接繼承自OutputStream抽象類,輸出流接收數據。能夠通過write方法寫入字節數據。在Java的IO流類中,大部分的輸入流和輸出流都是成對存在的。即假設存在XXXInputStream。那麼就存在XXXOutputStream。反之亦然。(SequenceInputStream和StringBufferInputStream是特例,沒有相應的SequenceOutputStream類和StringBufferOutputStream類,稍後會解釋)。很多IO操作都可能會拋出IOException異常,比方read、write、close操作。
下面是Java的IO流中常見的輸入流,由於每一個輸入流都有其相應的輸出流,所以此處就不再列出輸出流的繼承結構圖。
下面依次對這些類進行介紹以及怎樣使用。
ByteArrayInputStream & ByteArrayOutputStream
ByteArrayInputStream構造函數中須要傳入一個byte數組作為數據源,當運行read操作時,就會從該數組中讀取數據,正如其名,是一種基於字節數組實現的一種簡單輸入流,顯而易見的是,假設在構造函數中傳入了null作為字節數據。那麼在運行read操作時就會出現NullPointerException異常。可是在構造函數初始化階段不會拋出異常。與之相相應的是ByteArrayOutputStream,其內部也有一個字節數組用於存儲write操作時寫入的數據,在構造函數中能夠傳入一個size指定其內部的byte數組的大小。假設不指定,那麼默認它會將byte數組初始化為32字節,當持續通過write向ByteArrayOutputStream中寫入數據時,假設其內部的byte數組的剩余空間不能夠存儲須要寫入的數據,那麼那麼它會通過調用內部的ensureCapacity
方法對其內部維護的byte數組進行擴容以存儲全部要寫入的數據,所以不必操心其內部的byte數組太小導致的IndexOutOfBoundsException之類的異常。
下面是ByteArrayInputStream 和 ByteArrayOutputStream的代碼片段演示樣例:
private static void testByteArrayInputOutStream(){
byte[] bytes = "I am iSpring".getBytes();
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buf = new byte[1024];
int length = 0;
try{
while((length = bais.read(buf)) > 0){
baos.write(buf, 0, length);
}
System.out.println(baos.toString("UTF-8"));
bais.close();
baos.close();
}catch(IOException e){
e.printStackTrace();
}
}
在上面的樣例中,我們通過字符串獲取字節數組將其作為ByteArrayInputStream的數據流來源,然後通過讀取ByteArrayInputStream的數據,將讀到的數據寫入到ByteArrayOutputStream中。
FileInputStream & FileOutputStream
FileInputStream 能夠將文件作為數據源,讀取文件裡的流,通過File對象或文件路徑等初始化。在其構造函數中。假設傳入的File對象(或與其相相應的文件路徑所表示的File對象)不存在或是一個文件夾而不是文件或者由於其它原因無法打開讀取數據,都會導致在初始化階段導致拋出FileNotFoundException異常。與FileInputStream 相相應的是FileOutputStream,能夠通過FileOutputStream向文件裡寫入數據,也須要通過File對象或文件路徑對其初始化,如同FileInputStream ,假設傳入的File對象(或與其相相應的文件路徑所表示的File對象)是一個文件夾而不是文件或者由於其它原因無法創建該文件寫入數據,都會導致在初始化階段拋出FileNotFoundException異常。
下面是FileInputStream 和 FileOutputStream的代碼演示樣例片段:
private static void testFileInputOutStream(){
try{
String inputFileName = "D:\\iWork\\file1.txt";
String outputFileName = "D:\\iWork\\file2.txt";
FileInputStream fis = new FileInputStream(inputFileName);
FileOutputStream fos = new FileOutputStream(outputFileName);
byte[] buf = new byte[1024];
int length = 0;
while ((length = fis.read(buf)) > 0){
fos.write(buf, 0, length);
}
fis.close();
fos.close();
}catch (FileNotFoundException e){
e.printStackTrace();
}catch (IOException e){
e.printStackTrace();
}
}
在上面的樣例中。我們通過FileInputStream的read方法讀取file1.txt中的數據,然後將獲得的字節數據通過FileOutputStream的write方法將其寫入到還有一個文件file2.txt中,這樣就實現了文件的拷貝,即將file1.txt復制到file2.txt。
假設file2.txt已經存在,那麼在初始FileOutputStream時,能夠傳入一邊boolean變量append表示是向已有文件裡追加寫入數據還是覆蓋已有數據。
PipedInputStream & PipedOutputStream
PipedInputStream和PipedOutputStream通常是結合使用的。這兩個類用於在兩個線程間進行管道通信,一般在一個線程中運行PipedOutputStream 的write操作。而在還有一個線程中運行PipedInputStream的read操作。能夠在構造函數中傳入相關的流將PipedInputStream 和PipedOutputStream 綁定起來,也能夠通過二者的connect方法將二者綁定起來,一旦二者進進行了綁定,那麼PipedInputStream的read方法就會自己主動讀取PipedOutputStream寫入的數據。PipedInputStream的read操作是堵塞式的,當運行PipedOutputStream的write操作時,PipedInputStream會在還有一個線程中自己主動讀取PipedOutputStream寫入的內容,假設PipedOutputStream一直沒有運行write操作寫入數據,那麼PipedInputStream的read方法會一直堵塞PipedInputStream的read方法所運行的線程直至讀到數據。
單獨使用PipedInputStream或單獨使用PipedOutputStream時沒有不論什麼意義的。必須將二者通過connect方法(或在構造函數中傳入相應的流)進行連接綁定。假設單獨使用當中的某一個類,就會觸發IOException: Pipe Not Connected.
下面是PipedInputStream和PipedOutputStream的代碼演示樣例片段:
WriterThread類
import java.io.*;
public class WriterThread extends Thread {
PipedOutputStream pos = null;
public WriterThread(PipedOutputStream pos){
this.pos = pos;
}
@Override
public void run() {
String message = "這條信息來自於WriterThread.";
try{
byte[] bytes = message.getBytes("UTF-8");
System.out.println("WriterThread發送信息");
this.pos.write(bytes);
this.pos.close();
}catch (IOException e){
e.printStackTrace();
}
}
}
ReaderThread類
import java.io.*;
public class ReaderThread extends Thread {
private PipedInputStream pis = null;
public ReaderThread(PipedInputStream pis){
this.pis = pis;
}
@Override
public void run() {
byte[] buf = new byte[1024 * 8];
try{
System.out.println("ReaderThread堵塞式的等待接收數據...");
int length = pis.read(buf);
System.out.println("ReaderThread接收到例如以下信息:");
String message = new String(buf, 0, length, "UTF-8");
System.out.println(message);
pis.close();
}catch(IOException e){
e.printStackTrace();
}
}
}
測��代碼
private static void testPipedInputOutputStream(){
try{
PipedInputStream pis = new PipedInputStream();
PipedOutputStream pos = new PipedOutputStream();
pos.connect(pis);
WriterThread writerThread = new WriterThread(pos);
ReaderThread readerThread = new ReaderThread(pis);
readerThread.start();
writerThread.start();
}catch (IOException e){
e.printStackTrace();
}
}
在上面的實例中,我們創建了兩個線程類WriterThread和ReaderThread,在WriterThread的構造函數中我們傳入了一個PipedOutputStream,並在線程運行run方法時向WriterThread中寫入數據。在ReaderThread的構造函數中我們傳入了一個PipedInputStream。在其線程運行run方法時堵塞式的運行read操作。等待獲取數據。
我們通過pos.connect(pis)將這兩種流綁定在一起,最後分別運行線程ReaderThread和WriterThread。
輸出結果例如以下:
我們能夠看到即使我們先運行了ReaderThread線程,ReaderThread中的PipedInputStream還是一直在堵塞式的等待數據的到來。
ObjectInputStream & ObjectOutputStream
ObjectOutputStream具有一系列writeXXX方法,在其構造函數中能夠摻入一個OutputStream,能夠方便的向指定的輸出流中寫入基本類型數據以及String。比方writeBoolean、writeChar、writeInt、writeLong、writeFloat、writeDouble、writeCharts、writeUTF等,除此之外。ObjectOutputStream還具有writeObject方法。writeObject方法中傳入的類型必須實現了Serializable接口,從而在運行writeObject操作時將對象進行序列化成流。並將其寫入指定的輸出流中。與ObjectOutputStream相相應的是ObjectInputStream,ObjectInputStream有與OutputStream中的writeXXX系列方法全然相應的readXXX系列方法,專門用於讀取OutputStream通過writeXXX寫入的數據。
下面是ObjectInputStream 和 ObjectOutputStream的演示樣例代碼:
Person類
import java.io.Serializable;
public class Person implements Serializable {
private String name = "";
private int age = 0;
public Person(String name, int age){
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
測試代碼
private static void testObjectInputOutputStream(){
try{
String fileName = "D:\\iWork\\file.tmp";
//將內存中的對象序列化到物理文件裡
FileOutputStream fos = new FileOutputStream(fileName);
ObjectOutputStream oos = new ObjectOutputStream(fos);
String description = "下面是人員數組";
Person[] persons = new Person[]{
new Person("iSpring", 26),
new Person("Mr.Sun", 27),
new Person("Miss.Zhou", 27)
};
oos.writeObject(description);
oos.writeInt(persons.length);
for(Person person : persons){
oos.writeObject(person);
}
oos.close();
//從物理文件裡反序列化讀取對象信息
FileInputStream fis = new FileInputStream(fileName);
ObjectInputStream ois = new ObjectInputStream(fis);
String str = (String)ois.readObject();
System.out.println(str);
int personCount = ois.readInt();
for(int i = 0; i < personCount; i++){
Person person = (Person)ois.readObject();
StringBuilder sb = new StringBuilder();
sb.append("姓名: ").append(person.getName()).append(", 年齡: ").append(person.getAge());
System.out.println(sb);
}
}catch (FileNotFoundException e){
e.printStackTrace();
}catch(IOException e){
e.printStackTrace();
}catch (ClassNotFoundException e){
e.printStackTrace();
}
}
輸出結果例如以下:
Person實現了Serializable接口,須要注意的是,Serializable接口是一個標識接口,並不須要實現不論什麼方法。我們首先通過ObjectOutputStream。將Person等數組信息序列化成流。然後通過調用writeObject等方法將其寫入到FileOutputStream中,從而實現了將內存中的基本類型和對象序列化保存到硬盤的物理文件裡。
然後通過FileInputStream讀取文件,將文件的輸入流傳入到到ObjectInputStream的構造函數中。這樣ObjectInputStream就能夠通過運行相應的readXXX操作讀取基本類型或對象。當運行readObject操作時。返回的是Object類型,須要強制轉換為相應的實際類型。須要注意的是,ObjectInputStream運行readXXX操作的方法順序須要與ObjectOutputStream運行writeXXX操作的方法順序一致。否則就會讀到錯誤的數據或拋出異常。比方一開始向FileOutputStream中運行writeFloat。而在FileInputStream中首先運行了readInt操作,不會報錯,由於writeFloat寫入了4個字節的數據。readInt讀入了4個字節的數據,盡管能夠將該Float轉換為相應的int,可是事實上已經不是我們想要的數據了,所以要注意readXXX操作與writeXXX操作運行順序的相應。
SequenceInputStream
SequenceInputStream 主要是將兩個(或多個)InputStream在邏輯上合並為一個InputStream,比方在構造函數中傳入兩個InputStream。分別為in1和in2,那麼SequenceInputStream在讀取操作時會先讀取in1,假設in1讀取完成。就會接著讀取in2。在我們理解了SequenceInputStream 的作用是將兩個輸入流合並為一個輸入流之後,我們就能理解為什麼不存在相應的SequenceOutputStream 類了。由於將一個輸出流拆分為多個輸出流是沒有意義的。
下面是關於SequenceInputStream的演示樣例代碼:
private static void testSequenceInputOutputStream(){
String inputFileName1 = "D:\\iWork\\file1.txt";
String inputFileName2 = "D:\\iWork\\file2.txt";
String outputFileName = "D:\\iWork\\file3.txt";
try{
FileInputStream fis1 = new FileInputStream(inputFileName1);
FileInputStream fis2 = new FileInputStream(inputFileName2);
SequenceInputStream sis = new SequenceInputStream(fis1, fis2);
FileOutputStream fos = new FileOutputStream(outputFileName);
byte[] buf = new byte[1024];
int length = 0;
while((length = sis.read(buf)) > 0){
fos.write(buf, 0, length);
}
sis.close();
fos.close();
}catch (FileNotFoundException e){
e.printStackTrace();
}catch (IOException e){
e.printStackTrace();
}
}
我們通過FileInputStream分別獲取了file1.txt和file2.txt的輸入流,然後將這兩個輸入流作為構造函數的參數創建了SequenceInputStream 的實例,所以該SequenceInputStream 中已經在邏輯上將file1.txt和file2.txt的內容合並為了一個輸入流,然後我們讀取該SequenceInputStream 中的數據,並將讀到的數據寫入到一個新的FileOutputStream中,這樣我們就實現了將file1.txt和file2.txt合並為一個新的文件file3.txt,原有的file1.txt和file2.txt文件不受不論什麼影響。
StringBufferInputStream
StringBufferInputStream同意通過在構造函數中傳入字符串以讀取字節,在讀取時內部主要調用了String的charAt方法。與SequenceInputStream相似。StringBufferInputStream也沒有相應的OutputStream。即不存在StringBufferOutputStream類。
Java沒有設計StringBufferOutputStream類的理由也非常easy。我們假設StringBufferOutputStream存在,那麼StringBufferOutputStream應該是內部通過運行write操作寫入數據更新其內部的String對象,比方有可能是通過StringBuilder來實現,可是這樣做毫無意義,由於一旦我們String的構造函數中能夠直接傳入字節數組構建字符串。簡單明了,所以設計StringBufferOutputStream就沒有太大的必要了。StringBufferInputStream這個類本身存在一點問題,它不能非常好地將字符數組轉換為字節數組,所以該類被Java標記為廢棄的(Deprecated),其官方推薦使用StringReader作為取代。
下面是關於StringBufferInputStream的演示樣例代碼:
private static void testStringBufferInputStream(){
String message = "I am iSpirng.";
StringBufferInputStream sbis = new StringBufferInputStream(message);
byte[] buf = new byte[1024];
try{
int length = sbis.read(buf);
if(length > 0){
System.out.println(new String(buf, 0, length, "UTF-8"));
}
sbis.close();
}catch (IOException e){
e.printStackTrace();
}
}
輸出結果例如以下:
FilterInputStream & FilterOutputStream
FilterInputStream包括了其它的輸入流,說具體點就是在其構造函數中須要傳入一個InputStream並將其保存在其名為in的字段中,FilterInputStream僅僅是簡單的覆蓋了全部的方法。之所說是簡單覆蓋是由於在每一個覆蓋函數中。它僅僅是調用內部的保存在in字段中的InputStream所相應的方法,比方在其覆蓋read方法時,內部僅僅是簡單調用了in.read()方法。FilterInputStream的子類能夠進一步覆蓋某些方法以保持接口不變的情況下實現某一特性(比方其子類有的能夠通過使用緩存優化讀取的效率)或者提供一些其它額外的實用方法。所以在使用時FilterInputStream能夠讓傳入的InputStream具有一些額外的特性。即對構造函數傳入的InputStream進行了一層包裹,使用了典型的裝飾著模式,假設僅僅看FilterInputStream本身這一個類的話。則該類自己本身意義不大。由於其僅僅是通過內部的字段in簡單覆寫某些方法。
可是假設將FilterInputStream 和其子類結合起來使用話,那麼就非常實用了。
比方FilterInputStream 有兩個子類BufferedInputStream和DataInputStream,這兩個類在下面還會具體介紹。
BufferedInputStream對read操作做了優化,每次讀操作時都讀取一大塊數據。然後將其放入內部維護的一個字節數組緩沖區中。當外面調用BufferedInputStream的read方法時,首先去該緩沖區中讀取數據,這樣就避免了頻繁的實際的讀操作。BufferedInputStream對外沒有暴露額外的其它方法,可是其內部的read方法已經經過優化了。所以在運行讀操作的時候效率更高。
DataInputStream與ObjectInputStream有點相似,能夠通過一些readXXX方法讀取基本類型的數據,這是非常實用的一些方法。假設我們即想使用BufferedInputStream讀取效率高的特性。又想是想DataInputStream中的readXXX方法怎麼辦呢?非常easy,例如以下代碼所看到的:
InputStream is = getInputStreamBySomeway();
BufferedInputStream bis = new BufferedInputStream(is);
DataInputStream dis = new DataInputStream(bis);
然後我們就能夠調用dis.readXXX()等方法,即快又方便,這就是FilterInputStream子類通過構造函數層層傳遞結合在一起使用多種特性的魅力。與之相相應的是BufferedOutputStream和DataOutputStream,BufferedOutputStream優化了write方法。提高了寫的效率。DataOutputStream具有非常多writeXXX方法。能夠方便的寫入基本類型數據。假設想使用writeXXX方法,還想提高寫入到效率,能夠例如以下代碼使用,與上面的代碼差點兒相同:
OutputStream os = getOutputStreamBySomeway();
BufferedOutputStream bos = new BufferedOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
然後在調用dos.writeXXX方法時效率就已經提高了。
BufferedInputStream & BufferedOutputStream
如上面所介紹的那樣,在BufferedInputStream的構造函數中須要傳入一個InputStream, BufferedInputStream內部有一個字節數組緩沖區,每次運行read操作的時候就從這buf中讀取數據,從buf中讀取數據沒有多大的開銷。假設buf中已經沒有了要讀取的數據,那麼就去運行其內部綁定的InputStream的read方法,並且是一次性讀取非常大一塊數據,以便填充滿buf緩沖區。緩沖區buf的默認大小是8192字節,也就是8K,在構造函數中我們也能夠自己傳入一個size指定緩沖區的大小。由於我們在運行BufferedInputStream的read操作的時候。非常多時候都是從緩沖區中讀取的數據,這樣就大大降低了實際運行其指定的InputStream的read操作的次數,也就提高了讀取的效率。與BufferedInputStream 相對的是BufferedOutputStream。在BufferedOutputStream的構造函數中我們須要傳入一個OutputStream。這樣就將BufferedOutputStream與該OutputStream綁定在了一起。
BufferedOutputStream內部有一個字節緩沖區buf,在運行write操作時。將要寫入的數據先一起緩存在一起,將其存入字節緩沖區buf中,buf是有限定大小的,默認的大小是8192字節,即8KB,當然也能夠在構造函數中傳入size指定buf的大小。該buf僅僅要被指定了大小之後就不會自己主動擴容,所以其是有限定大小的,既然有限定大小,就會有被填充完的時刻,當buf被填充完成的時候會調用BufferedOutputStream的flushBuffer方法,該方法會通過調用其綁定的OutputStream的write方法將buf中的數據進行實際的寫入操作並將buf的指向歸零(能夠看做是將buf中的數據清空)。假設想讓緩存區buf中的數據理解真的被寫入OutputStream中。能夠調用flush方法。flush方法內部會調用flushBuffer方法。由於buf的存在。會大大降低實際運行OutputStream的write操作的次數,優化了寫的效率。
下面是BufferedInputStream 和 BufferedOutputStream的演示樣例代碼片段:
private static void testBufferedInputOutputStream(){
try{
String inputFileName = "D:\\iWork\\file1.txt";
String outputFileName = "D:\\iWork\\file2.txt";
FileInputStream fis = new FileInputStream(inputFileName);
BufferedInputStream bis = new BufferedInputStream(fis, 1024 * 10);
FileOutputStream fos = new FileOutputStream(outputFileName);
BufferedOutputStream bos = new BufferedOutputStream(fos, 1024 * 10);
byte[] buf = new byte[1024];
int length = 0;
while ((length = bis.read(buf)) > 0){
bos.write(buf, 0, length);
}
bis.close();
bos.close();
}catch (FileNotFoundException e){
e.printStackTrace();
}catch (IOException e){
e.printStackTrace();
}
}
上面的代碼將從file1.txt讀取文件輸入流。然後將讀到的數據寫入到file2.txt中。即實現了將file1.txt復制到file2.txt中。事實上不通過BufferedInputStream 和 BufferedOutputStream也能夠完成這種工作,使用這個兩個類的優點是,能夠對file1.txt的讀取以及file2.txt的寫入提高效率,從而提升文件拷貝的效率。