首先须要明确一点的是,字节流处理文件的时候是基于字节的,而字符流处理文件则是基于一个个字符为基本单元的。java
但实际上,字符流操做的本质就是「字节流操做」+「编码」两个过程的封装,你想是否是,不管你是写一个字符到文件,你须要将字符编码成二进制,而后以字节为基本单位写入文件,或是你读一个字符到内存,你须要以字节为基本单位读出,而后转码成字符。数组
理解这一点很重要,这将决定你对字符流总体上的理解是怎样的,下面咱们一块儿看看相关 API 的设计。缓存
在正式学习字符流基类以前,咱们须要知道 Java 中是如何表示一个字符的。学习
首先,Java 中的默认字符编码为:UTF-8,而咱们知道 UTF-8 编码的字符使用 1 到 4 个字节进行存储,越经常使用的字符使用越少的字节数。this
而 char 类型被定义为两个字节大小,也就是说,对于一般的字符来讲,一个 char 便可存储一个字符,但对于一些增补字符集来讲,每每会使用两个 char 来表示一个字符。编码
Reader 做为读字符流的基类,它提供了最基本的字符读取操做,咱们一块儿看看。spa
先看看它的构造器:设计
protected Object lock; protected Reader() { this.lock = this; } protected Reader(Object lock) { if (lock == null) { throw new NullPointerException(); } this.lock = lock; }
Reader 是一个抽象类,因此毋庸置疑的是,这些构造器是给子类调用的,用于初始化 lock 锁对象,这一点咱们后续会详细解释。code
public int read() throws IOException { char cb[] = new char[1]; if (read(cb, 0, 1) == -1) return -1; else return cb[0]; } public int read(char cbuf[]) throws IOException { return read(cbuf, 0, cbuf.length); } abstract public int read(char cbuf[], int off, int len)
基本的读字符操做都在这了,第一个方法用于读取一个字符出来,若是已经读到了文件末尾,将返回 -1,一样的以 int 做为返回值类型接收,为何不用 char?缘由是同样的,都是因为 -1 这个值的解释不肯定性。对象
第二个方法和第三个方法是相似的,从文件中读取指定长度的字符放置到目标数组当中。第三个方法是抽象方法,须要子类自行实现,而第二个方法却又是基于它的。
还有一些方法也是相似的:
这些个方法其实都见名知意,而且和咱们的 InputStream 大致上都差很少,都没有什么核心的实现,这里再也不赘述,你大体知道它内部有些个什么东西便可。
Writer 是写的字符流,它用于将一个或多个字符写入到文件中,固然具体的 write 方法依然是一个抽象的方法,待子类来实现,因此咱们这里亦再也不赘述了。
适配器字符流继承自基类 Reader 或 Writer,它们算是字符流体系中很是重要的成员了。主要的做用就是,将一个字节流转换成一个字符流,咱们先以读适配器为例。
首先就是它最核心的成员:
private final StreamDecoder sd;
StreamDecoder 是一个解码器,用于将字节的各类操做转换成字符的相应操做,关于它咱们会在后续的介绍中不间断的提到它,这里不作统一的解释。
而后就是构造器:
public InputStreamReader(InputStream in) { super(in); try { sd = StreamDecoder.forInputStreamReader(in, this, (String)null); } catch (UnsupportedEncodingException e) { throw new Error(e); } } public InputStreamReader(InputStream in, String charsetName) throws UnsupportedEncodingException { super(in); if (charsetName == null) throw new NullPointerException("charsetName"); sd = StreamDecoder.forInputStreamReader(in, this, charsetName); }
这两个构造器的目的都是为了初始化这个解码器,都调用的方法 forInputStreamReader,只是参数不一样而已。咱们不妨看看这个方法的实现:
这是一个典型的静态工厂模式,三个参数,var0 和 var1 没什么好说的,分别表明的是字节流实例和适配器实例。
而参数 var2 其实表明的是一种字符编码的名称,若是为 null,那么将使用系统默认的字符编码:UTF-8 。
最终咱们可以获得一个解码器实例。
接着介绍的全部方法几乎都是依赖的这个解码器而实现的。
public String getEncoding() { return sd.getEncoding(); } public int read() throws IOException { return sd.read(); } public int read(char cbuf[], int offset, int length){ return sd.read(cbuf, offset, length); } public void close() throws IOException { sd.close(); }
解码器中相关的方法的实现代码仍是相对复杂的,这里咱们不作深刻的研究,但大致上的实现思路就是:「字节流读取 + 解码」的过程。
固然了,OutputStreamWriter 中必然也存在一个相反的 StreamEncoder 实例用于编码字符。
除了这一点外,其他的操做并无什么不一样,或是经过字符数组向文件中写入,或是经过字符串向文件中写入,又或是经过 int 的低 16 位向文件中写入。
文件的字符流能够说很是简单了,除了构造器,就不存在任何其余方法了,彻底依赖文件字节流。
咱们以 FileReader 为例,
FileReader 继承自 InputStreamReader,有且仅有如下三个构造器:
public FileReader(String fileName) throws FileNotFoundException { super(new FileInputStream(fileName)); } public FileReader(File file) throws FileNotFoundException { super(new FileInputStream(file)); } public FileReader(FileDescriptor fd) { super(new FileInputStream(fd)); }
理论上来讲,全部的字符流都应当以咱们的适配器为基类,由于只有它提供了字符到字节之间的转换,不管你是写或是读都离不开它。
而咱们的 FileReader 并无扩展任何一个本身的方法,父类 InputStreamReader 中预实现的字符操做方法对他来讲已经足够,只须要传入一个对应的字节流实例便可。
FileWriter 也是同样的,这里再也不赘述了。
字符数组和字节数组流是相似的,都是用于解决那种不肯定文件大小,而须要读取其中大量内容的状况。
因为它们内部提供动态扩容机制,因此既能够彻底容纳目标文件,也能够控制数组大小,不至于分配过大内存而浪费了大量内存空间。
先以 CharArrayReader 为例
protected char buf[]; public CharArrayReader(char buf[]) { this.buf = buf; this.pos = 0; this.count = buf.length; } public CharArrayReader(char buf[], int offset, int length){ //.... }
构造器核心任务就是初始化一个字符数组到内部的 buf 属性中,之后全部对该字符数组流实例的读操做都基于 buf 这个字符数组。
关于 CharArrayReader 的其余方法以及 CharArrayWriter,这里再也不赘述了,和上篇的字节数组流基本相似。
除此以外,这里还涉及一个 StringReader 和 StringWriter,其实本质上和字符数组流是同样的,毕竟 String 的本质就是 char 数组。
一样的,BufferedReader/Writer 做为一种缓冲流,也是装饰者流,用于提供缓冲功能。大致上相似于咱们的字节缓冲流,这里咱们简单介绍下。
private Reader in; private char cb[]; private static int defaultCharBufferSize = 8192; public BufferedReader(Reader in, int sz){..} public BufferedReader(Reader in) { this(in, defaultCharBufferSize); }
cb 是一个字符数组,用于缓存从文件流中读取出来的部分字符,你能够在构造器中初始化这个数组的长度,不然将使用默认值 8192 。
public int read() throws IOException {..} public int read(char cbuf[], int off, int len){...}
关于 read,它依赖成员属性 in 的读方法,而 in 做为一个 Reader 类型,内部每每又依赖的某个 InputStream 实例的读方法。
因此说,几乎全部的字符流都离不开某个字节流实例。
关于 BufferedWriter,这里也再也不赘述了,大致上都是相似的,只不过一个是读一个是写而已,都围绕着内部的字符数组进行。
打印输出流主要有两种,PrintStream 和 PrintWriter,前者是字节流,后者是字符流。
这两个流算是对各自类别下的流作了一个集成,内部封装有丰富的方法,但实现也稍显复杂,咱们先来看这个 PrintStream 字节流:
主要的构造器有这么几个:
显然,简单的构造器会依赖复杂的构造器,这已经算是 jdk 设计「老套路」了。区别于其余字节流的一点是,PrintStream 提供了一个标志 autoFlush,用于指定是否自动刷新缓存。
接着就是 PrintStream 的写方法:
除此以外,PrintStream 还封装了大量的 print 的方法,写入不一样类型的内容到文件中,例如:
固然,这些方法并不会真正的将数值的二进制写入文件,而只是将它们所对应的字符串写入文件,例如:
print(123);
最终写入文件的不是 123 所对应的二进制表述,而仅仅是 123 这个字符串,这就是打印流。
PrintStream 使用的缓冲字符流实现全部的打印操做,若是指明了自动刷新,则遇到换行符号「\n」会自动刷新缓冲区。
因此说,PrintStream 集成了字节流和字符流中全部的输出方法,其中 write 方法是用于字节流操做,print 方法用于字符流操做,这一点须要明确。
至于 PrintWriter,它就是全字符流,彻底针对字符进行操做,不管是 write 方法也好,print 方法也好,都是字符流操做。
总结一下,咱们花了三篇文章讲解了 Java 中的字节流和字符流操做,字节流基于字节完成磁盘和内存之间的数据传输,最典型的就是文件字符流,它的实现都是本地方法。有了基本的字节传输能力后,咱们还可以经过缓冲来提升效率。
而字符流的最基本实现就是,InputStreamReader 和 OutputStreamWriter,理论上它俩就已经可以完成基本的字符流操做了,但也仅仅局限于最基本的操做,而构造它们的实例所必需的就是「一个字节流实例」+「一种编码格式」。
因此,字符流和字节流的关系也就如上述的等式同样,你写一个字符到磁盘文件中所必需的步骤就是,按照指定编码格式编码该字符,而后使用字节流将编码后的字符二进制写入文件中,读操做是相反的。