Java IO笔记(BufferedInputStream/BufferedOutputStream)


(最近刚来到简书平台,以前在CSDN上写的一些东西,也在逐渐的移到这儿来,有些篇幅是很早的时候写下的,因此可能会看到一些内容杂乱的文章,对此深感抱歉,以下为正文)


正文

本篇讲述的是Java IO包中的BufferedInputStream类和BufferedOutputStream类。

下面我们通过源码分别对这两个类进行学习。

1.BufferedInputStream:

package java.io;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

public class BufferedInputStream extends FilterInputStream {
  // 声明了一个int型常量,用于后面创建缓存时初始化赋值,默认缓存为8k。
   private static int DEFAULT_BUFFER_SIZE = 8192;
   // 声明了一个int型常量,表明了创建的缓存的最大容量。之所以减8是因为有一些JVM会在缓存数组头部存储一些信息。
   private static int MAX_BUFFER_SIZE = Integer.MAX_VALUE - 8;
   // 声明了一个byte型数组,用于作为缓存区,该数组容量会根据实际需要进行动态改变。并且使用了volatile关键字修饰,保证了数据改变的可见性。
   protected volatile byte buf[];

   /**
  * 从名字上可以看出这是一个原子性的更新器,它用于保证对缓存buf进行原子更新。因为流的关闭可能是异步的,所以以缓存区中buf是否有效作为流是否关闭的唯一
  * 标志。
  */
   private static final
       AtomicReferenceFieldUpdater<BufferedInputStream, byte[]> bufUpdater =
       AtomicReferenceFieldUpdater.newUpdater
       (BufferedInputStream.class,  byte[].class, "buf");

   // 声明了一个int型数值count,该值表示了当前buf中的有效存储字节数。
   protected int count;
   // 声明了一个int型数值pos,该值表示了当前数据读取的位置。
   protected int pos;
   // 声明了一个int型数值markpos,该值表示了在流中标记的位置,可通过reset方法返回至标记处,该值并不是无限延长的,它受保留区间的限制,如果超过,则该值可
  // 能被还原成-1。
   protected int markpos = -1;
   // 声明了一个int型数值marklimit,该值就是上述约束markpso的区间的最大值,如果pos-markpos>marklimit的话,markpos可能会被还原成-1。
   protected int marklimit;

   /**
  * 该方法用于获得传入的InputStream对象in,并对其进行检测,如果不为null则返回该对象,否则抛出IOException。
  */
   private InputStream getInIfOpen() throws IOException {
       InputStream input = in;
       if (input == null)
           throw new IOException("Stream closed");
       return input;
   }

   /**
  * 该方法用于获得内部缓存数组buf,并对其进行检测,如果不为null,则返回该缓存数组buf。
  */
   private byte[] getBufIfOpen() throws IOException {
       byte[] buffer = buf;
       if (buffer == null)
           throw new IOException("Stream closed");
       return buffer;
   }

   /**
  * 一个带一个参数的构造函数,传入的参数类型为一个InputStream对象,内部实质是继续调用另外一个带两个参数的构造函数,默认缓存容量为8k。
  */
   public BufferedInputStream(InputStream in) {
       this(in, DEFAULT_BUFFER_SIZE);
   }

   /**
  * 一个带两个参数的构造函数,第一个参数为一个InputStream对象,用于调用父类FilterInputStream类的构造方法,第二个参数为一个int型数值size,用于初始化缓存
  * 区数组的容量。
  */
   public BufferedInputStream(InputStream in, int size) {
       super(in);
       if (size <= 0) {
           throw new IllegalArgumentException("Buffer size <= 0");
       }
       buf = new byte[size];
   }

    /**
   * 该方法是BufferedInputStream类的核心方法,该方法用于向缓存区中填充内容,BufferedInputStream的核心就是通过缓存区的存在减少与磁盘之间直接IO的读写。
   */
   private void fill() throws IOException {
       //获得内置的数组缓存,之所以使用getBufIfOpen还有检测流是否关闭的作用。
       byte[] buffer = getBufIfOpen();
   //当没有标记的时候,因为不用考虑到数据保留,所以可以大胆的将pos重置为0,这样之后数据重新填充后,便会从头开始从缓存区中读取数据。
       if (markpos < 0)
           pos = 0;    
   //pos>=buffer.length表示读取位置超过了缓存区位置,此时需要向缓存区中重新填充数据,并将读取位置重置。       
       else if (pos >= buffer.length)  
           //makpos>0,表示流中存在着标记,此时向缓存区中填充数据时,要将标记处往后的数据都复制到缓存区的头部,然后再重新填充数据。
           if (markpos > 0) { 
       //获得当前位置到标记处的长度,通过System.arraycopy方法,将原缓存区中从标记处到当前位置的数据复制到新缓存的头部,将读取位置至于保存数据的
       //尾部,标记处置为0。
               int sz = pos - markpos;
               System.arraycopy(buffer, markpos, buffer, 0, sz);
               pos = sz;
               markpos = 0;
       //buffer.length >= marklimit,即当缓存区的容量已经超过marklimit的限制时,便会将标记丢弃,直接将标记markpos重置为-1,直接将pos置为0,丢弃掉以
       //前缓存区中的内容。前面说过,标记是不能无能延伸的,原因就在这里,超过限制后便会丢弃标记。
           } else if (buffer.length >= marklimit) {
               markpos = -1;   
               pos = 0; 
       //如果缓存区的容量超过了最大限制(MAX_BUFFER_SIZE),那么会抛出对应异常OutOfMemoryError。       
           } else if (buffer.length >= MAX_BUFFER_SIZE) {
               throw new OutOfMemoryError("Required array size too large");
           } else {   
           //如果缓存区容量可以扩展,此时会自动扩容,一般情况下缓存区容量是自动扩展一倍空间,如果当前容量扩展为2倍会超过最大容量限制,那么此时直接将
       //缓存区容量扩展至最大容量(MAX_BUFFER_SIZE)。
               int nsz = (pos <= MAX_BUFFER_SIZE - pos) ?
                       pos * 2 : MAX_BUFFER_SIZE;
       //如果新的缓存区容量大于了标记的限制大小,那么只需将新缓存区容量扩展到标记限制大小即可,扩展多了没有必要,因为在上一步过程中标记会被清空。
               if (nsz > marklimit)
                   nsz = marklimit;
       //根据得到的缓存区容量创建新的byte型缓存数组,将原缓存去中的内容复制到新的缓存区数组中,并将其赋值给内置的缓存数组。
               byte nbuf[] = new byte[nsz];
               System.arraycopy(buffer, 0, nbuf, 0, pos);
               if (!bufUpdater.compareAndSet(this, buffer, nbuf)) {
                   throw new IOException("Stream closed");
               }
               buffer = nbuf;
           }
   //将当前的读取位置赋值给count,从原始数据流中读取新的数据填充到缓存区中,将读取的字节数累加到count上,表明了当前缓存区中的有效字节数。
       count = pos;
       int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
       if (n > 0)
           count = n + pos;
   }

   /**
    * 定义了一个read方法,用于从内置的数组缓存区中直接读取数据,一次读取一个字节。 
    */
   public synchronized int read() throws IOException {
   //如果读取的位置超过了内置缓存区中可用数据的范围,那么调用fill方法,对内置缓存区中的数据进行更新。
       if (pos >= count) {
           fill();
       //缓存区数据更新后如果读取位置还是超过了内置缓存中可用数据范围,那么表示已经读取到了文件末尾,那么此时返回-1。
           if (pos >= count)
               return -1;
       }
   //正常情况缓存区中还有有效数据读取时,直接从缓存区中读取对应的数据。 
       return getBufIfOpen()[pos++] & 0xff;
   }

   /**
    * 定义了一个私有的read1方法,可以一次读取多个字节的数据。
    */
   private int read1(byte[] b, int off, int len) throws IOException {
   //定义了一个int型变量,用于表示数据缓存区的有效长度与读取长度之间的关系。
       int avail = count - pos;
   //avail<=0,表示内置的数组缓存区中的有效数据已经用完。
       if (avail <= 0) {
       //如果此时读取的长度大于内置数组缓存区中的长度,并且没有标记mark,那么这里讲直接从原始数据流中读取对应长度的数据,避免了无谓的操作,从原始数
       //据流中读取数据填充到内部缓存区中,然后再从内部缓存区中读取数据。
           if (len >= getBufIfOpen().length && markpos < 0) {
               return getInIfOpen().read(b, off, len);
           }
       //填充内部数据缓存区,然后重新计算avail,如果仍然小于0,则表示读到了文件的末尾,此时返回-1。
           fill();
           avail = count - pos;
           if (avail <= 0) return -1;
       }
   //将数据缓存区中的数据赋值给传入的byte数组中,pos累加上实际读取的字节数,最终返回实际读取到的数据大小。
       int cnt = (avail < len) ? avail : len;
       System.arraycopy(getBufIfOpen(), pos, b, off, cnt);
       pos += cnt;
       return cnt;
   }

   /** 
    * 定义了一个一个带参的read方法,该方法实质是调用上面的read1方法,并且通过一个循环不停读取,除非读取到足够多的数据或者读取到文件的末尾时才会返回数据。
    */
   public synchronized int read(byte b[], int off, int len)
       throws IOException
   {
   //此处调用geiBufIfOpen方法只是为了检测流是否关闭。
       getBufIfOpen();
   //对传入的参数进行安全监测。
       if ((off | len | (off + len) | (b.length - (off + len))) < 0) {
           throw new IndexOutOfBoundsException();
       } else if (len == 0) {
           return 0;
       }

       int n = 0;
       for (;;) {
       //通过一个循环,不停调用read1方法,进行数据读取。
           int nread = read1(b, off + n, len - n);
           if (nread <= 0)
       //如果nread<=0,表明已经读取到数据末尾,如果n==0,表示未读取到数据,否则返回n,表示读取到的字节数。
               return (n == 0) ? nread : n;
       //每次成功读取,将新读取到的有效字节数累加到变量n上。
           n += nread;
       //如果读取的数据达到了len的长度,那么便返回一次。
           if (n >= len)
               return n;
           //如果当前的流没有关闭,且流中已经没有数据了,那么直接返回已经读取到的有效字节数。
           InputStream input = in;
           if (input != null && input.available() <= 0)
               return n;
       }
   }

   /** 
    * 定义了一个skip方法,用于跳过指定的字节数,最终返回实际跳过的字节数。 
    */
   public synchronized long skip(long n) throws IOException {
   //此处调用getBufIfOpen是为了检测流是否关闭。
       getBufIfOpen();
   //如果n<=0,那么直接返回0,表示没有跳过任何的字节数据。
       if (n <= 0) {
           return 0;
       }
   //定义了一个long型的变量avail,用于接收内置缓存数组中的有效数据与当前读取位置的关系。
       long avail = count - pos;
   //avail<=0,表示内部缓存区中的有效数据已经使用完。
       if (avail <= 0) {
       //如果没有使用过标记,那么直接使用原始数据流中InputStream中的skip功能,跳过指定数据,避免了填充缓存再从缓存中跳过的步骤。
           if (markpos <0)
               return getInIfOpen().skip(n);
       //调用fill方法,向内置数据缓存区中填充数据。
           fill();
       //再次检测avail的大小如果仍然小于0,表示读取到文件末尾,返回0。
           avail = count - pos;
           if (avail <= 0)
               return 0;
       }
   //定义了一个long型数据skipped,用于表示实际跳过的字节数,判断avail与n的关系,为skipped赋值对应的数值。
       long skipped = (avail < n) ? avail : n;
   //为pos累加上跳过的字节数,好让下一次读取从跳过的字节数之后开始读取,最终返回实际跳过的字节数。
       pos += skipped;
       return skipped;
   }

  /** 
   * 定义了一个available方法,返回当前可以用的字节数。
   */
   public synchronized int available() throws IOException {
   //定义了一个int型变量n,用来表示内置数据缓存中可以使用的字节数。
       int n = count - pos;
   //定义了一个int型变量avail,用来表示原始数据流中可以使用的字节数。
       int avail = getInIfOpen().available();
   //判断内置缓存区中可以使用的字节数是否超过了最大容量减去原始数据流中的有效字节数,如果超过就返回容量的最大值,否则返回n和avail的和。
       return n > (Integer.MAX_VALUE - avail)
                   ? Integer.MAX_VALUE
                   : n + avail;
   }

   /**
    * 定义了一个带参的mark方法,传入的参数为一个int型数据,用于修改标记的限制大小maklimit,并且将当前读取位置当做标记位置。
    */
   public synchronized void mark(int readlimit) {
       marklimit = readlimit;
       markpos = pos;
   }

   /** 
    * 定义了一个reset方法,通过与标记一起使用,实现了返回标记处重新读取数据的功能。
    */
   public synchronized void reset() throws IOException {
   //此处调用getBufIfOpen方法是为了检测当前流是否关闭
       getBufIfOpen(); 
   //如果markpos小于0,表示此时的标记位置不合法。
       if (markpos < 0)
           throw new IOException("Resetting to invalid mark");
   //将读取位置还原到标记位置。
       pos = markpos;
   }

   /** 
    * 定义了一个markSupported方法,返回值表明了当前流是否支持标记功能,此处返回true,表示BufferedInputStream支持流标记功能。
    */
   public boolean markSupported() {
       return true;
   }

   /** 
    * 定义了一个close方法,用于关闭流。 
    */
   public void close() throws IOException {
       byte[] buffer;
       while ( (buffer = buf) != null) {
       //通过原子更新器将内置的数据缓存指向null。
           if (bufUpdater.compareAndSet(this, buffer, null)) {
           //将传入的InputSteram置为null。
               InputStream input = in;
               in = null;
       //如果此时input还不为null,则调用其close方法。
               if (input != null)
                   input.close();
               return;
           }
       }
   }
}

BufferedOutputStream:

package java.io;
 
public class BufferedOutputStream extends FilterOutputStream {
    //内置了一个byte型数组,用于作为数据缓存区使用。
    protected byte buf[];
 
    //定义了一个int型变量count,用于表示内置缓存区中已经占有的数据多少。
    protected int count;
 
    /**
     * 一个带一个参数的构造方法,传入的参数是一个OutputStream对象,内部本质是调用之后另一个构造方法,并将内置的缓存区容量初始化容量定为8k。
     */
    public BufferedOutputStream(OutputStream out) {
        this(out, 8192);
    }
 
    /**
     * 定义了一个带两个参数的构造方法,第一个参数是OutputSteram对象,将传入的参数赋值给内置的OutputStream对象(父类中的成员out),第二个参数是定义了内置
     * 缓存空间容量的大小。
     */
    public BufferedOutputStream(OutputStream out, int size) {
        super(out);
        if (size <= 0) {
            throw new IllegalArgumentException("Buffer size <= 0");
        }
        buf = new byte[size];
    }
 
    /** 
     * 该方法用于将缓存中的的数据都写入至目的处(),然后清空内置缓存区中的数据。
     */
    private void flushBuffer() throws IOException {
        if (count > 0) {
            out.write(buf, 0, count);
            count = 0;
        }
    }
 
    /**
     * 定义了一个write方法,每次写入一个字节。
     */
    public synchronized void write(int b) throws IOException {
    //如果内置缓存的有效数据已经>=内置缓存的容量,那么调用flushBuffer。
        if (count >= buf.length) {
            flushBuffer();
        }
    //一般情况将数据写入到内置的缓存中去
        buf[count++] = (byte)b;
    }
 
    /**
     * 定义了一个write方法,一次写入多个字节数据。
     */
    public synchronized void write(byte b[], int off, int len) throws IOException {
    //若果需要写入的长度大于内置缓存区中的容量,那么首先调用flushBuffer方法将缓存区中的数据写入,然后直接使用原始数据流将数据写入至目的地,避免了先写
    //入内置缓存区中再从缓存区中写入目的地的步骤。
        if (len >= buf.length) {
            flushBuffer();
            out.write(b, off, len);
            return;
        }
    //如果读取的长度超过了内置缓存区中剩余的空闲容量,那么需要先调用flushBuffer方法,向目的地写入数据,清空内置的缓存区。
        if (len > buf.length - count) {
            flushBuffer();
        }
    //将内置缓存区中指定的数据复制到传入的byte数组中区,然后为count累加上写入的长度len。
        System.arraycopy(b, off, buf, count, len);
        count += len;
    }
 
    /**
     * 定义了一个flush方法,内部调用了flushBuffer方法和OuputStream的flush方法。
     */
    public synchronized void flush() throws IOException {
        flushBuffer();
        out.flush();
    }
}

上面部分为对源码的简单分析,下面用一个简单的小例子展示它们的使用方法。

package BufferedIOTest;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

public class BufferedIOtest {
   public static void main(String[] args) throws FileNotFoundException,
           IOException {
       byte[] buffer = new byte[1024];
       try (BufferedInputStream bis = new BufferedInputStream(
               new FileInputStream(new File("./src/file/test.txt")));
               BufferedOutputStream bos = new BufferedOutputStream(
                       new FileOutputStream(new File(
                               "./src/file/testcopy1.txt")))) {
           int len;
           while ( (len = bis.read(buffer)) != -1) {
               bos.write(buffer,0,len);
           }
           System.out.println("copying file has been finished..");
       }
   }
}

执行上述代码可以在将制定路径的文件复制到指定的路径处。最终总结一下这两个流分别是FilterInputStream和FilterOutputStream类的子类,在其中添加了内置的缓存区,从而提升读写的效率。
以上为本篇的全部内容。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,423评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,147评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,019评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,443评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,535评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,798评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,941评论 3 407
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,704评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,152评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,494评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,629评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,295评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,901评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,742评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,978评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,333评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,499评论 2 348

推荐阅读更多精彩内容