Java 字符流文件读写
上篇文章,我们介绍了 Java 的文件字节流框架中的相关内容,而我们本篇文章将着重于文件字符流的相关内容。
首先需要明确一点的是,字节流处理文件的时候是基于字节的,而字符流处理文件则是基于一个个字符为基本单元的。
但实际上,字符流操作的本质就是「字节流操作」+「编码」两个过程的封装,你想是不是,无论你是写一个字符到文件,你需要将字符编码成二进制,然后以字节为基本单位写入文件,或是你读一个字符到内存,你需要以字节为基本单位读出,然后转码成字符。
理解这一点很重要,这将决定你对字符流整体上的理解是怎样的,下面我们一起看看相关 API 的设计。
基类 Reader/Writer
在正式学习字符流基类之前,我们需要知道 Java 中是如何表示一个字符的。
首先,Java 中的默认字符编码为:UTF-8,而我们知道 UTF-8 编码的字符使用 1 到 4 个字节进行存储,越常用的字符使用越少的字节数。
而 char 类型被定义为两个字节大小,也就是说,对于通常的字符来说,一个 char 即可存储一个字符,但对于一些增补字符集来说,往往会使用两个 char 来表示一个字符。
Reader 作为读字符流的基类,它提供了最基本的字符读取操作,我们一起看看。
先看看它的构造器:
protected Object lock;
protected Reader() {
this.lock = this;
}
protected Reader(Object lock) {
if (lock == null) {
throw new NullPointerException();
}
this.lock = lock;
}
Reader 是一个抽象类,所以毋庸置疑的是,这些构造器是给子类调用的,用于初始化 lock 锁对象,这一点我们后续会详细解释。
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 这个值的解释不确定性。
第二个方法和第三个方法是类似的,从文件中读取指定长度的字符放置到目标数组当中。第三个方法是抽象方法,需要子类自行实现,而第二个方法却又是基于它的。
还有一些方法也是类似的:
- public long skip(long n):跳过 n 个字符
- public boolean ready():下一个字符是否可读
- public boolean markSupported():见 reset 方法
- public void mark(int readAheadLimit):见 reset 方法
- public void reset():用于实现重复读操作
- abstract public void close():关闭流
这些个方法其实都见名知意,并且和我们的 InputStream 大体上都差不多,都没有什么核心的实现,这里不再赘述,你大致知道它内部有些个什么东西即可。
Writer 是写的字符流,它用于将一个或多个字符写入到文件中,当然具体的 write 方法依然是一个抽象的方法,待子类来实现,所以我们这里亦不再赘述了。
适配器 InpustStramReader/OutputStreamWriter
适配器字符流继承自基类 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/Writer
文件的字符流可以说非常简单了,除了构造器,就不存在任何其他方法了,完全依赖文件字节流。
我们以 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/Writer
字符数组和字节数组流是类似的,都是用于解决那种不确定文件大小,而需要读取其中大量内容的情况。
由于它们内部提供动态扩容机制,所以既可以完全容纳目标文件,也可以控制数组大小,不至于分配过大内存而浪费了大量内存空间。
先以 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
同样的,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 字节流:
主要的构造器有这么几个:
- public PrintStream(OutputStream out)
- public PrintStream(OutputStream out, boolean autoFlush)
- public PrintStream(OutputStream out, boolean autoFlush, String encoding)
- public PrintStream(String fileName)
显然,简单的构造器会依赖复杂的构造器,这已经算是 jdk 设计「老套路」了。区别于其他字节流的一点是,PrintStream 提供了一个标志 autoFlush,用于指定是否自动刷新缓存。
接着就是 PrintStream 的写方法:
- public void write(int b)
- public void write(byte buf[], int off, int len)
除此之外,PrintStream 还封装了大量的 print 的方法,写入不同类型的内容到文件中,例如:
- public void print(boolean b)
- public void print(char c)
- public void print(int i)
- public void print(long l)
- public void print(float f)
- 等等
当然,这些方法并不会真正的将数值的二进制写入文件,而只是将它们所对应的字符串写入文件,例如:
print(123);
最终写入文件的不是 123 所对应的二进制表述,而仅仅是 123 这个字符串,这就是打印流。
PrintStream 使用的缓冲字符流实现所有的打印操作,如果指明了自动刷新,则遇到换行符号「\n」会自动刷新缓冲区。
所以说,PrintStream 集成了字节流和字符流中所有的输出方法,其中 write 方法是用于字节流操作,print 方法用于字符流操作,这一点需要明确。
至于 PrintWriter,它就是全字符流,完全针对字符进行操作,无论是 write 方法也好,print 方法也好,都是字符流操作。
总结一下,我们花了三篇文章讲解了 Java 中的字节流和字符流操作,字节流基于字节完成磁盘和内存之间的数据传输,最典型的就是文件字符流,它的实现都是本地方法。有了基本的字节传输能力后,我们还能够通过缓冲来提高效率。
而字符流的最基本实现就是,InputStreamReader 和 OutputStreamWriter,理论上它俩就已经能够完成基本的字符流操作了,但也仅仅局限于最基本的操作,而构造它们的实例所必需的就是「一个字节流实例」+「一种编码格式」。
所以,字符流和字节流的关系也就如上述的等式一样,你写一个字符到磁盘文件中所必需的步骤就是,按照指定编码格式编码该字符,然后使用字节流将编码后的字符二进制写入文件中,读操作是相反的。
文章中的所有代码、图片、文件都云存储在我的 GitHub 上:
(https://github.com/SingleYam/overview_java)
欢迎关注微信公众号:扑在代码上的高尔基,所有文章都将同步在公众号上。
Java 字符流文件读写的更多相关文章
- Java 字符流实现文件读写操作(FileReader-FileWriter)
Java 字符流实现文件读写操作(FileReader-FileWriter) 备注:字符流效率高,但是没有字节流底层 字节流地址:http://pengyan5945.iteye.com/blog/ ...
- Java 字节流实现文件读写操作(InputStream-OutputStream)
Java 字节流实现文件读写操作(InputStream-OutputStream) 备注:字节流比字符流底层,但是效率底下. 字符流地址:http://pengyan5945.iteye.com/b ...
- java字符流操作flush()方法及其注意事项
java字符流操作flush()方法及其注意事项 flush()方法介绍 查阅文档可以发现,IO流中每一个类都实现了Closeable接口,它们进行资源操作之后都需要执行close()方法将流关闭 ...
- Java 字符流读写文件
据说,java读写文件要写很多,贼麻烦,不像c艹,几行代码就搞定.只能抄抄模板拿来用了. 输入输出流分字节流和字符流.先看看字符流的操作,字节转化为字符也可读写. 一.写入文件 1.FileWrite ...
- JAVA 字节流和字符流度读写的区别
java处理文件的类里面,stream结尾都是采用字节流,reader和writer结尾都是采用字符流.两者的区别就是读写的时候一个是按字节读写,一个是按字符. 字符流的底层就是字节流.而字符流主要是 ...
- java IO流文件的读写具体实例(转载)
引言: 关于java IO流的操作是非常常见的,基本上每个项目都会用到,每次遇到都是去网上找一找就行了,屡试不爽.上次突然一个同事问了我java文件的读取,我一下子就懵了第一反应就是去网上找,虽然也能 ...
- Java字符流和字节流对文件操作
记得当初自己刚开始学习Java的时候,对Java的IO流这一块特别不明白,所以写了这篇随笔希望能对刚开始学习Java的人有所帮助,也方便以后自己查询.Java的IO流分为字符流(Reader,Writ ...
- java字符流实现文件间的内容复制
package com.io.demo1; import java.io.FileReader; import java.io.FileWriter; public class TestFileSTr ...
- java 四种方式实现字符流文件的拷贝对比
将D:\\应用软件\\vm.exe 拷贝到C:\\vm.exe 四种方法耗费时间对比 4>2>3>1 package Copy; import java.io.Buffere ...
随机推荐
- How to configure ODBC DSN in Client to access remote DB2 for Windows
How to configure ODBC DSN in Client to access remote DB2 for Windows MA Gen feng (Guangdong Unito ...
- javascript原始值和对象引用
一句话来说:原始值是不可变的,而对象引用是可变的. js中的原始值(undefined.null.布尔值.数字和字符串)与对象(包括数组和函数)有着本质的区别.原始值是不可更改的,任何方法都无法更改一 ...
- obj-c编程15[Cocoa实例04]:基于Core Data的多文档程序示例[未完待续]
上一个例子我们使用的模式数据实际上是基于一个Person数组,现在我们看一下如何使用Cocoa中的Core Data框架支持,几乎不用写一行代码,完成模式数据的建立. 我们这里模式的元素使用的是Car ...
- obj-c编程10:Foundation库中类的使用(5)[时间对象]
隔了好久才有了这新的一篇,还是无奈的时间啊!so这次我们就着重谈谈它喽. F库中有很多时间相关的类,比如NSDate,NSTimeInterval,NSTimeZone,NSDateComponent ...
- Convert Sorted Array to Binary Search Tree(将一个有序数组转换成一颗二叉搜索树)
Given an array where elements are sorted in ascending order, convert it to a height balanced BST. Fo ...
- windows安装weblogic和域的建立
Copyright ©2014 Manchester United
- ThreadPoolExecutor的运转机制
最近发现几起对ThreadPoolExecutor的误用,其中包括自己,发现都是因为没有仔细看注释和内部运转机制,想当然的揣测参数导致,先看一下新建一个ThreadPoolExecutor的构建参数: ...
- [总结] 二维ST表及其优化
二维 \(\mathcal{ST}\) 表,可以解决二维 \(\mathcal{RMQ}\) 问题.这里不能带修改,如果要修改,就需要二维线段树解决了. 上一道例题吧 ZOJ2859 类比一维 \(\ ...
- 如何将程序集安装到全局程序集缓存GAC
针对一些类库项目或用户控件项目(一般来说,这类项目最后编译生成的是一个或多个dll文件),在程序开发完成后,有时需要将开发的程序集(dll文件)安装部署到GAC(全局程序集缓存)中,以便其他的程序也可 ...
- Flex编码随笔
1.CSS定义最好放在application里面. 2.数据源是数组数据时,最好把数组转换为ArrayCollection. 3.List.CheckBox等控件的HttpService Params ...