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
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; } }
不忘初心,方得始终