主要內容包括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
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 { } }
關於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; }
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 { } }
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; }
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(); } }
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(); } }
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; } }