JAVA篇:Java IO (四) 过滤流、缓冲流、推回输入流、特殊流

2021/7/11 12:35:47

本文主要是介绍JAVA篇:Java IO (四) 过滤流、缓冲流、推回输入流、特殊流,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

4、过滤流、缓冲流、推回输入流、特殊流

字节流中的过滤过滤流FilterInputStream和FilterOutputStream的作用是“为基础流提供一些额外的功能”。其常见子类包含缓冲流(BufferedInputStream/ BufferedOutputStream )、特殊流(DataInputStream /DataOutputStream)以及推回输入流(PunshbackInputStream):

  1. 特殊流(DataInputStream/DataOutputStream):数据输入/输出流,以机器无关的方式读取Java的基本类型。

  2. 缓冲流(BufferedInputStream/ BufferedOutputStream ):由于基础输入流一个字节一个字节地读取,频繁与磁盘进行交互,造成读取速度比较低,缓冲流的存在就是先将数据读取到缓冲流(内存中),然后一次性从内存中读取多个字符,提高读取的效率。之前介绍的其他的流,如果想要提高读取效率,建议应用缓冲流。

  3. 推回输入流(PunshbackInputStream):Java中读取数据的方式是顺序读取,如果某个数据不需要读取,需要程序处理,PushBackInputStream就可以将某些不需要的数据回退到缓冲中。

        int getnum = 1024;
        byte[] b = new byte[getnum];
​
        String readpath = "src/liwx/learning/CollectTest.java";
        //读取java 文件
        BufferedInputStream bufferedInputStream = null;//缓冲流
        try{
            FileInputStream fileinputStream = new FileInputStream(readpath);
            
            bufferedInputStream = new BufferedInputStream(fileinputStream);//缓冲流
            
            int bytesRead = 0;
            while ((bytesRead = bufferedInputStream.read(b)) != -1) {
                System.out.println(new String(b,0,bytesRead));
​
            }
​
            fileinputStream.close();
            //fileinputStream.read();//java.io.IOException: Stream Closed
​
        }catch (FileNotFoundException e){
            e.printStackTrace();
        }catch (IOException e){
            e.printStackTrace();
        }

 

  如上所示,就是一个缓冲流BufferedInputStream的应用实例,可以看出来是给缓冲流传入一个流对象后,通过缓冲流操作该流的读取。一般我们想为某个类添加功能,可以进行继承,作为子类来实现某些功能,而过滤流的这些子类却有所不同。

  过滤流的所有子类都是为基础流输入输出提供一些额外的功能(如缓冲),为什么不直接继承基础流,将基础流中的方法直接进行“装饰”?这里就要说要装饰者模式和继承的区别。

装饰者模式和继承

  装饰者模式,就是将原有的基础流进行“装饰”。譬如说,当我们向缓冲流传入一个基础流的实例的之后,通过缓冲流调用read(),缓冲流可以进行一些操作后再调用基础流对应的read()方法,除了进行基础流read()方法对应的操作外,还实现了一些其他的需要的功能,当你不想实现这些额外的功能也可以不要用缓冲流进行封装。装饰者和被装饰者之间不会产生耦合

而继承,则是继承父类,通过重写来实现新的功能。如,若使用继承的方法为基础流添加缓冲的功能,就需要分别继承这些基础流来实现对应的缓冲类,那么这些类就会爆炸式增长,而且类之间的耦合性特别高。

装饰者模式存在的意义就在于此处,相比继承,没有这么多繁杂的类,而且类与类之间的耦合性降低。对于某些特定的功能,可以实现某些“添加某些功能”的进行“装饰”的功能类,当需要实现这些功能可以将对应的基础类传入,功能类对传入的基础流可以进行一些额外的操作,实现某些功能。

4.1 过滤流

  过滤流分字节输入过滤流FilterInputStream,字节输出过滤流FilterOutputStream,字符输入过滤流FilterReader,字符输出过滤流FilterWriter。

作为这些“装饰类”的父类,过滤流并没有添加特殊功能,只是给装饰类的实现搭了一个框架,他们各自维系了四种抽象基类的实例参数(可指向基础流),并且每一个继承自父类的方法都是通过调用传入的抽象基类的实例对应的防范实现的。

字节输入过滤流FilterInputStream继承了基础字节输入流InputStream,维持了一个字节输入流的变量,可以传入任何字节输入流的子类实例,其未添加特殊的功能,相应方法通过调用传入实例in的方法实现,如read()方法的实现就是in.read()。

   protected volatile InputStream in;
​
    protected FilterInputStream(InputStream in) {
        this.in = in;
    }

 

  同样,继承基础字节输出流OutputStream的字节输出过滤流FilterOutputStream,继承基础字符输入流Reader的字符输入过滤流FilterReader和继承基础字符输出流Writer的FilterWriter也是如此。

    protected OutputStream out;
    public FilterOutputStream(OutputStream out) {
        this.out = out;
    }
   protected Reader in;
​
    protected FilterReader(Reader in) {
        super(in);
        this.in = in;
    }
    protected Writer out;
​
    protected FilterWriter(Writer out) {
        super(out);
        this.out = out;
    }

 

  只是有些疑惑在于这四种过滤流的差别……字节过滤流的基础流变量in/out由volatile所修饰,而字符过滤流的对应变量并没有,而且字符过滤流的两个类都定义成抽象类(虽然并无抽象方法)。不过字节流和字符流的实现本就不是一一对应的,譬如字符缓冲流(BufferReader和BufferWriter)就不是继承的字符过滤流,而是直接继承Reader和Writer。

4.2 缓冲流

  缓冲流是我们比较常见的流了,主要是起到缓冲的作用,减少与磁盘的频繁交互,提高读取与写入的效率。主要是维护一个可设定大小的缓冲区,在读写时写写入缓冲区,再返回足够的数据,若下次读取时缓冲区数据足够则直接从缓冲区读取。而在写入时,则先写入缓冲区,在一定条件(缓冲区写满了,或者调用flush时。)则调用基础流一次性写入,

4.2.1 BufferInputStream 字节输入缓冲流

  java.io.BufferInputStream可以封装基础流,进行读取缓冲,从底层读取数据存入缓冲区,在需要数据时优先从缓冲区获取数据,缓冲区数据不足时则重新从底层流读取数据。同时java.io.BufferInputStream还支持mark和reset操作。

  首先是有关缓冲区byte buf[],在BufferInputStream中设置了缓冲区的默认大小8192,和MAX_BUFFER_SIZE,在构造时,可使用默认大小设置缓冲区也可以传入设定好的大小。而私有常量bufUpdater,为buf提供compareAndSet的原子更新程序,最终会在colse()方法中调用。因为关闭可以是异步的所以这个是必要的。我们使用buf[]的空值作为该流已关闭的主要指示符。

    private static int DEFAULT_BUFFER_SIZE = 8192;//缓冲区的默认大小
    private static int MAX_BUFFER_SIZE = Integer.MAX_VALUE - 8;
    protected volatile byte buf[];
​
    private static final
        AtomicReferenceFieldUpdater<BufferedInputStream, byte[]> bufUpdater =
        AtomicReferenceFieldUpdater.newUpdater
        (BufferedInputStream.class,  byte[].class, "buf");
​
    public void close() throws IOException {
        byte[] buffer;
        while ( (buffer = buf) != null) {
            if (bufUpdater.compareAndSet(this, buffer, null)) {/////
                InputStream input = in;
                in = null;
                if (input != null)
                    input.close();
                return;
            }
            // Else retry in case a new buf was CASed in fill()
        }
    }
}
 

  而BufferInputStream还提供了两个方法,一是用来在使用前确认基础流是否是否被关闭getInIfOpen(),一个是用来确认缓冲区是否可用getBufIfOpen()

  而所实现的read相关的方法,在其中最主要的方式就是fill()。其重要的变量及实现如下所示:

  
//缓冲区可用的字节数量
    protected int count;
   //缓冲区当前读取位置
    protected int pos;
    //mark的位置,即reset时pos需要跳回的位置,一个类似于“书签”的位置    
    protected int markpos = -1;
   //限制了设置“书签”后再读取多少个字节以内reset方法还有效
    protected int marklimit;
 

其中的pos指向缓冲区中下一个读取的位置,count是记录缓冲区中可用的字节总数,当pos>=count就需要使用基础流重新读取数据填充缓冲区。

  当调用mark方法时,内部会保存一个markPos标志,它的值为目前读取字节流的位置pos。当调用reset方法时,会把po重置为markPos,可以重新从该位置读取已经读取过的字节。

  而参数markLimit则是限制,在调用mark后读取了多少个字节后,这个“书签”不再有效,无法进行reset。

 

  在fill()方法中我们所做的有两件事情,一是确认pos的位置,即我们要重新读取的字节防止的起始位置情况,二是从基础流读取数据填充满缓冲区。当确认了pos的位置,后续的读取才可以进行。在read方法中调用fill()的前提是pos>=count,所以在读取前重置缓冲区有以下几种情况:

  • 情况1:从未调用过mark,在pos>=count的情况下可将pos和count直接重置为0,直接从0开始读取。

  • 情况2::该条件是隐含的,因为不需要做任何调整。由于设置了“书签”,要保留markPos到pos中间的数据,不能直接将pos设置为0,而缓冲区的空间还足够,所以直接从当前位置放入新的字节,使用剩余空间来读取数据。

  • 情况3:在设置了“书签”而缓冲区又不够的时候,则有以下几种方案进行调整。

    • a)若是“书签”markpos位置>0,那么将markpos到pos的整个数据区域挪到缓冲区开头,那么就有了读取新数据的空间。

    • b)若makrpos==0,那么在需要保留整个数据区域的时候,有两种情况 b1)判断书签是否失效,若是失效了,那么直接当做没有调用过mark,将pos置为0;b2)若是书签未失效,则尝试拓展缓冲区。

​
  
private void fill() throws IOException {
        byte[] buffer = getBufIfOpen();//获取缓冲区
        if (markpos < 0)
            pos = 0;       /* 情况1:从未调用过mark */
        //else if(pos < buffer.length){}/*情况2(隐含):空间足够,不需要调整*/
        else if (pos >= buffer.length)  /* 情况3:之前有设置“书签”,而且缓冲区空间不足 */
            if (markpos > 0) {  //a)若是“书签”位置>0,则又有了空间
                int sz = pos - markpos;
                System.arraycopy(buffer, markpos, buffer, 0, sz);
                pos = sz;
                markpos = 0;
            } else if (buffer.length >= marklimit) {//b1) “书签”失效了
                markpos = -1;   /* buffer got too big, invalidate mark */
                pos = 0;        /* drop buffer contents */
            } else if (buffer.length >= MAX_BUFFER_SIZE) {//b2) 尝试拓展缓冲区,但是超出最大限制报错
                throw new OutOfMemoryError("Required array size too large");
            } else {            /* b2)拓展缓冲区 */
                int nsz = (pos <= MAX_BUFFER_SIZE - pos) ?
                        pos * 2 : MAX_BUFFER_SIZE;
                if (nsz > marklimit)
                    nsz = marklimit;
                byte nbuf[] = new byte[nsz];
                System.arraycopy(buffer, 0, nbuf, 0, pos);
                if (!bufUpdater.compareAndSet(this, buffer, nbuf)) {
                    // Can't replace buf if there was an async close.
                    // Note: This would need to be changed if fill()
                    // is ever made accessible to multiple threads.
                    // But for now, the only way CAS can fail is via close.
                    // assert buf == null;
                    throw new IOException("Stream closed");
                }
                buffer = nbuf;
            }
        count = pos;
        int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
        if (n > 0)
            count = n + pos;
    }

 

4.2.2 BufferOutputStream 字节输出缓冲流

  java.io.BufferOutputStream可以封装基础流,进行写入缓冲,应用程序先将字节写入缓冲区,当缓冲区满了以后再调用基础流写入数据。

  java.io.BufferOutputStreamjava.io.BufferInputStream的源码简单些,在构造时设置缓冲区的大小(默认8192),当调用write时发现缓冲区剩余空间不足以写入或者调用flush()方法时,则调用flushBuffer方法使用基础流将数据写入。

  
    protected byte buf[];
​
    protected int count;
    private void flushBuffer() throws IOException {
        if (count > 0) {
            out.write(buf, 0, count);
            count = 0;
        }
    }

 

  疑惑的是close方法并未重写,不会调用flushBuffer方法。

4.2.3 BufferReader 字符输入缓冲流

  java.io.BufferReader继承Reader,可以对基础字符输入流的读取进行字符缓冲,以提供字符,数组和行的高效读取。还支持mark和reset操作。

    //基础字符输入流
    private Reader in;
    //缓冲区
    private char cb[];
    //nChars缓冲区可读取的字符数,nextChar指向下一个可读取的字符
    private int nChars, nextChar;
    //mark、reset方法对应的参数
    private static final int INVALIDATED = -2;
    private static final int UNMARKED = -1;
    private int markedChar = UNMARKED;//mark“书签”的位置
    private int readAheadLimit = 0; /* 限制mark后读取多少字符内“书签”仍有效,必须>0才可以进行mark */
​
    /** 是否跳过换行符 */
    private boolean skipLF = false;
​
    /** mark是否跳过换行符 */
    private boolean markedSkipLF = false;
    /*缓冲区默认大小*/
    private static int defaultCharBufferSize = 8192;
 

  在其fill()方法中仍需要根据书签情况以及缓冲区大小,判断缓冲区是否需要拓展以及nextChar的位置后从基础字符流读取新数据。

private void fill() throws IOException {
        int dst;
        if (markedChar <= UNMARKED) {
            /*情况1:未调用mark */
            dst = 0;
        } else {
            /* 情况2:调用了mark */
            int delta = nextChar - markedChar;
            if (delta >= readAheadLimit) {
                /* 情况2.1:mark失效了 */
                markedChar = INVALIDATED;
                readAheadLimit = 0;
                dst = 0;
            } else { /* 情况2.2:mark仍有效 */
                if (readAheadLimit <= cb.length) {
                    /* 情况2.2.1:书签所标记位置到当前读取位置的空间缓冲区可以放下且仍有空间 */
                    System.arraycopy(cb, markedChar, cb, 0, delta);
                    markedChar = 0;
                    dst = delta;
                } else {
                    /* 情况2.2.2:空间不足,需要拓展缓冲区 */
                    char ncb[] = new char[readAheadLimit];
                    System.arraycopy(cb, markedChar, ncb, 0, delta);
                    cb = ncb;
                    markedChar = 0;
                    dst = delta;
                }
                nextChar = nChars = delta;
            }
        }
​
        int n;
        do {
            n = in.read(cb, dst, cb.length - dst);
        } while (n == 0);
        if (n > 0) {
            nChars = dst + n;
            nextChar = dst;
        }
    }

 

  同时,字符缓冲流提供了行的读取写入,在BufferReader中实现了readLine方法进行行的读入。

String readLine(boolean ignoreLF) throws IOException {
        StringBuilder s = null;
        int startChar;
​
        synchronized (lock) {
            ensureOpen();
            boolean omitLF = ignoreLF || skipLF;
​
        bufferLoop:
            for (;;) {
​
                if (nextChar >= nChars)
                    fill();
                if (nextChar >= nChars) { /* EOF */
                    if (s != null && s.length() > 0)
                        return s.toString();
                    else
                        return null;
                }
                boolean eol = false;
                char c = 0;
                int i;
​
                /* Skip a leftover '\n', if necessary */
                if (omitLF && (cb[nextChar] == '\n'))
                    nextChar++;
                skipLF = false;
                omitLF = false;
​
            charLoop:
                for (i = nextChar; i < nChars; i++) {
                    c = cb[i];
                    if ((c == '\n') || (c == '\r')) {
                        eol = true;
                        break charLoop;
                    }
                }
​
                startChar = nextChar;
                nextChar = i;
​
                if (eol) {
                    String str;
                    if (s == null) {
                        str = new String(cb, startChar, i - startChar);
                    } else {
                        s.append(cb, startChar, i - startChar);
                        str = s.toString();
                    }
                    nextChar++;
                    if (c == '\r') {
                        skipLF = true;
                    }
                    return str;
                }
​
                if (s == null)
                    s = new StringBuilder(defaultExpectedLineLength);
                s.append(cb, startChar, i - startChar);
            }
        }
    }

 

4.2.4 BufferWriter 字符输出缓冲流

  java.io.BufferWriter继承Writer,对基础字符输入流的字符写入操作提供了缓冲功能,提供单个字符,数据和字符串的高效写入。

    private char cb[];//缓冲区
   //nChars缓冲区写入的字符数,nextChar指向下一个写入缓冲区的位置
    private int nChars, nextChar;
   //缓冲区默认大小
    private static int defaultCharBufferSize = 8192;

 

调用flush方法或者colse方法以及调用write方法时发现缓冲区空间不够时,调用flushBuffer方法将缓冲区数据写入底层流。

   void flushBuffer() throws IOException {
        synchronized (lock) {
            ensureOpen();
            if (nextChar == 0)
                return;
            out.write(cb, 0, nextChar);
            nextChar = 0;
        }
    }

 

  同时,字符缓冲流提供了行的读取写入,在BufferWriter中实现了newLine方法,写入换行,使用平台自己的系统属性line.separator定义的行分隔符概念。

    public void newLine() throws IOException {
        write(lineSeparator);
    }

 

4.3 推回输入流

  通常情况下使用输入流从磁盘、网络或者其他的物理介质读取数据都是按顺序读取的,而在流的内部都会维护一个指针,在读取数据的同时,指针会向后移动,知道读取完成为止。

  而当我们需要将已经读取出来的数据推回输入流则需要引入推回输入流的概念,推回输入流继承过滤流,添加了缓冲区,在需要推回数据时则将数据推回缓冲区,若是推回的数据超出了缓冲区的大小则会报错。

  推回输入流主要分为两类:分别是PushbackInputStream和PushbackReader。

4.3.1 PushbackInputStream

  java.io.PushbackInputStream继承FilterInputStream,存在推回缓冲区buf,在构造函数中可设置缓冲区的大小,或者使用默认的缓冲区大小1,并且设置pos的初始值为缓冲区的长度,当pos与buf.length相等,说明此时没有推回的字节

    /*推回缓冲区*/
    protected byte[] buf;
    /*指向下一个可读取的字节的位置,pos与buf.length之间的数据则是被推回的数据*/
    protected int pos;

  而PushbackInputStream中比较重要的两类方法:read和unread

  read方法所做的是,当之前有推回数据,则优先先从推回数据缓冲区读取数据(重读),再从底层基础流读取数据,若是推回数据缓冲区无数据,则直接从底层基础数据流读取数据。

   public int read() throws IOException {
        ensureOpen();
        if (pos < buf.length) {//若推回数据缓冲区有数据
            return buf[pos++] & 0xff;
        }
        return super.read();//若推回数据缓冲区无数据
    }
​
​
    public int read(byte[] b, int off, int len) throws IOException {
        ensureOpen();
        if (b == null) {
            throw new NullPointerException();
        } else if (off < 0 || len < 0 || len > b.length - off) {
            throw new IndexOutOfBoundsException();
        } else if (len == 0) {
            return 0;
        }
​
        int avail = buf.length - pos;
        if (avail > 0) {//若推回数据缓冲区有数据
            if (len < avail) {
                avail = len;
            }
            System.arraycopy(buf, pos, b, off, avail);//先从缓冲区读数据
            pos += avail;
            off += avail;
            len -= avail;
        }
        if (len > 0) {//若是推回数据缓冲区无数据,或者说数据不够,则从底层基础流读数据
            len = super.read(b, off, len);
            if (len == -1) {
                return avail == 0 ? -1 : avail;
            }
            return avail + len;
        }
        return avail;
    }

 

  unread方法所做的则是将已读取数据推回缓冲区中。将指定数据推入到缓冲区pos和buf.length之间,若是缓冲区剩余空间不够,则会报错IOException。

   //1 
   public void unread(int b) throws IOException {
        ensureOpen();
        if (pos == 0) {
            throw new IOException("Push back buffer is full");
        }
        buf[--pos] = (byte)b;
    }
​
    //2
    public void unread(byte[] b, int off, int len) throws IOException {
        ensureOpen();
        if (len > pos) {
            throw new IOException("Push back buffer is full");
        }
        pos -= len;
        System.arraycopy(b, off, buf, pos, len);
    }
    //3 调用 2 unread(byte[] b, int off, int len)实现
    public void unread(byte[] b) throws IOException {
        unread(b, 0, b.length);
    }

 

4.3.2 PushbackReader

  java.io.PushbackReader继承自FilterReader,属于字符输入流,它的实现与PushbackInputStream相差无几,都是通过推回数据缓冲区,unread方法通过将数据推入缓冲区,read方法实现重读来实现的。只是作为字符流,添加了同步lock资源区。

    public int read() throws IOException {
        synchronized (lock) {
            ensureOpen();
            if (pos < buf.length)
                return buf[pos++];
            else
                return super.read();
        }
    }
​
    public void unread(int c) throws IOException {
        synchronized (lock) {
            ensureOpen();
            if (pos == 0)
                throw new IOException("Pushback buffer overflow");
            buf[--pos] = (char) c;
        }
    }

 

4.4 特殊流

  特殊流,或者说直译过来是数据输入流(DataInputStream)和数据输出流(DataOutputStream),其功能是以机器无关的方式读取写入Java的基本类型。之前我们所涉及的流分两种,一种是字节流,操作的是字节,一种是字符流,操作的是字符。而DataInputStream数据输入流允许应用程序以机器无关的方式从底层输入流中读取基本的Java类型,DataOutputStream数据输出流允许应用程序将基本Java数据类型写到基础输出流中

  主要是在特殊流中实现了从字节到基础数据类型的转换,如readInt()和writeInt(int v)。

 

  在DataInputStream的readInt()中,先从底层刘读取4个字节,后生成对应的int数据返回。

    public final int readInt() throws IOException {
        int ch1 = in.read();
        int ch2 = in.read();
        int ch3 = in.read();
        int ch4 = in.read();
        if ((ch1 | ch2 | ch3 | ch4) < 0)
            throw new EOFException();
        return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + (ch4 << 0));
    }

 

  在DataOutputStream的writeInt中将传入基础数据类型int按字节写入基础流中。

    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);
    }

 

 

 

Java IO流之FilterInputStream和FilterOutputStream分析

java.io.BufferedInputStream 源码分析

java.io.BufferedOutputStream 源码分析



这篇关于JAVA篇:Java IO (四) 过滤流、缓冲流、推回输入流、特殊流的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程