OutputStream類詳解


      主要內容包括OutputStream及其部分子類,以分析源代碼的方式學習。關心的問題包括:每個字節輸出流的作用,各個流之間的主要區別,何時使用某個流,區分節點流和處理流,流的輸出目標等問題。 OutputStream的類樹如下所示,其中,ObjectOutputStream和PipedOutputStream本文將不做討論。

java.io.OutputStream (implements java.io.Closeable, java.io.Flushable)
    java.io.ByteArrayOutputStream
    java.io.FileOutputStream
    java.io.FilterOutputStream
        java.io.BufferedOutputStream
        java.io.DataOutputStream (implements java.io.DataOutput)
        java.io.PrintStream (implements java.lang.Appendable, java.io.Closeable)
    java.io.ObjectOutputStream (implements java.io.ObjectOutput, java.io.ObjectStreamConstants)
    java.io.PipedOutputStream
View Code

OutputStream源碼分析

package java.io;

//它是抽象類,並且實現了兩個接口Closeable和Flushable。
public abstract class OutputStream implements Closeable, Flushable {

    //作為抽象類中唯一的抽象方法,(非抽象)子類必須實現這個方法。
    //我們可以看到,這個類還提供了另外兩個write方法,但是它們最終都是要調用這個方法來完成具體的實現
    //對於一個輸出流,我們需要關心輸出的內容到哪里去了,從這個write方法中我們根本看不到輸出的目的地,所以實現這個方法的子類必須告訴這一點
    //而實現這個方法的子類,就是節點流。
    //注意:作為字節輸出流,為何這里參數傳遞為int型,而非byte型,這個在后面子類實現中再分析
    public abstract void write(int b) throws IOException;

    //此方法直接輸出一個字節數組中的全部內容,調用了下面的write方法
    public void write(byte b[]) throws IOException {
        write(b, 0, b.length);
    }

    //功能:要輸出的內容已存儲在了字節數組b[]中,但並非全部輸出,只輸出從數組off位置開始的len個字節。因此,需要對傳入的三個參數作合理性判斷
    public void write(byte b[], int off, int len) throws IOException {
        //數組不能為空,否則拋出NullPointerException
        if (b == null) {
            throw new NullPointerException();
        } else if ((off < 0) || (off > b.length) || (len < 0) ||
                   ((off + len) > b.length) || ((off + len) < 0)) {
              //此處判斷off+len<0是多余的
              throw new IndexOutOfBoundsException();
        } else if (len == 0) {
            return;
        }
        //最終會調用第一個write方法。注意:1.子類可能會復寫當前的write方法;2.在輸出的過程中,還是一個一個字節輸出的。
        for (int i = 0 ; i < len ; i++) {
            write(b[off + i]);
        }
    }

    //這兩個方法就是實現兩個接口時分別需要實現的方法,但這里方法中內容是空的,子類可以override這兩個方法,如果子類不復寫,則此方法為空。 
    public void flush() throws IOException { }
    public void close() throws IOException { }

}
View Code

  關於override父類或接口的方法時,原以為要和父類或接口中聲明的一樣,包括權限,現在看來不然。

package java.io;
import java.io.IOException;
public interface Flushable {
    //此處的方法權限為包權限,而在OutputStream中則成為了public權限    
    void flush() throws IOException;
}

package java.lang;
public interface AutoCloseable {
    //此處的方法權限為包權限,而子接口Closeable中也變成了public權限
    void close() throws Exception;
}

package java.io;
import java.io.IOException;
public interface Closeable extends AutoCloseable {
    public void close() throws IOException;
} 
View Code

ByteArrayOutputStream

package java.io;
import java.util.Arrays;
public class ByteArrayOutputStream extends OutputStream {
    //這里可以回答輸出流寫到哪里的問題:當我們調用write方法時,把內容都存儲到了這個byte數組buf中,且是按照追加的方式添加
    //而count則指向下一個可以寫入的位置,它的初始值默認為0
    protected byte buf[];
    protected int count;

    public ByteArrayOutputStream() {
        this(32);
    }
    //類的構造方法只有兩個,實際的工作只是在堆中為數組buf申請一塊內存,大小可以指定,默認大小為32 
    public ByteArrayOutputStream(int size) {
        if (size < 0) {
            throw new IllegalArgumentException("Negative initial size: "
                                               + size);
        }
        buf = new byte[size];
    }

    //此方法是確保buf的大小不少於minCapacity,如果buf的空間不夠,則調用grow()方法來擴展空間。
    private void ensureCapacity(int minCapacity) {
        if (minCapacity - buf.length > 0)
            grow(minCapacity);
    }

    //這個方法的實現值得我們思考一些問題:數組空間不夠了,需要擴展,該如何擴展呢?
    //我們可能會這樣做:既然你需要minCapacity這么多,那就擴展這么多吧。這里沒有這么做,如果這樣做,那當用戶說我還需要一個字節的空間,那我們就又要在擴展一次,而每一次擴展,都會很耗時。
    //耗時的原因是擴展的方式,本人猜測應該是這么擴展(不確定):重新申請更大的一塊內存,然后把原數組的內容拷貝過去。若真如此,那確實會很耗時。
    //這里的策略是:先把原數組的大小通過左移運算擴展為2倍,若這樣還不夠,那再把大小改為你需要的大小minCapacity。
    //注意:左移運算可能會溢出,使得數組大小變為負數,如果存在溢出,則將其改為Integer.MAX_VALUE。這樣的大小是肯定夠的,如果這樣還不夠,那么你傳入的minCapacity參數一定有問題
    private void grow(int minCapacity) {
        int oldCapacity = buf.length;
        int newCapacity = oldCapacity << 1;
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity < 0) {
            if (minCapacity < 0) // overflow
                throw new OutOfMemoryError();
            newCapacity = Integer.MAX_VALUE;
        }
        //確定擴展后的數組大小后,通過調用Arrays.copyOf來復制數組,大家可以去研究看是否是先申請更大的一塊內存,然后在拷貝。
        buf = Arrays.copyOf(buf, newCapacity);
    }

    //這里實現了父類的抽象方法,從它可以看出,輸出流的內容都到了這個類在堆中申請的內存中了,己buf數組。
    //現在也可以回答另外一個問題:對於字節流為何傳入int型參數。
    //首先,無論用戶傳入何種類型參數,我們都強制轉換為byte類型。這樣可以方便用戶,因為它不需要自己實現強制類型轉換
    //舉例:int a = 10;  write((byte)a);
    //要求用戶傳入byte類型時,用戶需要自己做強制類型轉換,但現在我們幫用戶做了,豈不方便?
    //這樣一來,用戶在使用時必須注意這一點:這是字節輸出流,如果傳入short、char或int等,只把它當作byte處理。
    public synchronized void write(int b) {
        ensureCapacity(count + 1);
        buf[count] = (byte) b;
        count += 1;
    }

    //override了父類的方法,把byte b[]中從off開始的len個字節復制到了buf的后面,同時count增加了len
    public synchronized void write(byte b[], int off, int len) {
        if ((off < 0) || (off > b.length) || (len < 0) ||
            ((off + len) - b.length > 0)) {
            throw new IndexOutOfBoundsException();
        }
        ensureCapacity(count + len);
        System.arraycopy(b, off, buf, count, len);
        count += len;
    }

    //調用此方法,則用戶可以把buf中的全部內容輸出到用戶傳入的輸出流中
    public synchronized void writeTo(OutputStream out) throws IOException {
        out.write(buf, 0, count);
    }

    public synchronized void reset() {
        count = 0;
    }

    //調用此方法,則用戶可以得到一個byte數組,其內容為buf中的全部內容
    public synchronized byte toByteArray()[] {
        return Arrays.copyOf(buf, count);
    }

    public synchronized int size() {
        return count;
    }

    public synchronized String toString() {
        return new String(buf, 0, count);
    }

    public synchronized String toString(String charsetName)
        throws UnsupportedEncodingException
    {
        return new String(buf, 0, count, charsetName);
    }

    @Deprecated
    public synchronized String toString(int hibyte) {
        return new String(buf, hibyte, 0, count);
    }

    //個人認為,此方法既然與父類一樣為空,但又寫一遍是否多余?為何不像flush方法一樣,在這里省去不寫
    public void close() throws IOException {
    }
}
View Code

FileOutputStream

  這個類比較復雜,其中還包含nio包中的內容,因此我只看明白了其中一小部分:它是節點流;我們用它來寫文件很方便。

package java.io;
import java.nio.channels.FileChannel;
import sun.nio.ch.FileChannelImpl;
import sun.misc.IoTrace;
public class FileOutputStream extends OutputStream{
    public FileOutputStream(String name) throws FileNotFoundException {
        this(name != null ? new File(name) : null, false);
    }

    public FileOutputStream(String name, boolean append)
        throws FileNotFoundException
    {
        this(name != null ? new File(name) : null, append);
    }
    public FileOutputStream(File file) throws FileNotFoundException {
        this(file, false);
    }

    //構造方法一共5個,但實質上只有兩個,這是其中一個,另一個是public FileOutputStream(FileDescriptor fdObj),但我都看不懂
    //只說我理解的比較簡單的東西:當我們寫文件時,我們會選擇這個類,原因就是它提供了方法使我們方便地寫文件
    //它的構造方法--我們可以直接傳入一個File對象,或者代表文件pathName的String,我們就可以指明輸出流的目標是哪個文件了
    //其中,append表示是否以追加方式寫文件,默認為false,則會覆蓋之前文件中的內容
    public FileOutputStream(File file, boolean append)
        throws FileNotFoundException
    {
        ... ...
   }

    //這是必須實現父類的那個方法,我們看不到具體實現,因為它是native方法
    //我們選擇這個類操作文件的另一個原因是:這個方法的實現細節一定包含相關的文件操作命令,而其它類不具備這個方法,則不能把流寫到文件中 
    private native void write(int b, boolean append) throws IOException;
}
View Code

FilterOutputStream

  它不是節點流,與父類主要差別就是它多了個成員變量。我們一般不會使用這個類,它是另外三個節點輸出流的父類。理解它很簡單:它什么活也不干,都交給傳入的out去做。

package java.io;
public class FilterOutputStream extends OutputStream {
    //此成員變量非常重要,基本上這個類和其父類OutputStream的最主要差別就是它有這個成員變量
    //注意到權限為protected,因此在子類中可以直接使用
    protected OutputStream out;

    //構造方法,傳入OutputStream子類對象后,基本上該FilterOutputStream做的事情,它全交給這個傳入的對象去做
    public FilterOutputStream(OutputStream out) {
        this.out = out;
    }
    //我們一般從這個方法中就能看到節點輸出流的目的地,這里它並沒有真正實現,只是調用了傳入的out去做,所以FilterOutputStream不是節點流
    public void write(int b) throws IOException {
        out.write(b);
    }
    //表面上調用了下面的write方法,最終還是調用了out的write方法
    public void write(byte b[]) throws IOException {
        write(b, 0, b.length);
    }
    //間接調用out的write方法,以字節為單位地輸出
    //這里對傳入的參數的判斷比較有意思,雖然對參數的要求與OutputStream對應方法對參數的要求一致,但形式確不一樣了
    //我的理解:四個量是或的關系,若有一個為負,則最高位必定為1,則最終結果一定為負,因此要求都不能為負
    public void write(byte b[], int off, int len) throws IOException {
        if ((off | len | (b.length - (len + off)) | (off + len)) < 0)
            throw new IndexOutOfBoundsException();

        for (int i = 0 ; i < len ; i++) {
            write(b[off + i]);
        }
    }
    //自己不做,交給out去flush
    public void flush() throws IOException {
        out.flush();
    }
    //自己不做,交給out去close,但是關閉前先調用了flush方法
    public void close() throws IOException {
        try {
          flush();
        } catch (IOException ignored) {
        }
        out.close();
    }
}
View Code

BufferedOutputStream

  它是處理流,有個緩沖數組,能起到緩沖作用,似乎緩沖很有用,詳細就不懂了

package java.io;
public class BufferedOutputStream extends FilterOutputStream {  
    //這個類的核心就是這個buf,會將要輸出的內容先存在這個數組里,當這個數組滿之后再一次全部輸出,當然未滿是也可以主動輸出
    //這個buf似乎與ByteArrayOutputStream有些像,但還是有差別:這個buf大小固定后不會再擴展空間
    protected byte buf[];
    protected int count;
    public BufferedOutputStream(OutputStream out) {
        this(out, 8192);
    }

    //此構造方法需要傳入OutputStream實例,可以設置buf大小,默認為8192字節    
    public BufferedOutputStream(OutputStream out, int size) {
        super(out);
        if (size <= 0) {
            throw new IllegalArgumentException("Buffer size <= 0");
        }
        buf = new byte[size];
    }

    //private方法,將buf中緩存的內容全部輸出
    private void flushBuffer() throws IOException {
        if (count > 0) {
            out.write(buf, 0, count);
            count = 0;
        }
    }
    //這個寫方法根本沒有寫,只是把要寫的內容先存到了buf中。如果buf已經滿了,那才會先把buf內容輸出,然后再向buf里寫
    public synchronized void write(int b) throws IOException {
        if (count >= buf.length) {
            flushBuffer();
        }
        buf[count++] = (byte)b;
    }
    public synchronized void write(byte b[], int off, int len) throws IOException {
        //如果要寫入的字節數len比buf的長度還大,那就不需要緩沖了,直接調用out的write方法寫就可以
        if (len >= buf.length) {    
            flushBuffer();
            out.write(b, off, len);
            return;
        }
        //如果buf剩余的空間比len小,那就先輸出buf內容,騰出空間后再寫
        if (len > buf.length - count) {
            flushBuffer();
        }
        System.arraycopy(b, off, buf, count, len);
        count += len;
    }
    //用戶需要調用此方法才能實現真正的輸出,但是不要每次調用write都緊接着調用flush,那就失去了緩沖的意義了
    //另:在close時,父類FilterOutputStream會調用flush方法的,不用擔心,所以你如果調用close的話,該輸出的都會輸出
    public synchronized void flush() throws IOException {
        flushBuffer();
        out.flush();
    }
}
View Code

DataOutputStream  

  處理流,提供了多個很常用的方法。

package java.io;
public class DataOutputStream extends FilterOutputStream implements DataOutput {
    //這個written參數會不斷地累加,但有什么意義沒弄明白
    protected int written;
    private byte[] bytearr = null;

    public DataOutputStream(OutputStream out) {
        super(out);
    }

    private void incCount(int value) {
        int temp = written + value;
        if (temp < 0) {
            temp = Integer.MAX_VALUE;
        }
        written = temp;
    }

    public synchronized void write(int b) throws IOException {
        out.write(b);
        incCount(1);
    }

    public synchronized void write(byte b[], int off, int len)
        throws IOException
    {
        out.write(b, off, len);
        incCount(len);
    }

    public void flush() throws IOException {
        out.flush();
    }

    //這個類的核心就是為我們提供了類似writeBoolean這樣的方法,我們可以方便地把這些常見類型轉為字節並輸出,因為這是字節流
    public final void writeBoolean(boolean v) throws IOException {
        out.write(v ? 1 : 0);
        incCount(1);
    }

    //直接輸出
    public final void writeByte(int v) throws IOException {
        out.write(v);
        incCount(1);
    }

    //short占兩個字節,那么就先把高字節輸出,再把低字節輸出
    //>>>表示無符號右移,右移8位后在與0xFF做與運算,則可保證此int值的更高位為零,也就是只保留了原int的8-15位
    public final void writeShort(int v) throws IOException {
        out.write((v >>> 8) & 0xFF);
        out.write((v >>> 0) & 0xFF);
        incCount(2);
    }

    //與writeShort完全一致,我的理解是這樣在使用時名稱很形象
    public final void writeChar(int v) throws IOException {
        out.write((v >>> 8) & 0xFF);
        out.write((v >>> 0) & 0xFF);
        incCount(2);
    }

    //先高字節內容,后低字節
    public final void writeInt(int v) throws IOException {
        out.write((v >>> 24) & 0xFF);
        out.write((v >>> 16) & 0xFF);
        out.write((v >>>  8) & 0xFF);
        out.write((v >>>  0) & 0xFF);
        incCount(4);
    }

    private byte writeBuffer[] = new byte[8];

    //這里沒有像之前一個字節一個字節地寫,而是先存到writeBuffer中,可能是覺得這樣更好,怎么個好法,不懂
    public final void writeLong(long v) throws IOException {
        writeBuffer[0] = (byte)(v >>> 56);
        writeBuffer[1] = (byte)(v >>> 48);
        writeBuffer[2] = (byte)(v >>> 40);
        writeBuffer[3] = (byte)(v >>> 32);
        writeBuffer[4] = (byte)(v >>> 24);
        writeBuffer[5] = (byte)(v >>> 16);
        writeBuffer[6] = (byte)(v >>>  8);
        writeBuffer[7] = (byte)(v >>>  0);
        out.write(writeBuffer, 0, 8);
        incCount(8);
    }

    //float和int型都占用4個字節,因此對float轉為對應的int字節流,再調用writeInt
    //Float.floatToIntBits(v)這個方法的實現可能與IEEE規范中關於浮點數規范有關
    public final void writeFloat(float v) throws IOException {
        writeInt(Float.floatToIntBits(v));
    }

    //double和long型都占用8個字節,因此對double轉為對應的long字節流,再調用writeLong
    //Double.doubleToLongBits(v)這個方法的實現可能與IEEE規范中關於浮點數規范有關 
    public final void writeDouble(double v) throws IOException {
        writeLong(Double.doubleToLongBits(v));
    }

    //還可以byte處理字符串
    public final void writeBytes(String s) throws IOException {
        int len = s.length();
        for (int i = 0 ; i < len ; i++) {
            out.write((byte)s.charAt(i));
        }
        incCount(len);
    }

    //還可以char處理字符串
    public final void writeChars(String s) throws IOException {
        int len = s.length();
        for (int i = 0 ; i < len ; i++) {
            int v = s.charAt(i);
            out.write((v >>> 8) & 0xFF);
            out.write((v >>> 0) & 0xFF);
        }
        incCount(len * 2);
    }

    public final void writeUTF(String str) throws IOException {
        writeUTF(str, this);
    }

    //可以處理utf-8,這個方法很常用,但其實現還需仔細學習
    static int writeUTF(String str, DataOutput out) throws IOException {
        int strlen = str.length();
        int utflen = 0;
        int c, count = 0;

        /* use charAt instead of copying String to char array */
        for (int i = 0; i < strlen; i++) {
            c = str.charAt(i);
            if ((c >= 0x0001) && (c <= 0x007F)) {
                utflen++;
            } else if (c > 0x07FF) {
                utflen += 3;
            } else {
                utflen += 2;
            }
        }

        if (utflen > 65535)
            throw new UTFDataFormatException(
                "encoded string too long: " + utflen + " bytes");

        byte[] bytearr = null;
        if (out instanceof DataOutputStream) {
            DataOutputStream dos = (DataOutputStream)out;
            if(dos.bytearr == null || (dos.bytearr.length < (utflen+2)))
                dos.bytearr = new byte[(utflen*2) + 2];
            bytearr = dos.bytearr;
        } else {
            bytearr = new byte[utflen+2];
        }

        bytearr[count++] = (byte) ((utflen >>> 8) & 0xFF);
        bytearr[count++] = (byte) ((utflen >>> 0) & 0xFF);

        int i=0;
        for (i=0; i<strlen; i++) {
           c = str.charAt(i);
           if (!((c >= 0x0001) && (c <= 0x007F))) break;
           bytearr[count++] = (byte) c;
        }

        for (;i < strlen; i++){
            c = str.charAt(i);
            if ((c >= 0x0001) && (c <= 0x007F)) {
                bytearr[count++] = (byte) c;

            } else if (c > 0x07FF) {
                bytearr[count++] = (byte) (0xE0 | ((c >> 12) & 0x0F));
                bytearr[count++] = (byte) (0x80 | ((c >>  6) & 0x3F));
                bytearr[count++] = (byte) (0x80 | ((c >>  0) & 0x3F));
            } else {
                bytearr[count++] = (byte) (0xC0 | ((c >>  6) & 0x1F));
                bytearr[count++] = (byte) (0x80 | ((c >>  0) & 0x3F));
            }
        }
        out.write(bytearr, 0, utflen+2);
        return utflen + 2;
    }

    //不知道這個方法有什么用
    public final int size() {
        return written;
    }
}
View Code


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM