天天看點

java筆記(三):IO流InputStreamOutputStreamReaderWriter

筆記終于進行到第一個有深度且重要的子產品了。

IO流是java用于處理輸入輸出資料的類的集合。

java筆記(三):IO流InputStreamOutputStreamReaderWriter

上圖包含了java中所有的IO類。

在閱讀源碼的基礎上,寫一些對于各個類的簡單了解。

InputStream

首先看的是位元組流。位元組流是以byte為基本表現形式的輸入資料。主要差別于檔案裡的文字。

InputStream是抽象類,規定了輸入位元組流的主要公共方法。

ByteArrayInputStream

該類主要用于操作byte數組,和大部分的io類不同,該ByteArrayInputStream的資料來源是數組(可以是代碼中的資料,也可以是記憶體塊)。

該類對于InputStream的所有方法都進行了重寫,主要原因是該類操作數組,是以很多操作和操作檔案不同,有許多簡易的方法。

貼一段簡單的代碼供參考:

byte[] b = {,,,,};
ByteArrayInputStream b1 = new ByteArrayInputStream(b);
for (;b1.available()>;){
    System.out.print((char)b1.read());
    b1.skip();
} //結果是:)?U
           

用起來,感覺ByteArrayInputStream就是對byte[]做了一層封裝,和通常意義上的輸入輸出關系不大。

實際上該類的設計使用到了擴充卡模式(Adapter):将一個類的接口轉換成客戶希望的另外一個接口,Adapter模式使得原本由于接口不相容而不能一起工作的那些類可以一起工作(總感覺有點強行OO的意思)。

FilterInputStream

該類包含一個InputStream類型的屬性,然後該類又繼承了InputStream(無語)。

public class FilterInputStream extends InputStream {
    /**
     * The input stream to be filtered.
     */
    protected volatile InputStream in;
           

其作用是:封裝其它的輸入流,并為它們提供額外的功能(裝飾器模式)。

該類對被裝飾(封裝)的流進行基本的包裹,不增加額外的功能。

BufferedInputStream

BufferedInputStream繼承自FilterInputStream,主要用于緩存讀取自io的資料,避免過多的io操作,當然也是以不可避免的占用了較多的記憶體(因為讀取的io資料都存在數組裡):

public class BufferedInputStream extends FilterInputStream {

    private static int DEFAULT_BUFFER_SIZE = ;

    /**
     * The maximum size of array to allocate.
     */
    private static int MAX_BUFFER_SIZE = Integer.MAX_VALUE - ;

    /**
     * The internal buffer array where the data is stored. 
     */
    protected volatile byte buf[];
           

如上是BufferedInputStream的部分屬性源碼,資料就存在buf[]中。

BufferedInputStream中最重要的一個方法當屬fill(),該方法的作用是:

當緩存(即buf[])中的資料都被取出時,調用屬性in(輸入流),讀取io資料,并将資料存至buf[]中。

一個簡單的執行個體了解下:

byte[] b = {'1','2','3','a','b','c'};
ByteArrayInputStream bais = new ByteArrayInputStream(b);
BufferedInputStream bis = new BufferedInputStream(bais);
try {
    int i = -;
    while( (i=bis.read()) > ){
        System.out.print(i+" ");
    }
} catch (IOException e) {
    e.printStackTrace();
}//輸出結果是49 50 51 97 98 99
           

DataInputStream

該類繼承自FilterInputStream,主要功能是提供了各種和資料操作有關的功能,比如讀取行,讀取一個short, int, double等。

除了read()方法和read(byte[])方法外,該類還提供了一個readFully(byte b[])方法,該方法源碼如下:

public final void readFully(byte b[], int off, int len) throws IOException {
    if (len < )
        throw new IndexOutOfBoundsException();
    int n = ;
    while (n < len) {
        int count = in.read(b, off + n, len - n);
        if (count < )
            throw new EOFException();
        n += count;
    }
}
           

該方法用于讀取長度為len的資料到數組b中,和read(byte b[], int off, int len)不同的是,當in中的資料沒有len這麼多時,會報EOFException,而不會傳回實際讀取的長度。

另外值得注意的是,該類實作了DataInput接口,實作了許多讀取不同類型資料的方法。

byte[] b = {'l','o','d',,,'i',,,,,};
ByteArrayInputStream bais = new ByteArrayInputStream(b);
DataInputStream dis = new DataInputStream(bais);

try {
    byte by = dis.readByte();
    int uby = dis.readUnsignedByte();
    short sh = dis.readShort();
    char ch = dis.readChar();
    int in = dis.readInt();

    System.out.println(by);  //108
    System.out.println(uby); //111
    System.out.println(sh);  //25600
    System.out.println(ch);  //i
    System.out.println(in);  //111          
} catch (IOException e) {
    e.printStackTrace();
}
           

其中DataInputStream也實作了readLine()方法,但官方建議使用BufferedReader替代。

另一個值得注意的方法是readUTF()。該方使用utf編碼的檔案中的内容。其實上,在檔案寫入jdk時,會帶上該檔案的長度。是以readUTF()一開始會讀取檔案的長度,在按照這個長度讀取資料。是以該方法建議讀取打開的檔案。下面的例子雖然不是打開的檔案,但模拟了代開檔案使用readUTF()的方法。

byte[] b = {,,'l','o','v','e','l','i','v','e','!'};
ByteArrayInputStream bais = new ByteArrayInputStream(b);
DataInputStream dis = new DataInputStream(bais);
try {
    String str = dis.readUTF();
    System.out.println(str); //lovelive           
} catch (IOException e) {
    e.printStackTrace();
}
           

FileInputStream

該類用于從檔案讀取資料,它的檔案(對象)可以用關鍵字 new 來建立,也可以直接使用字元串代替。

該類的源碼比較通俗易懂,并沒有特别值得注意的方法或屬性。

具體執行個體如下:

/**
*運作結果:
*4 l o v e
*4 l i v e
*/
try{
    FileInputStream fis = new FileInputStream("test.txt");
    byte[] b = new byte[];
    int i;
    while(( i= fis.read(b))!=-){
        System.out.println(i+" "+(char)b[]+" "+(char)b[]+" "+(char)b[]+" "+(char)b[]);
    }
    fis.close();
} catch(FileNotFoundException e){
} catch(IOException e){
}
           

StringBufferInputStream

該類已經過時,簡單看了下源碼,其實很簡單。

一般推薦使用ByteArrayInputStream替代StringBufferInputStream。

ByteArrayInputStream bais = new ByteArrayInputStream("hello world".getBytes());
           

SequenceInputStream

該類用于存儲多個輸入流,并有序的從第一個流一直讀到最後一個流。

說起來也就是對InputStrem做了一個封裝。

public class SequenceInputStream extends InputStream {
    //用于存儲多個輸入流
    Enumeration<? extends InputStream> e;
    //用于讀取資料的目前輸入流
    InputStream in;
           

其中的一個初始化方法如下:

public SequenceInputStream(InputStream s1, InputStream s2) {
    Vector<InputStream> v = new Vector<>();

    v.addElement(s1);
    v.addElement(s2);
    e = v.elements();
    try {
        nextStream();
    } catch (IOException ex) {
        // This should never happen
        throw new Error("panic");
    }
}
           

其實是先把兩個輸入流轉變為 Enumeration後再存儲在e中。

該類中最重要的方法:

final void nextStream() throws IOException {
    if (in != null) {
        in.close();
    }

    if (e.hasMoreElements()) {
        in = (InputStream) e.nextElement();
        if (in == null)
            throw new NullPointerException();
    }
    else in = null;
}
           

其實就是順序檢查e中的輸入流,并将可用的輸入流賦給in而已。

一個簡單的執行個體如下:

byte[] by1 = {'l','o','v','e'};
byte[] by2 = {'l','i','v','e','!'};
ByteArrayInputStream b1 = new ByteArrayInputStream(by1);
ByteArrayInputStream b2 = new ByteArrayInputStream(by2);
SequenceInputStream sis = new SequenceInputStream(b1, b2);
int c;
try {
    while ( (c = sis.read()) != -){
        System.out.print((char)c);
    }
} catch (IOException e) {
    e.printStackTrace();
}//輸出結果為lovelive!
           

ObjectInputStream

在進入這一節之前,強烈建議先閱讀ObjectOutputStream的部分。

ObjectInputStream可以看做是ObjectOutputStream的逆操作,同時自定義了一個内部類,BlockDataInputStream用于讀取資料。同時利用該類實作了ObjectInput和DataInput中的各方法。

ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
Cat c = new Cat();
c.i++;
oos.writeObject(c);
oos.flush();
byte[] data = baos.toByteArray();

ByteArrayInputStream bais = new ByteArrayInputStream(data);
ObjectInputStream ois = new ObjectInputStream(bais);
Cat c1 = (Cat)ois.readObject();
System.out.println(c1.i); //2
System.out.println(c1.s); //Cat

class Cat implements Serializable{
    public static int i = ;
    public String s = "Cat";
    public Cat(){};
}
           

OutputStream

InputStream暫時告一段落,先看看OutputStream。

和InputStream一樣,OutputStream也是個抽象類,無法被執行個體化。

該類的源碼很少,主要是write()等系列的方法。繼承自該類的子類隻需實作write(int b).

FileOutputStream

和FileInputStream一樣,該類用于操作檔案。官方文檔推薦使用該類操作存byte類型的資料,比如圖檔。

該類有FileOutputStream(File file, boolean append)方法來指明寫入檔案的資料是追加還是覆寫。

FileOutputStream fos = new FileOutputStream("test.txt");
byte[] b = {'l','o','v','e','l','i','v','e'};
fos.write(b);
fos.close();
           

ByteArrayOutputStream

和ByteArrayInputStream一樣,該類同樣對byte數組做一層封裝,将資料寫入自己的屬性中。

/**
* The data can be retrieved using toByteArray() and toString()
*/
public class ByteArrayOutputStream extends OutputStream {
    //The buffer where data is stored.
    protected byte buf[];
    //The number of valid bytes in the buffer.
    protected int count;
           

如上所述,該類在将資料寫入自身的屬性buf後,可以通過toByteArray()和toString()方法讀出。

byte[] by = {'o','v','e'};
ByteArrayOutputStream baos = new ByteArrayOutputStream();
baos.write();
baos.write(by,  ,by.length);
String str = baos.toString();
System.out.println(str);  //love
byte[] by1 = baos.toByteArray();
for(int i = ;i<by1.length;i++){
    System.out.print(by1[i]+" ");  //108 111 118 101
}
           

上述方法使用了ByteArrayOutputStream 最重要的四個方法

write(int b)

write(byte b[], int off, int len)

toByteArray()

toString()

ByteArrayOutputStream 與ByteArrayInputStream互相獨立,兩者不必同時使用。

另外,該類有個方法

writeTo(OutputStream out)

可以将資料寫入指定輸出流中。如下:

FileOutputStream fos = new FileOutputStream("test.txt");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] by = {'o','v','e'};
baos.write();
baos.write(by,  ,by.length);
baos.writeTo(fos);  //檔案test.txt中被寫入love
fos.close();
           

FilterOutputStream

該類是所有 過濾 輸出流的父類。說白了,就是在輸出流上又套了一層操作,和FilterInputStream相對。

DataOutputStream

主要是提供了許多和資料類型有關的輸出流操作。

ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(baos);
byte[] b = {'l','o','v','e'};
dos.write(b);
dos.writeShort();
dos.writeUTF("live");
dos.close();
System.out.println(baos.toString());  //輸出love  live
           

BufferedOutputStream

與BufferedInputStream相對,提供寫入流的緩存。沒啥新東西,不寫例子了。

PrintStream

該類提供格式化輸出流,除了我們了解的螢幕輸出外,該類更多的可以用于格式化輸出檔案。

雖然該類繼承自FilterOutputStream,該類除了可以使用OutputStream初始化外,還可以使用檔案(檔案名)初始化。

在初始化時,可以選擇是否自動清理緩存 :

PrintStream(OutputStream out, boolean autoFlush)

雖然該類也實作了write()等一系列方法,但更多的還是建議使用print()或者println()方法。實際上print()的方法體基本上就是調用write()方法。

一個簡單的測試初始化和寫入的例子:

/**
*class Person{}
*Person定義如上
*/
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PrintStream ps = new PrintStream(baos, true);
String s = "school";
Person p = new Person();
ps.println(p);
ps.close();
byte[] b = baos.toByteArray();
for(int i=;i<b.length;i++){
    System.out.print((char)b[i]);
}//輸出[email protected]
           

上述用于初始化的輸出流換成FileInputStream,則最後結果就不是列印到螢幕上,而是輸出到檔案中。

ObjectOutputStream

用于将基本類型和類的對象寫進輸出流。

對于對象,隻有實作了Serializable的類才能被寫入流(String類實作了Serializable接口)。

該類的源碼過于複雜,這裡隻簡單剖析下基本實作。

在ObjectOutputStream的内部定義了一個私有類,并有一個指向該私有類的引用。

private final BlockDataOutputStream bout;

private static class BlockDataOutputStream extends OutputStream implements DataOutput {
    /** underlying output stream */
    private final OutputStream out;
    /** loopback stream (for data writes that span data blocks) */
    private final DataOutputStream dout;
    //初始化方法,在初始化ObjectOutputStream的同時,
    //初始化了内部的dout,dout=BlockDataOutputStream(out)
    BlockDataOutputStream(OutputStream out) {
        this.out = out;
        dout = new DataOutputStream(this);
    }
    //BlockDataOutputStream的其中一個寫方法,
    //BlockDataOutputStream内部,write()系方法都調用的是
    //out的一系列方法
    public void write(int b) throws IOException {
        if (pos >= MAX_BLOCK_SIZE) {
            drain();
        }
        buf[pos++] = (byte) b;
    }
    //writeXXX()系方法都調用的是dout的一系列方法
    public void writeChar(int v) throws IOException {
        if (pos +  <= MAX_BLOCK_SIZE) {
            Bits.putChar(buf, pos, (char) v);
            pos += ;
        } else {
            dout.writeChar(v);
        }
    }
}   
           

然後,ObjectOutputStream内部對于繼承自ObjectOuput和DataOutput的方法,都是通過調用bout的方法來完成的。

然後我們知道,因為調用的是stream的輸出流(OutputStream)其實寫入的東西都是二進制的byte資料。

ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);

oos.write();
oos.writeInt();
oos.writeDouble();
oos.writeBytes("hello");
oos.writeChars("hello");
oos.writeUTF("hello");
oos.writeObject(new Cat());
oos.flush();
byte[] data = baos.toByteArray();
for(byte b:data){
    System.out.print(b+" ");
}
/*
-84 -19 0 5 119 35 123 0 0 0 123 64 94 -64 16 98 77 -46 -14 104 101 108 108 111 0 104 0 101 0 108 0 108 0 111 0 5 104 101 108 108 111 115 114 0 3 67 97 116 -19 -85 -95 117 -55 -104 -79 -94 2 0 1 73 0 1 105 120 112 0 0 0 0
*/
class Cat implements Serializable{
    public int i = ;
    public Cat(){};
}
           

對于類的屬性的寫入,有一些限制,比如非transient和非靜态屬性。

Reader

看完了位元組流,來看字元流。

Reader也是抽象類,該類實作了兩個接口:

且Reader将Closeable的方法也聲明為了abstract,并自己也聲明了一個abstract方法abstract public int read(char cbuf[], int off, int len)。

是以繼承自Reader的類都必須實作read(char[], int, int)和close()方法。

CharArrayReader

CharArrayReader(圖檔上寫的是CharReader)的功能和ByteArrayInpitStream的功能有些類似,差別在于前者使用char[]數組進行初始化,後者使用byte[]初始化。

public class CharArrayReader extends Reader {
    /** The character buffer. */
    protected char buf[];

    /** The current buffer position. */
    protected int pos;

    /** The position of mark in buffer. */
    protected int markedPos = ;

    /**
     *  The index of the end of this buffer.
     *  data at or beyond this index.
     */
    protected int count;
           

簡單看下執行個體:

char[] cha = {'l','o','v','e','l','i','v','e','!'};
CharArrayReader car= new CharArrayReader(cha);
char[] ch = new char[];
int i = car.read(ch);
//輸出lovelive!
System.out.println(String.valueOf(ch)+(char)car.read());
           

StringReader

和CharArrayReader類似,StringReader以字元串為初始化對象,但奇怪的是,讀取的傳回值仍然是char[]。

隻貼一段簡單的例子好了:

StringReader sr = new StringReader("lovelive!");
char[] ch = new char[];
int i = sr.read(ch);
//輸出lovelive!
System.out.println(String.valueOf(ch)+(char)sr.read());
           

BufferedReader

該類和FilterInputStream的設計思路類似,使用裝飾器模式,緩存字元輸入。

直接貼部分源碼及源碼的注釋:

/**
 * Reads text from a character-input stream, buffering   
 * characters so as to provide for the efficient reading
 * of characters, arrays, and lines.
 * It is advisable to wrap a BufferedReader around any Reader whose read() operations may be costly, such as FileReaders and InputStreamReaders.  For example,
 * BufferedReader in = new BufferedReader(new FileReader("foo.in"));
 * 建議使用BufferedReader來包裝檔案的讀取,這是因為檔案的讀取操作(read())是非常昂貴的(因為檔案涉及到硬體)。
 * will buffer the input from the specified file.  Without buffering, each invocation of read() or readLine() could cause bytes to be read from the file, converted into characters, and then returned, which can be very inefficient.
 *Programs that use DataInputStreams for textual input can be localized by replacing each DataInputStream with an appropriate BufferedReader.
 建議DataInputStreams操作文本這種寫法使用BufferedReader替代。
 */
 public class BufferedReader extends Reader {

    private Reader in;

    private char cb[];
           

因為是buffer,是以和BufferedInputStream一樣,該類同樣有一個fill()方法用于填充自身的緩存cb[]。

StringReader sr = new StringReader("lovelive!\nschool idol");
BufferedReader db = new BufferedReader(sr);
int ch = db.read();
char[] cha = new char[];
db.read(cha);
String s1 = db.readLine();
String s2 = db.readLine();
System.out.print((char)ch);
System.out.print(""+(char)cha[]+(char)cha[]+(char)cha[]+(char)cha[]);
System.out.print(s1);
System.out.print(s2);
//lovelive!school idol
           

上述方法示範了BufferedReader中最重要的兩個讀方法。可以看出,實際上在readLine時,自動剔除了原資料中的換行操作。

LineNumberReader

該類繼承自BufferedReader,用于需要跟蹤行号的字元輸入流。該類多了兩個方法setLineNumber(int lineNumber)和getLineNumber(),其他方法和BufferedReader沒有本質差別。

需要注意的是,該類的getLineNumber()方法實際上并未實際改變緩存中指針的位置,而隻是單純的改變了目前的行号。即該類不支援随意行的讀操作。

StringReader sr = new StringReader("lovelive!\nschool idol");
LineNumberReader lnb = new LineNumberReader(sr);

lnb.setLineNumber();

int s2i = lnb.getLineNumber();
String s2 = lnb.readLine();

int s3i = lnb.getLineNumber();
String s3 = lnb.readLine();

System.out.println(s2i+":"+s2);  //2:lovelive!
System.out.println(s3i+":"+s3);  //3:school idol
           

InputStreamReader

該類是位元組流和字元流的一個橋梁,通過讀取位元組流将其轉換為字元。

注意,這是用到了擴充卡模式。

在初始化時可以設定使用的字元格式。

byte[] b = {'l','o','v','e','l','i','v','e','!'};
ByteArrayInputStream bais = new ByteArrayInputStream(b);
InputStreamReader isr = new InputStreamReader(bais);
String encode = isr.getEncoding();
int i = isr.read();
char[] ch = new char[];
isr.read(ch);
String chs = String.valueOf(ch);
System.out.println(encode+":"+(char)i+chs);  //UTF8:lovelive!  
           

處于性能考慮,建議使用BufferedReader包裝一下InputStreamReader。

FileReader

該類大概是整個io流非抽象類最簡單的一個,其實就是對InputStreamReader增加了幾個針對檔案的初始化方法。

還是直接看例子吧:

/**test.txt
 *hello
 *world
 */
FileReader fr = new FileReader("test.txt");
char[] by = new char[];
fr.read(by);
String s = String.valueOf(by);
fr.close();
System.out.println(s);
//hello
//world
           

Writer

來看看IO流的最後一個子產品writer吧。

該抽象類實作了三個接口:

public abstract class Writer implements Appendable, Closeable, Flushable {

    /**
     * Temporary buffer used to hold writes of strings and single characters
     */
    private char[] writeBuffer;
           

分别是追加接口(Appendable),關閉接口(Closeable),重新整理緩存接口(Flushable)。

CharArrayWriter

類似CharArrayrReader,子什麼包含一個存儲字元的數組。

CharArrayWriter caw = new CharArrayWriter();
caw.write('l');
char[] ch = {'o','v','e'};
caw.write(ch);
caw.write("liv");
caw.append("e!");
String s1 = String.valueOf(caw.toCharArray());
String s2 = caw.toString();
System.out.println(s1);  //lovelive!
System.out.println(s2);  //lovelive!
           

CharArrayrReader還有一個寫向其他流的方法:writeTo(Writer out)

StringWriter

該類其實是對StringBuffer做了一層包裝。可以傳回String,也可以傳回StringBuffer,源碼十分簡單,就不舉例子了。

FilterWriter

抽象類,包含一個Writer類型的屬性,其他方法都是對于該屬性操作的封裝,同理于FilterReader,使用裝飾器模式。目前暫無實作類。

OutputStreamWriter

将字元轉換為位元組。關于該類,需要注意的是,每一次的read()方法都會進行轉碼(encoding converter)操作。是以官方建議使用BufferedWriter進行包裝。

Writer out = new BufferedWriter(new OutputStreamWriter(System.out));
           

一個簡單的例子如下:

ByteArrayOutputStream baos = new ByteArrayOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(baos);
String encode = osw.getEncoding();
osw.write('l');
char[] ch = {'o','v','e'};
osw.write(ch);
osw.write("liv");
osw.append("e!");
osw.flush();
System.out.println(encode+":"+baos.toString());  //GBK:lovelive!
           

FileWriter

該類其實就是在OutputStreamWriter的基礎上增加了初始化檔案的構造方法。簡單的不能再簡單的類,直接看源代碼吧。

BufferedWriter

帶有緩存的寫入流,當write()操作十分昂貴時,建議使用該類進行包裝。

CharArrayWriter caw = new CharArrayWriter();
FileWriter fw = new FileWriter("test.txt");
BufferedWriter bw1 = new BufferedWriter(caw);
BufferedWriter bw2 = new BufferedWriter(fw);
bw1.write("love");
bw2.write("love");
bw1.newLine();
bw2.newLine();
bw1.write("live!");
bw2.write("live!");
bw1.close();
bw2.close();
System.out.println(caw.toString());  
//love
//live!
fw.close();
//檔案裡是
//love
//live!
           

PrintWriter

格式化輸出到文本的内容。

值得一提的是,該類既可以使用writer進行初始化,又可以使用OutputStream進行初始化,還可以使用字元串進行初始化。

值得一提的是:

//下面兩個初始化方法等價
PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter("foo.out")));
PrintWriter out = new PrintWriter("foo.out");
           

除了本身的write()一系列方法,該類還提供了各種資料類型的列印方法(print)。當然也包括append()方法。

PrintWriter pw = new PrintWriter("test.txt");
pw.print("lovelive!");
pw.println();
pw.println("μ's");
pw.close();
//檔案的内容是:
//lovelive!
//μ's
           

到這裡io流的方法暫告一段落。

pip等系列的io待更。