• 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
  • 相关阅读:
    @controller和@restController注解详解
    customer.sql
    jsp自定义标签
    git常用命令
    dubbo问题
    idea maven项目的移除添加
    bean type not found
    利率配置修改时选中下拉框时,加alert选中,否则不选中
    Vmware文件类型
    抖音平台分析
  • 原文地址:https://www.cnblogs.com/wxgblogs/p/5647197.html
Copyright © 2020-2023  润新知