Java_io体系之BufferedInputStream、BufferedOutputStream简介、走进源码及示例——10

Java_io体系之BufferedInputStream、BufferedOutputStream简介、走进源码及示例——10


一:BufferedInputStream


1、类功能简介:


         缓冲字节输入流、作为FilterInputStream的一个子类、他所提供的功能是为传入的底层字节输入流提供缓冲功能、他会通过底层字节输入流(in)中的字节读取到自己的buffer中(内置缓存字节数组)、然后程序调用BufferedInputStream的read方法将buffer中的字节读取到程序中、当buffer中的字节被读取完之后、BufferedInputStream会从in中读取下一个数据块到buffer中供程序读取、直到in中数据被读取完毕、这样做的好处一是提高了读取的效率、二是减少了打开存储介质的连接次数、详细的原因下面BufferedOutputStream有说到。其有个关键的方法fill()就是每当buffer中数据被读取完之后从in中将数据填充到buffer中、程序从内存中读取数据的速度是从磁盘中读取的十倍!这是一个很恐怖的效率的提升、同时我们也不能无禁止的指定BufferedInputStream的buffer大小、毕竟、一次性读取in中耗时较长、二是内存价格相对昂贵、我们能做的就是尽量在其中找到合理点。一般也不用我们费这个心、创建BufferedInputStream时使用buffer的默认大小就好。


2、BufferedInputStream  API简介:


A:关键字段


	private static int defaultBufferSize = 8192;//内置缓存字节数组的大小
	
	protected volatile byte buf[];	内置缓存字节数组
	
	protected int count;	当前buf中的字节总数、注意不是底层字节输入流的源中字节总数
	
	protected int pos;		当前buf中下一个被读取的字节下标
	
	protected int markpos = -1;		最后一次调用mark(int readLimit)方法记录的buf中下一个被读取的字节的位置
	
	protected int marklimit;	调用mark后、在后续调用reset()方法失败之前云寻的从in中读取的最大数据量、用于限制被标记后buffer的最大值


B:构造方法


	BufferedInputStream (InputStream in);	使用默认buf大小、底层字节输入流构建bis
	
	BufferedInputStream (InputStream in, int size);		使用指定buf大小、底层字节输入流构建bis


C:一般方法


	int available();	返回底层流对应的源中有效可供读取的字节数	
	
	void close();	关闭此流、释放与此流有关的所有资源
	
	boolean markSupport();	查看此流是否支持mark、此方法一直返回true
	
	void mark(int readLimit);	标记当前buf中读取下一个字节的下标
	
	int read();		读取buf中下一个字节
	
	int read(byte[] b, int off, int len);		读取buf中下一个字节
	
	void reset();	重置最后一次调用mark标记的buf中的位子
	
	long skip(long n);	跳过n个字节、 不仅仅是buf中的有效字节、也包括in的源中的字节


3、源码分析:


package com.chy.io.original.code;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

/**
 * BufferedInputStream 缓冲字节输入流、作为FilterInputStream的一个实现类、为子类添加缓冲功能、自带一个缓冲数组、
 * 是为传入的InputStream的实现类(简称in)提供缓冲读取功能、他的原理是从in中一次性读取一个数据块放入自带的缓冲数组中、
 * 当这个缓冲数组中的数据被读取完时、BufferedInputStream再从in中读取一个数据块、这样循环读取。
 * 缓存数组是放在内存中的、当程序从buf中读取字节的时候、相当于从内存中读取、从内存中读取的效率至少是从磁盘等存储介质的十倍以上!
 * 这里会有人想干嘛不一次性读取所有in中的字节放入内存中?
 * 1)读取全部字节可能耗费的时间较长、
 * 2)内存有限、不想磁盘等存储介质存储空间那么大、价格也相对昂贵的多!
 * 
 */
public class BufferedInputStream extends FilterInputStream {

//	BufferedInputStream自带的数组大小
    private static int defaultBufferSize = 8192;

    /**
     *自带数组、如果在根据传入的in构造BufferedInputStream没有传入时、其大小为8192、
     *如果传入则使用传入的大小。
     */
    protected volatile byte buf[];

    /**
     * 缓存数组的原子更新器。
 	 * 该成员变量与buf数组的volatile关键字共同组成了buf数组的原子更新功能实现,
 	 * 即,在多线程中操作BufferedInputStream对象时,buf和bufUpdater都具有原子性(不同的线程访问到的数据都是相同的)
     */
    private static final 
        AtomicReferenceFieldUpdater<BufferedInputStream, byte[]> bufUpdater = 
        AtomicReferenceFieldUpdater.newUpdater
        (BufferedInputStream.class,  byte[].class, "buf");

    /**
     *当前缓冲字节数组中的有效可供读取的字节总数。
     *注意这里是缓冲区、即缓冲数组buf中的有效字节、而不是in中有效的字节
     */
    protected int count;

    /**
     * 用于标记buf中下一个被读取的字节的下标、即下一个被读取的字节是buf[pos]、pos取值范围一般是 0 到count、
     * 如果pos = count则说明这个buf中的字节被读取完、那么需要从in中读取下一个数据块用于读取。
     * 同样:这里指的是缓冲字节数组中字节的下标、而不是in中的字节的下标
     */
    protected int pos;
    
    /**
     * 当前缓冲区的标记位置、
     * mark() reset()结合使用步骤(单独使用没有意义)
     * 1)调用mark()、将标记当前buf中下一个要被读取的字节的索引 pos保存到 markpos中
     * 2)调用reset()、将markpos的值赋给pos、这样再次调用read()的时候就会从最后一次调用mark方法的位置继续读取。
     */
    protected int markpos = -1;

    /**
     * 调用mark方法后、在后续调用reset()方法失败之前所允许的最大提前读取量。
     */
    protected int marklimit;

    /**
     * 检测传入的基础流in是否关闭。若没有关闭则返回此流。
     */
    private InputStream getInIfOpen() throws IOException {
        InputStream input = in;
		if (input == null){
		    throw new IOException("Stream closed");
		} 
		return input;
    }

    /**
     *检测BufferedInputStream是否关闭、如果没有关闭则返回其自带缓存数组buf。
     */
    private byte[] getBufIfOpen() throws IOException {
        byte[] buffer = buf;
		if (buffer == null)
		    throw new IOException("Stream closed");
        return buffer;
    }

    /**
     * 使用BufferedInputStream默认的缓冲字节数组大小及传入的基础输入字节流in创建BufferedInputStream。
     */
    public BufferedInputStream(InputStream in) {
    	this(in, defaultBufferSize);
    }

    /**
     * 使用传入的缓冲字节数组大小及传入的基础输入字节流in创建BufferedInputStream。
     */
    public BufferedInputStream(InputStream in, int size) {
		super(in);
	        if (size <= 0) {
	            throw new IllegalArgumentException("Buffer size <= 0");
	        }
		buf = new byte[size];
    }

    /**
     * 从输入流in中获取字节、填充到缓冲字节数组中。这里由于时间关系没有过于纠结内部实现、有兴趣的可以自己研究下
     */
    private void fill() throws IOException {
        byte[] buffer = getBufIfOpen();
		if (markpos < 0){
			pos = 0;		/* no mark: throw away the buffer */
		}else if (pos >= buffer.length){	/* no room left in buffer */
		    if (markpos > 0) {	/* can throw away early part of the buffer */
				int sz = pos - markpos;
				System.arraycopy(buffer, markpos, buffer, 0, sz);
				pos = sz;
				markpos = 0;
		    } else if (buffer.length >= marklimit) {
				markpos = -1;	/* buffer got too big, invalidate mark */
				pos = 0;	/* drop buffer contents */
		    } else {		/* grow buffer */
		    	int nsz = pos * 2;
				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){
        	//根据从输入流中读取的实际数据的多少,来更新buffer中数据的实际大小
        	count = n + pos;
        }
    }

    /**
     * 读取下一个字节、并以整数形式返回。
     */
    public synchronized int read() throws IOException {
		if (pos >= count) {
			// 若已经读完缓冲区中的数据,则调用fill()从输入流读取下一部分数据来填充缓冲区
		    fill();
		    //读取完in最后一个字节。
		    if (pos >= count)
			return -1;
		}
		return getBufIfOpen()[pos++] & 0xff;
    }

    /**
     * 将缓存字节数组中的字节写入到从下标off开始、长度为len的byte[]b 中。
     */
    private int read1(byte[] b, int off, int len) throws IOException {
		int avail = count - pos;
		if (avail <= 0) {
		    /** 
		     * 如果传入的byte[]的长度len大于buf的size、并且没有markpos、就直接从原始流中读取len个字节放入byte[]b中
		     * 避免不必要的copy:从原始流中读取buf.length个放入buf中、再将buf中的所有字节放入byte[]b 中
		     * 再清空buf、再读取buf.length个放入buf中 、还要放入byte[]b中直到 len个。
		     */
		    if (len >= getBufIfOpen().length && markpos < 0) {
		    	return getInIfOpen().read(b, off, len);
		    }
		    //若缓冲区数据被读取完、则调用fill()填充buf。
		    fill();
		    avail = count - pos;
		    if (avail <= 0) return -1;
		}
		int cnt = (avail < len) ? avail : len;
		System.arraycopy(getBufIfOpen(), pos, b, off, cnt);
		pos += cnt;
		return cnt;
    }

    /**
     * 将buf中的字节读取到下标从off开始、len长的byte[]b 中
     */
    public synchronized int read(byte b[], int off, int len)
	throws IOException
    {
        getBufIfOpen(); // Check for closed stream
        if ((off | len | (off + len) | (b.length - (off + len))) < 0) {
		    throw new IndexOutOfBoundsException();
		} else if (len == 0) {
	            return 0;
	        }
        /**
         * 不断的读取字节、放入b中、有以下几种情况
         * 1)buf中有len个可供读取、则将len个字节copy到b中
         * 2)buf中不足len个、从源输入流中读取下一个数据块
         * 	  a)源输入流中有可供读取的有效的、读取并填充buf、继续填充到b中
         * 	  b)读到结尾、依然不够、将所有读取的填充到b中
         */
        
		int n = 0;
        for (;;) {
            int nread = read1(b, off + n, len - n);
            if (nread <= 0) 
                return (n == 0) ? nread : n;
            n += nread;
            if (n >= len)
                return n;
            // if not closed but no bytes available, return
            InputStream input = in;
            if (input != null && input.available() <= 0)
                return n;
        }
    }

    /**
     * 跳过n个字节、返回实际跳过的字节数。
     */
    public synchronized long skip(long n) throws IOException {
        getBufIfOpen(); // Check for closed stream
		if (n <= 0) {
		    return 0;
		}
		long avail = count - pos;
     
        if (avail <= 0) {
            // If no mark position set then don't keep in buffer
            if (markpos <0) 
                return getInIfOpen().skip(n);
            
            // Fill in buffer to save bytes for reset
            fill();
            avail = count - pos;
            if (avail <= 0)
                return 0;
        }
        
        long skipped = (avail < n) ? avail : n;
        pos += skipped;
        return skipped;
    }

    /**
     * 返回in中有效可供被读取的字节数。从这里也可以看出、有效字节是in中现有有效字节与buf中剩余字节的和。
     * 即buf中是从in中读取的一个供程序读取的数据块。
     */
    public synchronized int available() throws IOException {
    	return getInIfOpen().available() + (count - pos);
    }

    /** 
     * @param   readlimit   在mark方法方法失效前最大允许读取量
     */
    public synchronized void mark(int readlimit) {
		marklimit = readlimit;
		markpos = pos;
    }

    /**
     * 将最后一次调用mark标记的位置传递给pos、使得此流可以继续接着最后一次mark的地方读取。
     * 如果没有调用mark、或者mark失效则抛出IOException。
     */
    public synchronized void reset() throws IOException {
        getBufIfOpen(); // Cause exception if closed
		if (markpos < 0)
		    throw new IOException("Resetting to invalid mark");
		pos = markpos;
    }

    /**
     * 查看此流是否支持markSupport
     * @return true 此流支持mark。
     */
    public boolean markSupported() {
    	return true;
    }

    /**
     * 关闭此流并释放所有资源。
     */
    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;
            }
        }
    }
}


4、实例演示:

            当使用BufferedInputStream时、一般就是冲着他的缓冲功能去的、其他的方法在前面底层的类中有代码、使用方法、效果完全相同只是内部实现不同、这里对于BufferedInputStream、BufferedOutputStream的实例放在一起的、可见下面。

二:BufferedOutputStream 


1、类功能简介:


     BufferedOutputStream、缓存字节输出流、作为FilterInputStream的一个子类、他的功能是为传入的底层字节输出流提供缓存功能、同样当使用底层字节输出流向目的地中写入字节或者字节数组时、没写入一次就要打开一次到目的地的连接、这样频繁的访问不断效率底下、也有可能会对存储介质造成一定的破坏、比如当我们向磁盘中不断的写入字节时、夸张一点、将一个非常大单位是G的字节数据写入到磁盘的指定文件中的、没写入一个字节就要打开一次到这个磁盘的通道、这个结果无疑是恐怖的、而当我们使用BufferedOutputStream将底层字节输出流、比如FileInputStream包装一下之后、我们可以在程序中先将要写入到文件中的字节写入到BufferedOutputStream的内置缓存空间中、然后当达到一定数量时、一次性写入FileInputStream流中、此时、FileInputStream就可以打开一次通道、将这个数据块写入到文件中、这样做虽然不可能达到一次访问就将所有数据写入磁盘中的效果、但也大大提高了效率和减少了磁盘的访问量!这就是其意义所在、

    他的具体工作原理在这里简单提一下:这里可能说的比较乱、具体可以看源码、不懂再回头看看这里、当程序中每次将字节或者字节数组写入到BufferedOutputStream中时、都会检查BufferedOutputStream中的缓存字节数组buf(buf的大小是默认的或者在创建bos时指定的、一般使用默认的就好)是否存满、如果没有存满则将字节写入到buf中、如果存满、则调用底层的writer(byte[] b, int off, int len)将buf中的所有字节一次性写入到底层out中、如果写入的是字节数组、如果buf中已满则同上面满的时候的处理、如果能够存下写入的字节数组、则存入buf中、如果存不下、并且要写入buf的字节个数小于buf的长度、则将buf中所有字节写入到out中、然后将要写入的字节存放到buf中(从下标0开始存放)、如果要写入out中的字节超过buf的长度、则直接写入out中、


2、BufferedOutputStream  API简介:


A:关键字段


	protected byte[] buf;	内置缓存字节数组、用于存放程序要写入out的字节
	
	protected int count;	内置缓存字节数组中现有字节总数

B:构造方法


	BufferedOutputStream(OutputStream out);		使用默认大小、底层字节输出流构造bos
	
	BufferedOutputStream(OutputStream out, int size);	使用指定大小、底层字节输出流构造bos


C:一般方法


	//在这里提一句、BufferedOutputStream没有自己的close方法、当他调用父类FilterOutputStrem的方法关闭时、会间接调用自己实现的flush方法将buf中残存的字节flush到out中、再out.flush()到目的地中、DataOutputStream也是如此、
	
	void  flush();	将写入bos中的数据flush到out指定的目的地中、注意这里不是flush到out中、因为其内部又调用了out.flush()
	
	write(byte b);		将一个字节写入到buf中
	
	write(byte[] b, int off, int len);		将b的一部分写入buf中

3、源码分析:


package com.chy.io.original.code;

import java.io.IOException;

/**
 * 为传入的基础输出流(以下简称 out)提供缓冲功能、先将要写入out的数据写入到BufferedOutputStream缓冲数组buf中、
 * 当调用bufferedOutputStream的flush()时、或者调用flushBuffer()时一次性的将数据使用out.write(byte[]b , 0, count)写入out指定的目的地中、
 * 避免out.write(byte b)方法每写一个字节就访问一次目的地。
 * 
 */
public class BufferedOutputStream extends FilterOutputStream {
    /**
     * BufferedOutputStream内置的缓存数组、当out进行写入时、BufferedOutputStream会先将out写入的数据
     * 放到buf中、
     * 注意:不一定buf[]被写满之后才会写入out中。
     */
    protected byte buf[];

    /**
     * 缓存数组中现有的可供写入的有效字节数、
     * 注意:这里是buf中的、而不是out中的。
     */
    protected int count;
    
    /**
     * 使用BufferedOutputStream默认的缓存数组大小与基础输出流out创建BufferedOutputStream。
     */
    public BufferedOutputStream(OutputStream out) {
    	this(out, 8192);
    }

    /**
     * 使用指定的的缓存数组大小与基础输出流out创建BufferedOutputStream。
     */
    public BufferedOutputStream(OutputStream out, int size) {
	super(out);
        if (size <= 0) {
            throw new IllegalArgumentException("Buffer size <= 0");
        }
        buf = new byte[size];
    }

    /** 如果buf中有的数据、则立刻将buf中所有数据写入目的地中。 并且将count重置为0。*/
    private void flushBuffer() throws IOException {
        if (count > 0) {
		    out.write(buf, 0, count);
		    count = 0;
        }
    }

    /**
     * 将要写入out中的数据先写入到buf中。
     */
    public synchronized void write(int b) throws IOException {
		if (count >= buf.length) {
		    flushBuffer();
		}
		buf[count++] = (byte)b;
    }

    /**
     * 将从下标off开始、长度为len个字节的byte[]b写入buf中、
     */
    public synchronized void write(byte b[], int off, int len) throws IOException {
		if (len >= buf.length) {
		    /* 如果写入的字节个数超过buf的长度、
		     * 那么直接调用out.write(byte b[] , int off, int len)避免重复操作、即读到buf中但buf立马装满
		     * 此时将buf写入out中又将剩下的填充到buf中。
		     */
		    flushBuffer();
		    out.write(b, off, len);
		    return;
		}
		//如果写入的个数大于buf中现有剩余空间、则将buf中的现有count个字节写入out中、注意:flushBuffer之后、count变成了0。
		//再将len个字节写入从count下标开始长度为len的buf中。
		//同时将count 变成 count +=len.
		if (len > buf.length - count) {
		    flushBuffer();
		}
		System.arraycopy(b, off, buf, count, len);
		count += len;
    }

    /**
     * 1)将buf中现有字节写入out中
     * 2)将out中的现有字节flush到目的地中。
     */
    public synchronized void flush() throws IOException {
        flushBuffer();
        out.flush();
    }
}


4、实例演示:


package com.chy.io.original.test;

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

public class BufferedStreamTest {
	
	private static final byte[] byteArray = {
        0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F,
        0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A
    };
	public static void testBufferedOutputStream() throws IOException{
		BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(new File("D:\\bos.txt")));
		bos.write(byteArray[0]);
		bos.write(byteArray, 1, byteArray.length-1);
		//bos.write(byteArray); 注意:这个方法不是BufferedOutputStream的方法、此方法是调用FilterOutputStream的write(byte[] b)
		//write(byte[] b)调用本身的write(byte[]b , 0, b.length)方法、而此方法的本质是循环调用传入的out的out.write(byte b)方法.
		bos.flush();
	}
	
	public static void testBufferedInpuStream() throws IOException{
		BufferedInputStream bis = new BufferedInputStream(new FileInputStream(new File("D:\\bos.txt")));
		for(int i=0; i<10; i++){
			if(bis.available() >=0){
				System.out.println(byteToString((byte)bis.read()));
			}
		}
		if(!bis.markSupported()){
			return;
		}
		
		bis.mark(6666);//标记"当前索引位子" 即第11个字节 k
		
		bis.skip(10);//丢弃10个字节。
		
		//读取剩下的b.length个字节
		byte[] b = new byte[1024];
		int n1 = bis.read(b, 0, b.length);
		System.out.println("剩余有效字节 : " +n1);
		printByteValue(b);
		
		bis.reset();//重置输入流最后一次调用mark()标记的索引位置
		int n2 = bis.read(b, 0, b.length);
		System.out.println("剩余有效字节 : " +n2);
		printByteValue(b);
	}
	private static void printByteValue(byte[] buf) {
		for(byte b : buf){
			if(b != 0){
				System.out.print(byteToString(b) + " ");
			}
		}
	}
	
	private static String byteToString(byte b){
		byte[] bAray = {b};
		return new String (bAray);
	}
	
	public static void main(String[] args) throws IOException {
		testBufferedOutputStream();
		testBufferedInpuStream();
	}
}
           
             对实例的一些补充:这里没有体现出使用这两个类的上面层吹的天花的优点、仅仅是简单的方法、留个坑、有时间弄个实例测试一下读取的效率、、一般这两个类最长用的是包装文件流(当然也可以是其他的流)、有兴趣的可以自己拿被包装的FileInputStream、FileOutputStream、和裸的这两个流、拷贝一个大文件看看差距。

总结:


            对于BufferedInputStream、BufferedOutputStream、本质就是为底层字节输入输出流添加缓冲功能、先将底层流中的要读取或者要写入的数据先以一次读取一组的形式来讲数据读取或者写入到buffer中、再对buffer进行操作、这样不但效率、还能节省资源。最后、在程序中、出于效率的考虑、也应为低级流使用这两个类进行装饰一下、而不是直接拿着流直接上、觉得能实现就行。这两个流说的有些粗糙、有时间回头修理、不足之处请见谅、、、、


更多IO内容:java_io 体系之目录


你可能感兴趣的:(java,源码,Inputstream,OutputStream,IO流)