Java IO流之字符流【二】
1. 概述
如果使用字节流读取中文。当GBK编码时,中文占用2个字节,当使用UTF-8时,中文占用3个字节。
因此字节流无法读取完整的字符,有可能出现乱码。
在这样的基础上,java的字符流应运而生。
2.字符流类图结构
2.1字符输入流【Reader】
java.io.Reader
是用于读取字符流的抽象类,是表示字符输入流的所有类的超类。它定义了字符输入流的基本共性功能方法。
子类必须实现的方法只有 read(char[], int, int)
和 close()
。但是,多数子类将重写此处定义的一些方法,以提供更高的效率和/或其他功能。
public abstract void close(): 关闭该流并释放与之关联的所有资源。
public int read():读取单个字符, 底层是 read(new char[1], 0, 1)。
public int read(char[] cbuf): 将字符读入数组,底层是read(cbuf, 0, cbuf.length)。
public abstract int read(char[] cbuf, int off, int len): 将字符读入数组的某一部分。
public int read(CharBuffer target): 试图将字符读入指定的字符缓冲区。
public boolean ready(): 判断是否准备读取此流。
public void reset():重置该流。
public long skip(long n) :跳过字符。
java.io.Reader
源码注释:
package java.io;
/**
* 子类必须实现的方法只有 `read(char[], int, int)` 和 `close()`。
* 但是,多数子类将重写此处定义的一些方法,以提供更高的效率和/或其他功能
* @author Mark Reinhold
* @since JDK1.1
*/
public abstract class Reader implements Readable, Closeable {
//锁对象
protected Object lock;
//最大可跳过的字节数
private static final int maxSkipBufferSize = 8192;
// 跳过的字符缓冲区,默认为null
private char skipBuffer[] = null;
// 空参构造的锁对象是调用者本身
protected Reader() {
this.lock = this;
}
// 有参构造
protected Reader(Object lock) {
if (lock == null) {
throw new NullPointerException();
}
this.lock = lock;
}
//尝试将字符读入指定的字符缓冲区。
public int read(java.nio.CharBuffer target) throws IOException {
int len = target.remaining();
char[] cbuf = new char[len];
int n = read(cbuf, 0, len);
if (n > 0)
target.put(cbuf, 0, n);
return n;
}
/**
* 读取单个字符。
* 返回读取的字符,作为0到65535之间的整数,如果到达流末尾,则返回-1
*/
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) throws IOException;
// 跳过读取n个字符
public long skip(long n) throws IOException {
if (n < 0L)
throw new IllegalArgumentException("skip value is negative");
// 比较n和maxSkipBufferSize的最小值,并返回(如果n>8192,则只跳过8192个字符)
int nn = (int) Math.min(n, maxSkipBufferSize);
synchronized (lock) {
if ((skipBuffer == null) || (skipBuffer.length < nn))
//初始化跳过的字符数组
skipBuffer = new char[nn];
long r = n;
while (r > 0) {
// 尝试向缓冲字符数组中读取字符,返回实际读取的字符数
int nc = read(skipBuffer, 0, (int)Math.min(r, nn));
if (nc == -1)
break;
r -= nc;
}
// 返回实际跳过的字符数量
return n - r;
}
}
// 流对象是否就绪(Reader永远返回false)
public boolean ready() throws IOException {
return false;
}
// 是否支持标记操作(默认false)
public boolean markSupported() {
return false;
}
// 标记流位置(Reader不支持该方法)
public void mark(int readAheadLimit) throws IOException {
throw new IOException("mark() not supported");
}
// 重置流对象(Reader不支持该方法)
public void reset() throws IOException {
throw new IOException("reset() not supported");
}
// 关闭流对象(子类应重写该方法)
abstract public void close() throws IOException;
}
2.1.1 【文件字符输入流】FileReader
java.io.FileReader
类是读取字符文件的便利类,使用系统默认的字符编码和字节缓冲区。
java.io.FileReader
本身只有三个构造方法,没有自己定义的方法。
public FileReader(File file) : 创建一个新的 FileReader 流对象 ,指向要读取的File对象。
public FileReader(FileDescriptor fd) : 创建一个新的 FileReader 流对象 ,指向要读取的FileDescriptor对象。
public FileReader(String fileName) : 创建一个新的 FileReader 流对象,指向要读取的文件名称。
java.io.FileReader
源码注释:
package java.io;
public class FileReader extends InputStreamReader {
// 创建一个新的 FileReader 流对象,指向要读取的文件名称。
public FileReader(String fileName) throws FileNotFoundException {
super(new FileInputStream(fileName));
}
// 创建一个新的 FileReader 流对象 ,指向要读取的File对象。
public FileReader(File file) throws FileNotFoundException {
super(new FileInputStream(file));
}
// 创建一个新的 FileReader 流对象 ,指向要读取的FileDescriptor对象。
public FileReader(FileDescriptor fd) {
super(new FileInputStream(fd));
}
}
由java.io.FileReader
的源码可以看出,所谓的字符流的底层,竟然是用字节流实现的。
而且FileReader
继承自InputStreamReader
,并没有扩展任何功能,就连构造方法也是调用父类的。可见FileReader
对象其实是通过InputStreamReader
工作的。
InputStreamReader
源码注释:
package java.io;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import sun.nio.cs.StreamDecoder;
/**
* InputStreamReader 是字节流通向字符流的桥梁:它使用指定的字符集读取字节并将其解码为字符。
* 它使用的字符集可以由名称指定或显式给定,也可以使用平台默认的字符集。
* 每次调用 InputStreamReader 中的一个 read() 方法都会导致从底层输入流读取一个或多个字节。
* 要启用从字节到字符的有效转换,可以提前从流中读取更多字节。
* 为了达到最高效率,可要考虑在 BufferedReader 内包装 InputStreamReader。例如:
* BufferedReader in= new BufferedReader(new InputStreamReader(System.in));
*/
public class InputStreamReader extends Reader {
// 流解码器对象
private final StreamDecoder sd;
// 使用给定的字节输入流对象创建一个InputStreamReader对象(使用系统默认字符集)
public InputStreamReader(InputStream in) {
// 将传入的InputStream参数作为锁对象
super(in);
try {
// 使用系统默认字符集构建解码器对象(使用this作为锁对象)
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);
}
// 使用给定的字符集,创建流解码器对象
public InputStreamReader(InputStream in, Charset cs) {
super(in);
if (cs == null)
throw new NullPointerException("charset");
sd = StreamDecoder.forInputStreamReader(in, this, cs);
}
// 使用给定的字符集解码器对象,创建流解码器对象
public InputStreamReader(InputStream in, CharsetDecoder dec) {
super(in);
if (dec == null)
throw new NullPointerException("charset decoder");
sd = StreamDecoder.forInputStreamReader(in, this, dec);
}
// 返回此流使用的字符编码的名称。
public String getEncoding() {
return sd.getEncoding();
}
//读取单个字符
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);
}
// 判断此流是否已经准备好用于读取。
public boolean ready() throws IOException {
return sd.ready();
}
// 关闭该流并释放与之关联的所有资源。
public void close() throws IOException {
sd.close();
}
}
由java.io.InputStreamReader
的源码可以看出,其实工作的也不是它,而是sun.nio.cs.StreamDecoder
这个流解码器对象。
StreamDecoder
读取字符源码注释
package sun.nio.cs;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
public class StreamDecoder extends Reader {
// 流的关闭状态
private volatile boolean isOpen;
// 是否有剩余字符
private boolean haveLeftoverChar;
// 剩余的字符
private char leftoverChar;
// 读取单个字符,并返回其整数形式
public int read() throws IOException {
return this.read0();
}
private int read0() throws IOException {
// 获取锁对象
Object var1 = this.lock;
synchronized(this.lock) {
if (this.haveLeftoverChar) {
// 判断是否有剩余的字符。如果有就直接返回,不再读取
this.haveLeftoverChar = false;
return this.leftoverChar;
} else {
char[] var2 = new char[2];
// 一次性读取2个字符,并获取实际读到的字符个数
int var3 = this.read(var2, 0, 2);
switch(var3) {
case -1: //读到流末尾
return -1;
case 0:
default:
// 断言,返回-1
assert false : var3;
return -1;
case 2:
// 如果读取到2个字符,就把第二个字符赋值给leftoverChar,以便下次读取时直接返回
// 由于case2没有返回值,所以返回的是case 1中的 var2[0];
this.leftoverChar = var2[1];
this.haveLeftoverChar = true;
case 1:
// 如果只读取到1个字符,直接返回
return var2[0];
}
}
}
}
// 将字符读入数组中的某一部分。
public int read(char[] var1, int var2, int var3) throws IOException {
// 偏移量
int var4 = var2;
// 读取的个数
int var5 = var3;
Object var6 = this.lock;
synchronized(this.lock) {
// 判断流是否已经关闭
this.ensureOpen();
// 判断偏移量是否不小于0,并且不大于数组长度,并且读取的字符个数大于0....巴拉巴拉一堆
if (var4 >= 0 && var4 <= var1.length && var5 >= 0 && var4 + var5 <= var1.length && var4 + var5 >= 0) {
if (var5 == 0) {
return 0;
} else {
byte var7 = 0;
if (this.haveLeftoverChar) {
// 判断是否有剩余字符,如果有,就把剩余的字符放到数组中偏移量的位置上。
// 即,偏移的起始位置时剩余字符的位置。
var1[var4] = this.leftoverChar;
++var4;
--var5;
this.haveLeftoverChar = false;
var7 = 1;
if (var5 == 0 || !this.implReady()) { // 判断是否满足读取的个数,或者当前字符区是否还存在字符,或者该流是否已经关闭
return var7;
}
}
// 只读取一个字符
if (var5 == 1) {
// 读取一个
int var8 = this.read0();
if (var8 == -1) {
// 读取到流末尾
return var7 == 0 ? -1 : var7;
} else {
//向数组中存入读到的字符,并返回1
var1[var4] = (char)var8;
return var7 + 1;
}
} else {
// 读取多个字符(大于1),并返回读取到的字符个数
return var7 + this.implRead(var1, var4, var4 + var5);
}
}
} else {
throw new IndexOutOfBoundsException();
}
}
}
从以上的源码可以看出,read()
在API中说是读取单个字符,实际上是读取2个字符,但是只返回读取到的第一个字符。
简单的代码Demo演示
package com.hanyxx.io;
import java.io.FileReader;
import java.io.IOException;
/**
* 字符流(我是一个粉刷匠,粉刷本领强!)
* @author layman
*/
public class Demo04 {
private static FileReader fr ;
public static void main(String[] args) throws IOException {
char[] var2 = new char[13];
int offset = 1;
int length = 3;
//readSingleChar();
//readCharSJ(var2);
readCharSJByLength(var2,offset,length);
}
// 读取单个字符
public static void readSingleChar() throws IOException {
fr = new FileReader("layman.txt");
int read;
// 读取单个字符,并返回读取到的字符,以整数形式
while((read = fr.read()) != -1){
// 我是一个粉刷匠,粉刷本领强!
System.out.print((char) read);
}
fr.close();
}
// 将字符读入数组
public static void readCharSJ(char[] var2) throws IOException {
fr = new FileReader("layman.txt");
System.out.println("编码方式:" + fr.getEncoding());
int length;
// 将字符读入数组,并返回读取的字符个数
while((length = fr.read(var2)) != -1){
// 我是一个粉刷匠,粉刷本领强!
System.out.print(new String(var2,0,length));
}
fr.close();
}
// 将字符读入数组中的某一部分。
public static void readCharSJByLength(char[] var2,int offset,int length) throws IOException {
fr = new FileReader("layman.txt");
System.out.println("编码方式:"+fr.getEncoding());
int len;
// 将字符读入数组,并返回读取的字符个数
while((len = fr.read(var2,offset,length)) != -1){
System.out.print(new String(var2,0,len));
}
fr.close();
}
}
2.2 【字符输出流】Writer
java.io.Writer
是写入字符流的抽象类。是表示字符输出流的所有类的超类。它定义了字符输出流的基本共性功能方法。
子类必须实现的方法仅有write(char[], int, int)
、flush()
和 close()
。
但是,多数子类将重写此处定义的一些方法,以提供更高的效率和/或其他功能。
java.io.Writer
常用方法:
public abstract void close(): 关闭此流,但要先刷新它。
public abstract void flush() :刷新该流的缓冲。将缓冲区的字符立刻写入预期目标。
public void write(char[] cbuf) :写入字符数组。
public abstract void write(char[] cbuf, int off, int len) : 写入字符数组的某一部分。
public void write(int c) :写入单个字符。
java.io.Writer
源码注释
package java.io;
/**
* 写入字符流的抽象类。
* 子类必须实现的方法仅有 write(char[], int, int)、flush() 和 close()。
* 但是,多数子类将重写此处定义的一些方法,以提供更高的效率和/或其他功能。
*/
public abstract class Writer implements Appendable, Closeable, Flushable {
//用于保存字符串和单个字符的临时缓冲区
private char[] writeBuffer;
//缓冲区容量1024
private static final int WRITE_BUFFER_SIZE = 1024;
// 锁对象
protected Object lock;
// 空参构造
protected Writer() {
this.lock = this;
}
// 有参构造,指定锁对象
protected Writer(Object lock) {
if (lock == null) {
throw new NullPointerException();
}
this.lock = lock;
}
// 写入单个字符,只写入C的16个低阶位,剩余的16个高阶位将被舍弃
public void write(int c) throws IOException {
synchronized (lock) {
if (writeBuffer == null){
writeBuffer = new char[WRITE_BUFFER_SIZE];
}
writeBuffer[0] = (char) c;
write(writeBuffer, 0, 1);
}
}
// 写入字符数组
public void write(char cbuf[]) throws IOException {
write(cbuf, 0, cbuf.length);
}
// 子类必须重写该方法
abstract public void write(char cbuf[], int off, int len) throws IOException;
// 写一个字符串
public void write(String str) throws IOException {
write(str, 0, str.length());
}
// 写一个字符串的一部分
public void write(String str, int off, int len) throws IOException {
synchronized (lock) {
char cbuf[];
// 初始化字符数组,如果写入的字符个数比1024小,那么就初始化容量为1024的字符数组
// 如果写入的字符个数比1024大,就用写入的字符个数当作容量
if (len <= WRITE_BUFFER_SIZE) {
if (writeBuffer == null) {
writeBuffer = new char[WRITE_BUFFER_SIZE];
}
cbuf = writeBuffer;
} else {
cbuf = new char[len];
}
// 将字符从字符串复制到目标字符数组中。
str.getChars(off, (off + len), cbuf, 0);
write(cbuf, 0, len);
}
}
// 将指定字符序列添加到此 writer。底层调用的是write(String str)
public Writer append(CharSequence csq) throws IOException {
if (csq == null)
write("null");
else
write(csq.toString());
return this;
}
// 将指定字符序列的子序列添加到此 writer。底层调用的是write(String str)
public Writer append(CharSequence csq, int start, int end) throws IOException {
CharSequence cs = (csq == null ? "null" : csq);
write(cs.subSequence(start, end).toString());
return this;
}
//将指定字符添加到此 writer。底层调用的是write(int c)
public Writer append(char c) throws IOException {
write(c);
return this;
}
// 刷新流,并写入缓冲区的所有内容。(子类必须重写该方法)
abstract public void flush() throws IOException;
// 关闭流并释放资源
abstract public void close() throws IOException;
}
2.2.1 【文件字符输出流】FileWriter
java.io.FileWriter
是用来写入字符文件的便捷类。它只有五个构造方法。
public FileWriter(File file) :该构造方法等同于FileWriter(File file, false)
public FileWriter(File file, boolean append) :根据给定的 File 对象构造一个 FileWriter 对象。 如果第二个参数为 true,则是追加写入,若为false,则是覆盖写入。
public FileWriter(FileDescriptor fd): 构造与某个文件描述符相关联的 FileWriter 对象。
public FileWriter(String fileName) :该构造方法等同于FileWriter(String fileName, false)
public FileWriter(String fileName, boolean append) : 根据给定的文件名构造 FileWriter 对象。 如果第二个参数为 true,则是追加写入,若为false,则是覆盖写入。
java.io.FileWriter
源码
package java.io;
public class FileWriter extends OutputStreamWriter {
public FileWriter(String fileName, boolean append) throws IOException {
super(new FileOutputStream(fileName, append));
}
public FileWriter(File file) throws IOException {
super(new FileOutputStream(file));
}
public FileWriter(File file, boolean append) throws IOException {
super(new FileOutputStream(file, append));
}
public FileWriter(FileDescriptor fd) {
super(new FileOutputStream(fd));
}
}
由java.io.FileWriter
的源码可以看出,它的底层,也是用字节流实现的。
而且FileWriter
继承自OutputStreamWriter
,并没有扩展任何功能,就连构造方法也是调用父类的。可见FileWriter
对象其实是通过OutputStreamWriter
工作的。
java.io.OutputStreamWriter
源码注释
package java.io;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import sun.nio.cs.StreamEncoder;
/**
* OutputStreamWriter 是字符流通向字节流的桥梁:可使用指定的 charset 将要写入流中的字符编码成字节。
* 它使用的字符集可以由名称指定或显式给定,否则将接受平台默认的字符集。
* 每次调用 write() 方法都会导致在给定字符(或字符集)上调用编码转换器。
* 在写入底层输出流之前,得到的这些字节将在缓冲区中累积。可以指定此缓冲区的大小,
* 不过,默认的缓冲区对多数用途来说已足够大。注意,传递给 write() 方法的字符没有缓冲。
* 为了获得最高效率,可考虑将 OutputStreamWriter 包装到 BufferedWriter 中,以避免频繁调用转换器。
* 例如:Writer out = new BufferedWriter(new OutputStreamWriter(System.out));
*/
public class OutputStreamWriter extends Writer {
// 流编码器对象
private final StreamEncoder se;
// 使用指定的字符集名称构建OutputStreamWriter 对象
public OutputStreamWriter(OutputStream out, String charsetName)
throws UnsupportedEncodingException
{
super(out);
if (charsetName == null)
throw new NullPointerException("charsetName");
se = StreamEncoder.forOutputStreamWriter(out, this, charsetName);
}
// 使用系统默认字符集构建OutputStreamWriter 对象
public OutputStreamWriter(OutputStream out) {
super(out);
try {
se = StreamEncoder.forOutputStreamWriter(out, this, (String)null);
} catch (UnsupportedEncodingException e) {
throw new Error(e);
}
}
// 使用指定的字符集构建OutputStreamWriter 对象
public OutputStreamWriter(OutputStream out, Charset cs) {
super(out);
if (cs == null)
throw new NullPointerException("charset");
se = StreamEncoder.forOutputStreamWriter(out, this, cs);
}
// 使用指定的字符编码器构建OutputStreamWriter 对象
public OutputStreamWriter(OutputStream out, CharsetEncoder enc) {
super(out);
if (enc == null)
throw new NullPointerException("charset encoder");
se = StreamEncoder.forOutputStreamWriter(out, this, enc);
}
// 返回此流使用的字符编码的名称。
public String getEncoding() {
return se.getEncoding();
}
// 刷新缓冲区,并不刷新流本身。此方法可以被PrintStream调用。
void flushBuffer() throws IOException {
se.flushBuffer();
}
// 写单个字符
public void write(int c) throws IOException {
se.write(c);
}
// 写入字符数组的某一部分。
public void write(char cbuf[], int off, int len) throws IOException {
se.write(cbuf, off, len);
}
// 写字符串
public void write(String str, int off, int len) throws IOException {
se.write(str, off, len);
}
// 刷新流
public void flush() throws IOException {
se.flush();
}
// 关闭流并释放与之相关的系统资源
public void close() throws IOException {
se.close();
}
}
通过阅读源码可以得知:
-
在
OutputStreamWriter(文件字符输出流)
底层起作用的是StreamEncoder(流编码器)
对象。 -
在
InputStreamReader (文件字符输入流)
底层起作用的是StreamDecoder(流解码器)
对象。
字符输出流使用步骤
- 创建FileWriter对象,构造方法中绑定要写入数据的目的地。
- 使用FileWriter中的write方法,将数据写入到内存缓冲区中(字符转换为字节)。
- 使用FileWriter中的flush方法,将内存缓冲区中的数据,刷新到文件中
- 释放资源(会将内存缓冲区中的数据刷新到文件中)
Q:flush方法和close方法的区别
A:flush方法刷新缓冲区,流对象仍然可以使用。close方法刷新缓冲区,然后关闭流对象,不能再被使用了。
简单Demo演示
package com.hanyxx.io;
import java.io.FileWriter;
import java.io.IOException;
/**
* @author layman
*/
public class Demo05 {
private static FileWriter fw;
private static char[] chars = new char[]{'这','把','我','必','C'};
private static String str = "葫芦小金刚";
public static void main(String[] args) throws IOException {
//writeSingleChar();
//writeCharArray(chars);
//writeCharArray02(chars,1,3);
writeString(str);
}
// 写单个字符 write(int c) 底层实际上调用的是write(new char[]{(char)c}, 0, 1);
private static void writeSingleChar() throws IOException {
// 如果文件不存在,会创建该文件
fw = new FileWriter("layman01.txt");
char a = '哈';
fw.write(a);
fw.flush();
fw.close();
}
// 写字符数组
private static void writeCharArray(char[] chars) throws IOException {
fw = new FileWriter("layman01.txt");
fw.write(chars);
fw.flush();
fw.close();
}
// 写字符数组的一部分
private static void writeCharArray02(char[] chars,int offset,int length) throws IOException {
fw = new FileWriter("layman01.txt");
fw.write(chars,offset,length);
fw.flush();
fw.close();
}
//写字符串
private static void writeString(String str) throws IOException {
fw = new FileWriter("layman01.txt");
fw.write(str);
fw.flush();
fw.close();
}
}