主要内容包括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;
}
}
05-02 00:57