黑马程序员技术交流社区

标题: 关于自定义BufferedInputStream的问题 [打印本页]

作者: cyh8807    时间: 2013-1-25 12:33
标题: 关于自定义BufferedInputStream的问题
本帖最后由 张向辉 于 2013-1-26 11:04 编辑
  1. import java.io.*;
  2. public class CopyMp3Test
  3. {
  4.         public static void main(String[] args)
  5.         {
  6.                 long start = System.currentTimeMillis();
  7.                 copy_2();
  8.                 long end = System.currentTimeMillis();
  9.                 System.out.println((end-start) + "毫秒");
  10.         }
  11.         public static void copy_2()
  12.         {
  13.                 MyBufferedInputStream bis = null;
  14.                 BufferedOutputStream bos = null;
  15.                 try
  16.                 {
  17.                         bis = new MyBufferedInputStream(new FileInputStream("1.mp3"));
  18.                         bos = new BufferedOutputStream(new FileOutputStream("3.mp3"));
  19.                         
  20.                         int by = 0;
  21.                         while((by = bis.myRead()) != -1)
  22.                         {
  23.                                 bos.write(by);
  24.                         }
  25.                 }catch(IOException e)
  26.                 {
  27.                         throw new RuntimeException("读取文件失败");
  28.                 }
  29.                 finally
  30.                 {
  31.                         try
  32.                         {
  33.                                 if(bis != null)
  34.                                         bis.myClose();
  35.                         }catch(IOException e)
  36.                         {
  37.                                 throw new RuntimeException("读取关闭失败");
  38.                         }
  39.                         try
  40.                         {
  41.                                 if(bos != null)
  42.                                         bos.close();
  43.                         }catch(IOException e)
  44.                         {
  45.                                 throw new RuntimeException("写入关闭失败");
  46.                         }
  47.                 }
  48.         }
  49. }
  50. class MyBufferedInputStream
  51. {
  52.         private InputStream in;
  53.         private byte[] buf = new byte[1024];
  54.         private int pos = 0;
  55.         private int count = 0;
  56.         
  57.         MyBufferedInputStream(InputStream in)
  58.         {
  59.                 this.in = in;
  60.         }
  61.         public int myRead()throws IOException
  62.         {
  63.                 if(count == 0)
  64.                 {        
  65.                         count = in.read(buf);
  66.                         byte b = buf[pos];
  67.                         count --;
  68.                         pos ++;
  69.                         return b&255;
  70.                 }
  71.                 if(count > 0)
  72.                 {
  73.                         byte b = buf[pos];
  74.                         count --;
  75.                         pos ++;
  76.                         return b&255;
  77.                 }
  78.                 return -1;
  79.         }
  80.         public void myClose() throws IOException
  81.         {
  82.                 in.close();
  83.         }
  84. }
复制代码
上面的代码在执行期间会抛出 ArrayIndeOutOfBoundsExceptions异常,请各位给我分析为什么?
另外烦请各位给详细解释下面这段代码是如何一步一步单独在内存中执行的。
  1. public int myRead()throws IOException
  2.         {
  3.                 if(count == 0)
  4.                 {        
  5.                         count = in.read(buf);
  6.                         byte b = buf[pos];
  7.                         count --;
  8.                         pos ++;
  9.                         return b&255;
  10.                 }
  11.                 if(count > 0)
  12.                 {
  13.                         byte b = buf[pos];
  14.                         count --;
  15.                         pos ++;
  16.                         return b&255;
  17.                 }
  18.                 return -1;
  19.         }
复制代码

作者: 惠晖    时间: 2013-1-25 13:19
你没有判断count <0的情况  所以当然数组脚本越界了
作者: 杨世平    时间: 2013-1-25 13:43
  1. class MyBufferedInputStream

  2. {

  3.         private InputStream in;
  4.         //每次按1024byte的读取速度进行读取
  5.         private byte[] buf = new byte[1024];
  6.         //数组下标
  7.         private int pos = 0;
  8.         //读取了多少字节
  9.         private int count = 0;


  10.         MyBufferedInputStream(InputStream in)

  11.         {

  12.                 this.in = in;

  13.         }

  14.         public int myRead()throws IOException{
  15.                
  16.                 if(count == 0){//每次当count=0的时候说明都是重新开始读取
  17.                         count = in.read(buf);
  18.                         //当count<0的时候说明该文件剩下的不足1024byte,就是所谓的最后一次读取
  19.                         if(count<0){
  20.                                 return -1;
  21.                         }
  22.                         //当你每次是按1024byte读取,再次进来时,数组的值已经变了.pos不设为0,当然会出去
  23.                         //数组溢出的状况,所有这个是一定要加的
  24.                         pos=0;
  25.                         byte b = buf[pos];
  26.                         count --;
  27.                         pos ++;
  28.                         return b&255;
  29.                 }else{
  30.                         byte b = buf[pos];
  31.                         count --;
  32.                         pos ++;
  33.                         return b&255;
  34.                 }
  35.         }

  36.         public void myClose() throws IOException

  37.         {

  38.                 in.close();

  39.         }

  40. }
复制代码
内存怎么读的,按照代码顺序咯.
作者: ︶ㄣ布丁    时间: 2013-1-25 14:21
BufferedInputStream是一个带有缓冲区域的InputStream,它的继承体系如下:

InputStream
|__FilterInputStream
        |__BufferedInputStream

首先了解一下FilterInputStream:
FilterInputStream通过装饰器模式将InputStream封装至内部的一个成员变量:
Java代码  
protected volatile InputStream in;  

需要注意的是该成员变量使用了volatile关键字进行修饰,这意味着该成员变量的引用的内存可见性为多线程即时可见的。
其它地方FilterInputStream将所有的操作委托给了in这个成员进行操作。

了解了这些过后,来仔细看看BufferedInputStream的成员变量:
Java代码  
private static int defaultBufferSize = 8192 //该变量定义了默认的缓冲大小  
  
protected volatile byte buf[]; //缓冲数组,注意该成员变量同样使用了volatile关键字进行修饰,作用为在多线程环境中,当对该变量引用进行修改时保证了内存的可见性。  
  
private static final AtomicReferenceFieldUpdater<BufferedInputStream, byte[]> bufUpdater = AtomicReferenceFieldUpdater.newUpdater(BufferedInputStream.class,  byte[].class, "buf")//缓存数组的原子更新器,该成员变量与buf数组的volatile关键字共同组成了buf数组的原子更新功能实现。  
  
protected int count;//该成员变量表示目前缓冲区域中有多少有效的字节。  
  
protected int pos;//该成员变量表示了当前缓冲区的读取位置。  
  
protected int markpos = -1;/*表示标记位置,该标记位置的作用为:实现流的标记特性,即流的某个位置可以被设置为标记,允许通过设置reset(),将流的读取位置进行重置到该标记位置,但是InputStream注释上明确表示,该流不会无限的保证标记长度可以无限延长,即markpos=15,pos=139734,该保留区间可能已经超过了保留的极限(如下)*/  
  
protected int marklimit;/*该成员变量表示了上面提到的标记最大保留区间大小,当pos-markpos> marklimit时,mark标记可能会被清除(根据实现确定)。*/  



通过构造函数可以看到:初始化了一个byte数组作为缓冲区域
Java代码  
public BufferedInputStream(InputStream in, int size) {  
    super(in);  
        if (size <= 0) {  
            throw new IllegalArgumentException("Buffer size <= 0");  
        }  
    buf = new byte[size];  
}  



这个类中最为重要的方法是fill()方法,它提供了缓冲区域的读取、写入、区域元素的移动更新等。下面着重分析一下该方法:
Java代码  
private void fill() throws IOException {  
        byte[] buffer = getBufIfOpen();  
    if (markpos < 0) {  
          /*如果不存在标记位置(即没有需要进行reset的位置需求)
            则可以进行大胆地直接重置pos标识下一可读取位置,但是这样
            不是会读取到以前的旧数据吗?不用担心,在后面的代码里☆会实现输入流的新  
            数据填充*/  
        pos = 0;         
    }else if (pos >= buffer.length){  
       /* 位置大于缓冲区长度,这里表示已经没有可用空间了 */  
        if (markpos > 0) {     
             /* 表示存在mark位置,则要对mark位置到pos位置的数据予以保留,
                以确保后面如果调用reset()重新从mark位置读取会取得成功*/  
        int sz = pos - markpos;  
                /*该实现是通过将缓冲区域中markpos至pos部分的移至缓冲区头部实现*/  
        System.arraycopy(buffer, markpos, buffer, 0, sz);  
        pos = sz;  
        markpos = 0;  
        } else if (buffer.length >= marklimit) {  
                /* 如果缓冲区已经足够大,可以容纳marklimit,则直接重置*/  
                markpos = -1;     
        pos = 0;/* 丢弃所有的缓冲区内容 */  
        } else {         
                /* 如果缓冲区还能增长的空间,则进行缓冲区扩容*/  
        int nsz = pos * 2;  
                /*新的缓冲区大小设置成满足最大标记极限即可*/  
        if (nsz > marklimit)  
            nsz = marklimit;  
        byte nbuf[] = new byte[nsz];  
                //将原来的较小的缓冲内容COPY至增容的新缓冲区中  
        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;  
    }  

整个fill的过程,可以看作是BufferedInputStream对外提供滑动读取的功能实现,通过预先读入一整段原始输入流数据至缓冲区中,而外界对BufferedInputStream的读取操作实际上是在缓冲区上进行,如果读取的数据超过了缓冲区的范围,那么BufferedInputStream负责重新从原始输入流中载入下一截数据填充缓冲区,然后外界继续通过缓冲区进行数据读取。这样的设计的好处是:避免了大量的磁盘IO,因为原始的InputStream类实现的read是即时读取的,即每一次读取都会是一次磁盘IO操作(哪怕只读取了1个字节的数据),可想而知,如果数据量巨大,这样的磁盘消耗非常可怕。而通过缓冲区的实现,读取可以读取缓冲区中的内容,当读取超过缓冲区的内容后再进行一次磁盘IO,载入一段数据填充缓冲,那么下一次读取一般情况下就直接可以从缓冲区读取,减少了磁盘IO。减少的磁盘IO大致可以通过以下方式计算(限read()方式):

length  流的最终大小
bufSize 缓冲区大小

则通过缓冲区实现的输入流BufferedInputStream的磁盘IO数为原始InputStream磁盘IO的
1/(length/bufSize)

read方法解析:该方法返回当前位置的后一位置byte值(int表示).
Java代码  
public synchronized int read() throws IOException {  
    if (pos >= count) {  
           /*表示读取位置已经超过了缓冲区可用范围,则对缓冲区进行重新填充*/  
        fill();  
           /*当填充后再次读取时发现没有数据可读,证明读到了流末尾*/  
        if (pos >= count)  
        return -1;  
    }  
        /*这里表示读取位置尚未超过缓冲区有效范围,直接返回缓冲区内容*/  
    return getBufIfOpen()[pos++] & 0xff;  
}  


一次读取多个字节(尽量读,非贪婪)
Java代码  
private int read1(byte[] b, int off, int len) throws IOException {  
    int avail = count - pos;  
    if (avail <= 0) {  
        /*这里使用了一个巧妙的机制,如果读取的长度大于缓冲区的长度
              并且没有markpos,则直接从原始输入流中进行读取,从而避免无谓的
              COPY(从原始输入流至缓冲区,读取缓冲区全部数据,清空缓冲区,
              重新填入原始输入流数据)*/  
        if (len >= getBufIfOpen().length && markpos < 0) {  
        return getInIfOpen().read(b, off, len);  
        }  
            /*当无数据可读时,从原始流中载入数据到缓冲区中*/  
        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;  
    }  


以下方法和上面的方法类似,唯一不同的是,上面的方法是尽量读,读到多少是多少,而下面的方法是贪婪的读,没有读到足够多的数据(len)就不会返回,除非读到了流的末尾。该方法通过不断循环地调用上面read1方法实现贪婪读取。
Java代码  
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;  
        }  
  
    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;  
        }  
    }  
作者: 黑马张英涛    时间: 2013-1-25 14:48
本帖最后由 黑马张英涛 于 2013-1-25 14:52 编辑

public int myRead()throws IOException
        {

/*  你的代码里count==0的时候有两种情况,一种是刚开始读的时候,还有一种是count-- 到0的时候。
     当count计数到0时, 开始下一次read(buf),这时候由于pos是累加的,没有清零,当执行到
      byte b = buf[pos];时,由于这时候pos=1024,所以数组越界了。
*/
                if(count == 0)
                {        
                        count = in.read(buf);
                        byte b = buf[pos];
                        count --;
                        pos ++;
                        return b&255;
                }
                if(count > 0)
                {
                        byte b = buf[pos];
                        count --;
                        pos ++;
                        return b&255;
                }
                return -1;
        }
当然你可以加入判断。但是我觉得你的思路不太对,你明明在自己的方法里加入了buf缓存,写出的时候却又一个一个字符的写,
显然是舍近求远的做法。或许我的看法不对,不过我修改了一下你的方法(修改的地方都加粗了),希望能对你有所帮助:

public class TheTest {

        public static void main(String[] args) {
                long start = System.currentTimeMillis();
                copy_2();
                long end = System.currentTimeMillis();
                System.out.println((end - start) + "毫秒");
        }

        public static void copy_2() {
                MyBufferedInputStream bis = null;
                BufferedOutputStream bos = null;
                try {
                        bis = new MyBufferedInputStream(new FileInputStream("1.mp3"));
                        bos = new BufferedOutputStream(new FileOutputStream("2.mp3"));

                        byte[] by = null;
                        while ((by = bis.myRead()) != null) {
                                bos.write(by,0,by.length);
                        }

                } catch (IOException e) {
                        throw new RuntimeException("读取文件失败");
                } finally {
                        try {
                                if (bis != null)
                                        bis.myClose();
                        } catch (IOException e) {
                                throw new RuntimeException("读取关闭失败");
                        }
                        try {
                                if (bos != null)
                                        bos.close();
                        } catch (IOException e) {
                                throw new RuntimeException("写入关闭失败");
                        }
                }
        }
}

class MyBufferedInputStream {
        private InputStream in;
        private byte[] buf = new byte[1024];
//        private int pos = 0;
        private int count = 0;

        MyBufferedInputStream(InputStream in) {
                this.in = in;
        }

        public byte[] myRead() throws IOException {
                count = in.read(buf);        
                if (count >0)                 //只要能读数据
                /* 将buf中的有效数据变成另外一个数组返回,
                    由于存在最后一次读取buf装不满的情况,为了防止将上次剩下的数据也写出去
                    所以这里把buf中的0~count个元素重新封装成一个数组
                */
                        return Arrays.copyOf(buf, count);
                return null;
        }

        public void myClose() throws IOException {
                in.close();
        }
}




作者: 梁俊    时间: 2013-1-25 15:14
本帖最后由 梁俊 于 2013-1-25 15:18 编辑
  1. import java.io.*;
  2. public class CopyMp3Test
  3. {
  4.         public static void main(String[] args)
  5.         {
  6.                 long start = System.currentTimeMillis();
  7.                 copy_2();
  8.                 long end = System.currentTimeMillis();
  9.                 System.out.println((end-start) + "毫秒");
  10.         }
  11.         public static void copy_2()
  12.         {
  13.                 MyBufferedInputStream bis = null;
  14.                 BufferedOutputStream bos = null;
  15.                 try
  16.                 {
  17.                         bis = new MyBufferedInputStream(new FileInputStream("1.mp3"));
  18.                         bos = new BufferedOutputStream(new FileOutputStream("3.mp3"));
  19.                         
  20.                         int by = 0;
  21.                         while((by = bis.myRead()) != -1)
  22.                         {
  23.                                 bos.write(by);
  24.                         }
  25.                 }catch(IOException e)
  26.                 {
  27.                         throw new RuntimeException("读取文件失败");
  28.                 }
  29.                 finally
  30.                 {
  31.                         try
  32.                         {
  33.                                 if(bis != null)
  34.                                         bis.myClose();
  35.                         }catch(IOException e)
  36.                         {
  37.                                 throw new RuntimeException("读取关闭失败");
  38.                         }
  39.                         try
  40.                         {
  41.                                 if(bos != null)
  42.                                         bos.close();
  43.                         }catch(IOException e)
  44.                         {
  45.                                 throw new RuntimeException("写入关闭失败");
  46.                         }
  47.                 }
  48.         }
  49. }
  50. class MyBufferedInputStream
  51. {
  52.         private InputStream in;
  53.         private byte[] buf = new byte[1024];
  54.         private int pos = 0;
  55.         private int count = 0;
  56.         
  57.         MyBufferedInputStream(InputStream in)
  58.         {
  59.                 this.in = in;
  60.         }
  61.         /**
  62.          *一次读取一个字节
  63.          *返回类型是int而不是byte是为了防止是读取到的字节是1111 1111的情况,当被调用时会以为是-1而停止读取
  64.          */
  65.         public int myRead()throws IOException
  66.         {
  67.                 //buf是个用来缓冲的数组,当buf里面的数据都读取完时,也就是count=0时,执行一次in.read()
  68.                 if(count == 0)
  69.                 {
  70.                         count = in.read(buf);//一次读取1024个字节,放入缓冲的数组中
  71.                         if(count<0)//当没有数据时,返回-1
  72.                                             return -1;
  73.                         byte b = buf[pos];//从buf数组中按下标读取字节
  74.                         count --;//读取的次数--
  75.                         pos ++;//数组下标++
  76.                         return b&255;//b&255,是为了防止二进制字节数据读取到的字节是1111 1111的情况
  77.                         //当是1111 1111时,b&255 提升为int类型,会留原数据不变化,
  78.                 }
  79.                 //当buf缓冲的数组中还有数据时,也就是count>0时,不执行in.read(),继续读取buf缓冲中的数据
  80.                 if(count > 0)//
  81.                 {
  82.                         byte b = buf[pos];
  83.                         count --;
  84.                         pos ++;
  85.                         return b&255;
  86.                 }
  87.                 return -1;
  88.         }
  89.         public void myClose() throws IOException
  90.         {
  91.                 in.close();
  92.         }
  93. }
复制代码





欢迎光临 黑马程序员技术交流社区 (http://bbs.itheima.com/) 黑马程序员IT技术论坛 X3.2