上一篇博客中,测试分析了FileReader&FileWriter,和BufferWriter&BufferReader之间的性能对比。仅仅只是简单分析。现在我基于源码的角度,来分析BufferReader与FileReader的区别。

首先在构造函数上

// FileReader构造函数
public FileReader(File file) throws FileNotFoundException {
super(new FileInputStream(file));
}
//BufferReader构造函数
public BufferedReader(Reader in, int sz) {
super(in);
if (sz <= 0)
throw new IllegalArgumentException("Buffer size <= 0");
this.in = in;
cb = new char[sz];
nextChar = nChars = 0;
}
public BufferedReader(Reader in) {
this(in, defaultCharBufferSize);//defaultCharBufferSize = 8192,默认缓冲区大小
}

因为BufferedReader是对FileReader或者InputStreamReader进行包装,而FileReader的父类是InputStreamReader,所以两者的交集就是InputStreamReader,换句话说二者在构造时,都调用了InputStreamReader的构造函数:

//InputStreamReader的构造函数
public InputStreamReader(InputStream in) {
super(in);
try {
sd = StreamDecoder.forInputStreamReader(in, this, (String)null); // ## check lock object
} catch (UnsupportedEncodingException e) {
// The default encoding should always be available
throw new Error(e);
}
}

InputStreamReader还有其它几个重载构造函数,但是它们有一个共性,全都调用了sd = StreamDecoder.forInputStreamReader()方法。用它可以获得一个StreamDecoder对象。

再看后面的源码:

//InputStreamReader
public int read() throws IOException {
return sd.read();
}
public int read(char cbuf[], int offset, int length) throws IOException {
return sd.read(cbuf, offset, length);
}

可见InputStreamReader中的read()和read(cbuf, offset, length)操作,实际上都是在StreamDecoder中进行。下面分析一下read()和read(cbuf, offset, length)两种情况下FileReader和BufferReader的区别。

read()

逐个读入字符

FileReader没有重写read()方法,所以相当于直接调用StreamDecoder中的read()。

而BufferReader重写了read(),如下:

public int read() throws IOException {
synchronized (lock) {
ensureOpen();
for (;;) {
if (nextChar >= nChars) {
fill();
if (nextChar >= nChars)
return -1;
}
if (skipLF) {
skipLF = false;
if (cb[nextChar] == '\n') {
nextChar++;
continue;
}
}
return cb[nextChar++];
}
}
}

可以看到read()调用了一个fill()函数。其实精髓地方就在这个fill(),它的作用就是填充输入缓冲区。代码如下:

/**
* Fills the input buffer, taking the mark into account if it is valid.
*/
private void fill() throws IOException {
int dst;
if (markedChar <= UNMARKED) {
/* No mark */
dst = 0;
} else {
/* Marked */
int delta = nextChar - markedChar;
if (delta >= readAheadLimit) {
/* Gone past read-ahead limit: Invalidate mark */
markedChar = INVALIDATED;
readAheadLimit = 0;
dst = 0;
} else {
if (readAheadLimit <= cb.length) {
/* Shuffle in the current buffer */
System.arraycopy(cb, markedChar, cb, 0, delta);
markedChar = 0;
dst = delta;
} else {
/* Reallocate buffer to accommodate read-ahead limit */
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;
}
}

fill()在循环中调用了in.read(cb, dst, cb.length - dst),这实际上就是调用InputStreamReader中的read(cbuf, offset, length),即调用StreamDecoder中的read(cbuf, offset, length)。

所以,即便是一次读入一个字符,BufferReader依然是走char[]模式,性能比直接调用read()要好。

read(cbuf, offset, length)

一次读入一个char数组

注意我前一个博客中的代码是这样写的:read(cbuf),没有后面两个参数,这不用惊讶,实际上就是调用了read(cbuf, offset, length),因为在顶级父类Reader中,有如下代码:

public int read(char cbuf[]) throws IOException {
return read(cbuf, 0, cbuf.length);
}

但是注意,read(cbuf, offset, length)方法在Reader中是抽象方法,这个方法在StreamDecoder中实现。

根据源码,FileReader没有重写read(cbuf, offset, length)方法,所以相当于调用StreamDecoder中的read(cbuf, offset, length);BufferReader重写了,但也使用了fill()方法进行填充,实质上也是使用了StreamDecoder中的read(cbuf, offset, length)。

所以,使用FileReader一次读入一个char数组时,只要字符数组的大小设置合理,可以达到BufferReader的缓冲区效果。这样就解释了上篇博客测试结果。

简单总结:

  • InputStreamReader构造函数会创建StreamDecoder对象,在该对象中进行输入操作;
  • BufferReader,不管是一次读入一个字符,还是一次一个数组,都会调用StreamDecoder中的read(cbuf, offset, length),即都使用了缓冲区;
  • FileReader,逐个读取就调用read(),一次一个数组就调用read(cbuf, offset, length)。

其它

StreamDecoder

StreamDecoder的源码分析可参见这个博客,分析得很好:

JAVA基础知识之StreamDecoder流

我这里就不再赘述,只提StreamDecoder中几个关键的方法:

  • read0():逐个读入字符的方法;
  • read(char cbuf[], int offset, int length):调用implRead(cbuf, off, off + len);
  • readBytes():读入底层输入的字节流;
  • implRead():将char[]包装成字符缓冲区,调用readBytes()将数据写入字节缓冲区,然后将字节缓冲区中的字节转码成字符存入字符缓冲区。

为啥都说OutputStreamWriter和InputStreamReader是字节流和字符流的桥梁,这里貌似能窥见一斑。

FileWriter与BufferWriter

输出流的分析我这里就不深入写了,跟输入流大同小异,总结成以下三点:

  • 类似的,OutputStreamWriter在构造函数中创建StreamEncoder对象;
  • FileWriter依然不重写各种write方法;
  • BufferWriter重写了write()和write(cbuf, off, len)方法,实际上两者原理类似,都是将字符或者char数组写入buffer,buffer满了再flush写入文件,然后再继续写入buffer循环往复直到结束(char[]写入buffer,实际上就是用System.arraycopy把一个char[]复制进另一个char[];当然还有一种把string写入char[]的方法,getChars);

【java I/O流总结】基于源码比较FileReader和BufferReader的更多相关文章

  1. [Java并发] AQS抽象队列同步器源码解析--锁获取过程

    要深入了解java并发知识,AbstractQueuedSynchronizer(AQS)是必须要拿出来深入学习的,AQS可以说是贯穿了整个JUC并发包,例如ReentrantLock,CountDo ...

  2. 【转载】深度解读 java 线程池设计思想及源码实现

    总览 开篇来一些废话.下图是 java 线程池几个相关类的继承结构: 先简单说说这个继承结构,Executor 位于最顶层,也是最简单的,就一个 execute(Runnable runnable) ...

  3. Java并发指南12:深度解读 java 线程池设计思想及源码实现

    ​深度解读 java 线程池设计思想及源码实现 转自 https://javadoop.com/2017/09/05/java-thread-pool/hmsr=toutiao.io&utm_ ...

  4. 转:微信开发之使用java获取签名signature(贴源码,附工程)

    微信开发之使用java获取签名signature(贴源码,附工程) 标签: 微信signature获取签名 2015-12-29 22:15 6954人阅读 评论(3) 收藏 举报  分类: 微信开发 ...

  5. Java禁止浏览器有缓存的源码

    Java禁止浏览器有缓存的源码 import java.io.IOException; import javax.servlet.Filter; import javax.servlet.Filter ...

  6. 从壹开始微服务 [ DDD ] 之十一 ║ 基于源码分析,命令分发的过程(二)

    缘起 哈喽小伙伴周三好,老张又来啦,DDD领域驱动设计的第二个D也快说完了,下一个系列我也在考虑之中,是 Id4 还是 Dockers 还没有想好,甚至昨天我还想,下一步是不是可以写一个简单的Angu ...

  7. 并发编程(十二)—— Java 线程池 实现原理与源码深度解析 之 submit 方法 (二)

    在上一篇<并发编程(十一)—— Java 线程池 实现原理与源码深度解析(一)>中提到了线程池ThreadPoolExecutor的原理以及它的execute方法.这篇文章是接着上一篇文章 ...

  8. java集合树状结构及源码

    java集合树状结构及源码 最近一直想看一下java集合的源码,毕竟平时用的比较多,但总是感觉是跟着习惯new出来一个对象,比如ArrayList,HashMap等等,所以就简单的看了一下,了解了一下 ...

  9. 基于源码编译安装openssh

     最近的,openssl/openssh等相继漏洞的暴露,让暴露在公网的linux.沦陷为肉鸡的正营... 没办法,还是升级版本... 00.openssh简介 OpenSSH 是一组安全远程的连接工 ...

随机推荐

  1. 递归遍历磁盘下的某一文件夹中所有文件,并copy文件生成文件和带文件夹的文件

    package com.hudong.test; import java.io.File; import java.io.IOException; import java.util.ArrayList ...

  2. python3.4多线程实现同步的四种方式

    临界资源即那些一次只能被一个线程访问的资源,典型例子就是打印机,它一次只能被一个程序用来执行打印功能,因为不能多个线程同时操作,而访问这部分资源的代码通常称之为临界区. 1. 锁机制 threadin ...

  3. 漫谈国内外Android生态:华为发布的 HMS 服务,对 Mate30 系列无法搭载 Google GMS 的补偿有多大(原创)

    如果既用过iPhone,也用过国际版Android,还用过国内的安卓,(并且这三种都用了半年以上),就能体会到GMS多重要.可以说,iOS的体验大幅度领先于国内的安卓,一多半的原因是国内安卓没有GMS ...

  4. webpack配置中环境变量-process.env. NODE_ENV

    背景 webpack有一特性就是可以让使用者灵活的在不同环境(开发环境,生产环境等)进行相应的特性的策略打包,比如: 是否使用反向代理使用接口,针对不同的静态资源(如图片等)是直接拷贝还是进行打包编译 ...

  5. Java面试必问之Hashmap底层实现原理(JDK1.7)

    1. 前言 Hashmap可以说是Java面试必问的,一般的面试题会问: Hashmap有哪些特性? Hashmap底层实现原理(get\put\resize) Hashmap怎么解决hash冲突? ...

  6. 天坑,CSS之定位Position(六分之五)

    Position定位 个人觉得position这个属性真的算是CSS的见面杀了.尤其是absolute,当年可是被虐的不轻.当然了,现在爱上了这个属性,谁用谁知道. position属性 positi ...

  7. Java基础--方法的定义

    1.为什么要有方法? 方法(又叫函数)就是一段特定功能的代码块.方法提高程序的复用性和可读性. 比如,有了方法,我们可以把要重复使用的一段代码提炼出来,然后在每个需要执行这段代码的地方去调用即可. 2 ...

  8. 这样学习Servlet,会事半功倍!!

    前言 工作已经有一段时间了,如果让我重新学Servlet,我会怎么学呢?下面抛出两个常见的问题,我分开来解答 2020年了,还需要学Servlet吗? Servlet的学习路线(学习重点) 一.202 ...

  9. Java探针技术-retransformclasses的介绍

    retransformclasses void retransformclasses(class... classes) throws unmodifiableclassexception 重转换提供 ...

  10. php通过单例模式使一个类只能创建一个对象。

    单例模式也就是一个类只能创建出一个对象 首先你要知道它的基本思想为:三私一公! 何为三私一公?   1(私).防止用户通过构造方法创建对象,因此私有化构造方法. 2(公).创建一个公共静态函数用来进入 ...