`
DiaoCow
  • 浏览: 244322 次
  • 性别: Icon_minigender_1
  • 来自: 南京
社区版块
存档分类
最新评论

BufferedInputStream实现原理分析

    博客分类:
  • Java
阅读更多
BufferedInputStream是一个带有缓冲区的输入流,通常使用它可以提高我们的读取效率,现在我们看下BufferedInputStream的实现原理:
BufferedInputStream内部有一个缓冲区,默认大小为8M,每次调用read方法的时候,它首先尝试从缓冲区里读取数据,若读取失败(缓冲区无可读数据),则选择从物理数据源(譬如文件)读取新数据(这里会尝试尽可能读取多的字节)放入到缓冲区中,最后再将缓冲区中的内容部分或全部返回给用户.由于从缓冲区里读取数据远比直接从物理数据源(譬如文件)读取速度快,所以BufferedInputStream的效率很高!


在具体看源码之前,我们还需要了解BufferedInputStream的mark操作:void mark(int markLimit)
当你调用mark方法时,内部会保存一个markPos标志,它的值为目前读取字节流的pos位置,倘若你调用reset方法,这时候会把pos重置为markPos的值,这样你就可以重读已经读过的字节.好像说的不是很清楚,那我们打个比方:有一段字节流是abcdefg, 当你读取完字母a调用mark方法(此时markPos指向字母b),接着你继续读取字母b,字母c,字母d,然后此时你调用reset方法(内部把pos重置为markPos),当你再读取下一个字节的时候,你会发现你读取到的是b而不是字母e,这样通过mark方法我们就是实现了重复读(re-read the same bytes)

mark方法中还有个参数markLimit,它的值表示在调用mark方法后reset方法前最多允许读取的字节数(根据我的测试,以及查看源代码发现,这个最大字节数,其实是由markLimit和buffer.size中较大的那个决定的),如果超过这个限制,则在调用reset方法时会报:Reseting to invalid mark 异常


说完了这么多,让我们来赶紧看看源码吧:

// BufferedInputStream主要有这两个构造方法

public BufferedInputStream(InputStream in) {
     this(in, defaultBufferSize);   // 默认缓冲区为8M
    }

public BufferedInputStream(InputStream in, int size) {
    super(in);
    if (size <= 0) {
        throw new IllegalArgumentException("Buffer size <= 0");
    }
    buf = new byte[size];
}

你需要指定InputStream(装饰模式的体现)以及bufferSize(可选)

当我们调用read()方法时,它在内部做了一下事情:

public synchronized int read() throws IOException {
    if (pos >= count) {         // 检查是否有可读缓冲数据
        fill();                 // 没有缓冲数据可读,则从物理数据源读取数据并填充缓冲区
        if (pos >= count)       // 若物理数据源也没有多于可读数据,则返回-1,标示EOF
        return -1;
    }

 // 从缓冲区读取buffer[pos]并返回(由于这里读取的是一个字节,而返回的是整型,所以需要把高位置0)
    return getBufIfOpen()[pos++] & 0xff;   
}

private byte[] getBufIfOpen() throws IOException {
    byte[] buffer = buf;      // buf为内部缓冲区
    if (buffer == null)
        throw new IOException("Stream closed");
        return buffer;
}

其中pos为缓冲区buffer下一个可读的数组下标,我们可以一直从缓冲区里读取数据,直到pos变为count(此时只能从物理数据源读取数据),下面我们就分析下,当缓冲区里没有数据可读时,BufferInputStream是如何处理的:

1.若用户没有开启re-read功能(即未调用mark方法),当pos == count时,我们只需要将pos重新置为0,然后从物理源读取数据(假设读到了n个字节),最后把count设置成 n + pos 即可 (其实就是n,因为pos之前被设置成了0), 当下次你在调用read方法时,就直接从缓冲读取,非常快速(如下图);


2.若用户调用了mark方法,情况就变得很复杂了,为什么呢? 这意味着我们需要保存从markPos到pos这段数据(以供用户re-read),现在我们分情况讨论:

a.此时pos < buffer.length,这意味着缓冲区还有多余空间,所以我们可以继续从物理数据源读取数据放入到缓冲区中(如下图);


b.pos == buffer.length,这意味着缓冲区已经没有多余空间,所以只能清空缓冲区内容,但是不要忘了,我们还必须保留原来
markPos到pos那段数据,以供用户re-read,所以我需要这样做:

// 计算需要保留多少字节的数据
int sz = pos - markPos;   
// 然后拷贝到缓冲头部                              
System.arraycopy(buffer, markpos, buffer, 0, sz); 
// 重置pos以及markPos      
pos=sz;                                                                                
markPos=0;
                                  
接着从缓冲区的sz 到 buffer.length又变成可用区间,用来存放从物理数据源读到的数据(如下图)


到这里似乎问题完美的解决了,其实不然,我们忘记考虑markPos失效问题,以及若pos - markPos == buffer.length,那么移了等于白移动,还是没有挪出多余空间,所以实我们应该这样做(后面讨论都是建立在pos == buffer.length的基础上):


2.1 若markPos > 0, 那么 pos - makrPos一定小于缓冲区大小,这样意味着我们按照刚才的算法挪动后,缓冲区就有了空余空间

2.2 若makrPos == 0,  这意味着需要保存的数据满满的充斥着缓冲区,所以这时候我们是无法通过挪动位置来使缓冲区有多余空间的,所以我们只可以清空或扩展缓冲区

2.2.1 当buffer.length >= marklimit时 ,此时意味着markPos已经失效,用户不可以在进行re-read,所以此时我们就可以简单释放整个缓冲区了:pos=0, markPos=-1;

2.2.2 其余情况,意味着markPos还有效,所以我们只能通过扩展缓冲区的方式来使缓冲区有多余空间

说了这么多,我们还是看下相关代码吧:

private void fill() throws IOException {
        byte[] buffer = getBufIfOpen();        // 得到当前缓冲区
     if (markpos < 0)                          // 对应情况1
         pos = 0;         
     else if (pos >= buffer.length)            // 对应情况2
         if (markpos > 0) {                    // 对应情况2.1
               int sz = pos - markpos;
               System.arraycopy(buffer, markpos, buffer, 0, sz);
               pos = sz;
               markpos = 0;
         } else if (buffer.length >= marklimit) {  // 对应情况2.2.1
               markpos = -1;    
               pos = 0;    
         } else {                                  // 对应情况2.2.2
               int nsz = pos * 2;
               if (nsz > marklimit)
                   nsz = marklimit;
               byte nbuf[] = new byte[nsz];
               System.arraycopy(buffer, 0, nbuf, 0, pos);
            ...
         }
    count = pos;
     int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
    if (n > 0)
        count = n + pos;
}

好了关于BufferedInputStream就说道这里,它的 read(byte b[], int off, int len)其实内部实现也大概如此:先从缓冲区读,如果读不到则从物理数据源读取并刷新到缓冲区(可能需要对缓冲区原来内容作必要的挪动或者对缓冲区大小进行扩展)





  • 大小: 29 KB
  • 大小: 33.8 KB
  • 大小: 33.3 KB
  • 大小: 35.9 KB
  • 大小: 27.8 KB
分享到:
评论
2 楼 longshaohang 2014-11-07  
写的不错,不过最后markLimit的讨论太少,我的理解是buffer会维持在markLimit的大小
1. 如果markPos == 0, 同时buffer.length >= markLimit 那么只能抛弃所有的buffer的内容(对应情况2.2.1)
2. 如果markPos == 0,但是buffer.length < markLimit 那么会通过扩大buffer.length 和markLimit一样大,然后再读取数据(对应情况2.2.2)
1 楼 jaychang 2014-04-19  
写的很不错MARK下

相关推荐

    BufferedInputStream(缓冲输入流)详解_动力节点Java学院整理

    BufferedInputStream 的工作原理是通过一个内部缓冲区数组实现的。例如,在新建某输入流对应的 BufferedInputStream 后,当我们通过 read() 读取输入流的数据时,BufferedInputStream 会将该输入流的数据分批填入到...

    socket编程 TCP文件的传输实现 客户端和服务端

    接下来,我们分析文件传输的实现。客户端和服务端都需要实现Socket接口,创建套接字对象。客户端通过Socket对象连接到服务器的特定端口,而服务器则在指定端口监听连接请求。一旦连接建立,文件的传输就可以开始了。...

    socket实现文件上传下载

    通过本文的介绍,我们可以了解到使用 Socket 实现文件上传下载的基本原理和步骤。这种实现方式适用于简单的文件传输需求。对于更复杂的场景,如大文件传输、断点续传等,可能需要考虑更高级的技术方案,例如使用 ...

    用socket实现的复制功能

    下面我们将详细介绍其实现原理及代码分析。 #### 二、技术背景 - **Socket通信**:Socket通信是网络编程的基础之一,它提供了一种进程间通信的方式,允许不同计算机上的应用程序相互通信。 - **Java Socket API**...

    基于java的文件压缩与解压缩系统毕业设计与实现(源代码+项目报告).zip

    项目的核心部分可能会使用到`java.io`和`java.nio`包中的类,如`FileInputStream`、`FileOutputStream`、`BufferedInputStream`、`BufferedOutputStream`等,用于读写文件,以及`ZipOutputStream`和`ZipInputStream`...

    基于JAVA的网络通讯系统设计与实现(论文+系统)

    10. **论文部分**:论文可能详细描述了系统的需求分析、设计决策、实现过程、性能评估和未来改进方向,为读者提供了全面的理解。 总的来说,"基于JAVA的网络通讯系统设计与实现"是一个涵盖理论与实践的综合性项目,...

    基于JAVA的网络通讯系统设计与实现(论文+系统).zip

    通过这个项目,学习者不仅可以掌握Java网络编程的基本技能,还能了解到实际系统开发的全过程,包括需求分析、系统设计、编码实现、测试和文档编写。对于想要深入理解网络通信系统的人来说,这是一个非常有价值的资源...

    JAVA实现文件移动

    根据给定的信息,我们可以总结出以下关于“Java实现文件移动”...通过以上内容,我们可以了解到Java实现文件移动的基本原理和技术要点,包括使用的类库、关键步骤以及示例代码等。这对于理解和实践文件操作非常有帮助。

    http上传--android上实现

    通过对这两个文件的详细分析和理解,开发者可以学习到如何在Android应用中实现实时、高效的文件上传功能。不过,具体的实现细节需要查看源代码才能得知。在实际项目中,可以根据需求调整和优化这部分代码,比如添加...

    基于java的局域网飞鸽传书软件设计与实现(源代码+LW).zip

    4. **文件I/O操作**:读写文件是文件传输的核心,需要熟悉Java的`FileInputStream`、`FileOutputStream`、`BufferedInputStream`、`BufferedOutputStream`等类。 5. **用户界面(UI)设计**:为了让用户能方便地...

    基于JAVA的RSA文件加密软件的设计与实现(源代码+论文)

    8. **论文撰写**:论文可能涵盖了项目的背景、设计思路、实现方法、测试结果和安全性分析等内容,是理论与实践的结合,对于理解和评估项目具有重要意义。 通过以上这些知识点,我们可以了解到一个基于JAVA的RSA文件...

    基于JAVA的文件传输设计与实现.doc

    实现这些功能,开发者需要熟练掌握 Java 的网络编程和文件操作,理解 FTP 协议的工作原理,并可能结合 Socket 编程来建立客户端和服务端之间的通信。SSL/TLS 加密可以用于提供安全的传输层,确保数据不被窃取或篡改...

    基于JAVA的网络通讯系统设计与实现(参考文献+系统).zip

    9. **源代码分析**:提供的源代码可以作为学习和研究的实例,通过阅读和理解代码,可以深入理解上述知识点在实际项目中的应用。 10. **参考文献**:提供的参考文献可能包括相关的技术书籍、研究论文或者在线教程,...

    计算机Java多线程下载技术分析.zip

    4.1 `BufferedInputStream`和`BufferedOutputStream`:用于提高I/O操作性能,减少磁盘和网络的交互次数。 4.2 文件分块读写:使用`RandomAccessFile`实现文件的随机读写,以便于在不同线程间进行数据合并。 4.3 ...

    jsp文件下载源代码

    ### JSP文件下载实现原理与源代码解析 #### 背景介绍 在Web开发过程中,经常需要处理文件上传和下载的功能。对于Java Web开发者来说,利用JSP(JavaServer Pages)来实现文件的下载是一种常见的方式。本文将详细...

    装饰者模式——Decorator

    在实际应用中,装饰者模式常常用于对IO流的处理,如BufferedInputStream和DataOutputStream等,它们都继承自InputStream和OutputStream,通过组合的方式增加了缓冲和数据转换等功能。此外,还可以应用于UI组件的扩展...

    JAVA文件传输(论文+源代码).zip

    在本项目中,“JAVA文件传输(论文+源代码)”可能包含了一份详细探讨Java文件传输原理和技术的学术论文,以及一份实际实现了这些理论的源代码。 首先,文件传输的基础是网络通信,Java中的Socket编程提供了TCP/IP...

    用java做的测试服务器带宽项目(原创)

    下面我们将详细探讨这个项目的实现原理、关键技术和相关知识点。 1. **Java网络编程基础** Java提供了丰富的网络编程API,如Socket、ServerSocket等,用于创建客户端和服务器端的通信。在这个项目中,Java的网络...

    structure.zip

    在IT行业中,设计模式是软件开发中的重要概念,它们代表了在特定场景下解决常见问题的最佳实践。结构型设计模式是其中一类...对于每个模式,深入研究其代码实现,分析其工作原理,将有助于提升你的编程技能和设计思维。

Global site tag (gtag.js) - Google Analytics