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 ...
随机推荐
- 程序设计之---单例模式VS静态方法
我们在设计程序经常会有这种需求 , 某个类里的方法能够全局访问. 在这种情况下有两种实现方案 : 1>单例模式(Singleton); 2>静态方法. 但是, 对于这两种实现方式 , 那种 ...
- ASI与AFN网络请求的的比较
对比 ASI AFN 更新状态 2012年10月份,已经停止更新 持续更新中,目前已更新至3.0版 介绍 ASI的直接操作对象ASIHTTPRequest,是一个实现了了NSCopying协议的NSO ...
- rails中weill_paginate的paginate方法中不能使用额外参数的解决办法
我们知道高版本中的rails中的分页功能已经放在will_paginate这个gem中,我们在控制器方法中往往需要调用其paginate方法来实现分页数据集控制,举个例子:正常的情况我们想要每页显示1 ...
- iframe不起作用?你可能碰到它了。
有一个需求要在iframe里显示一个网站,但设置iframe的src后,iframe并没有起作用.然后打开控制台,发现错误如下: , 对其搜索找到了答案:https://stackoverflow.c ...
- 关于MySQL 5.6.24 解压缩版重启电脑后,无法启动的问题
最近的项目需要用到mysql,想起以前安装过,就得应该没啥问题.也不知道是软件更新换代的问题,还是版权问题,网上找的msi版本的mysql都很难安装,一开始要安装.NET,我忍了,然后又要安装Visu ...
- Oracle——多表查询
本次预计讲解的知识点 1. 多表查询的操作.限制.笛卡尔积的问题: 2. 统计函数及分组统计的操作: 3. 子查询的操作,并且结合限定查询.数据排序.多表查询.统计查询一起完成各个复杂查询的操作: 一 ...
- virtualenv 中 install flask 的小问题
最经在学习Python flask 框架 ,用virtualenv建立好我的flask虚拟环境后,执行 sudo pip install flask 并没有报错 我以为已经装上了flask,但当我进入 ...
- C#学习笔记 day_three
C#学习笔记 day three Chapter 3 类型 3.3引用类型 引用类型的变量也成为对象,有六种类型:(1)对象类型 (2)字符串类型 (3)类类型 (4)数组类型 (5)接口类型 (6) ...
- java解析xml字符串方法
一,用DOM4J 针对无重复标签的xml字符串格式,如下: 针对此种情况可用DOM4J解析法,引入 dom4j的相关jar包代码如下: Document document=DocumentHelpe ...
- 运算符优先级--C
优先口决 括号成员第一; //括号运算符[]() 成员运算符. -> 全体单目第二; //所有的单目运算符比如++ -- +(正) -(负) 指针运算*& 乘除余三,加减四; //这个& ...