在 Java 的 I/O 体系中,字符流(Reader
和 Writer
)是专门用于处理文本数据的输入输出流。与字节流不同,字符流以字符为单位进行读取和写入,能够更好地处理文本信息,尤其是包含多字节字符(如中文)的文本文件。本文将从字符流的类关系图开始,详细介绍字符流的原理、使用方法以及常见问题。
1. 字符流与字节流的区别
字节流(InputStream
和 OutputStream
)以字节为单位进行读写,适用于处理二进制数据(如图片、音频、视频等)。而字符流(Reader
和 Writer
)以字符为单位进行读写,适用于处理文本数据。
示例:字节流读取中文乱码问题
/**
* 中文乱码问题
* @throws IOException
*/
private static void testChineseCharacterEncode() throws IOException {//FileInputStream为操作文件的字符输入流FileInputStream inputStream = new FileInputStream("JavaSE/resourses/a.txt");//内容为“沉默王二是傻 X”int len;while ((len=inputStream.read())!=-1){System.out.print((char)len);}
}
运行结果:
沉默王二是傻 X
出现乱码的原因是字节流无法正确处理多字节字符的编码问题。为了解决这个问题,可以使用字符流或者在字节流的基础上进行编码转换。
示例:字节流正确读取中文
/**
* 字节流正确读取中文
* @throws IOException
*/
private static void testCharacterCharacterEncode2() throws IOException {try (FileInputStream inputStream = new FileInputStream("JavaSE/resourses/a.txt")) {byte[] bytes = new byte[1024];int len;while ((len = inputStream.read(bytes)) != -1) {System.out.print(new String(bytes, 0, len));}}
}
通过 new String(byte bytes[], int offset, int length)
构造方法,Java 会根据默认的 UTF-8 编码将字节流转换为字符串,从而正确解码中文字符。
public String(byte bytes[], int offset, int length) {checkBounds(bytes, offset, length);this.value = StringCoding.decode(bytes, offset, length);
}
继续追看 StringCoding.decode() 方法调用的 defaultCharset() 方法,会发现默认编码是UTF-8,代码如下
public static Charset defaultCharset() {if (defaultCharset == null) {synchronized (Charset.class) {if (cs != null)defaultCharset = cs;elsedefaultCharset = forName("UTF-8");}}return defaultCharset;
}
static char[] decode(byte[] ba, int off, int len) {String csn = Charset.defaultCharset().name();try {// use charset name decode() variant which provides caching.return decode(csn, ba, off, len);} catch (UnsupportedEncodingException x) {warnUnsupportedCharset(csn);}
}
2. 字符流的基本概念
字符流 = 字节流 + 编码表
字符流的核心思想是将字节流与字符编码表结合起来,通过编码表将字节数据转换为字符数据。常见的字符编码包括 ASCII、ISO-8859-1、UTF-8、UTF-16 等。
3. 字符输入流(Reader)
java.io.Reader
是字符输入流的超类,定义了字符输入流的一些共性方法:
close()
:关闭流并释放相关资源。read()
:读取一个字符。read(char[] cbuf)
:读取多个字符并存储到字符数组中。
FileReader 是 Reader 的子类,用于从文件中读取字符数据。它的主要特点如下:
- 可以通过构造方法指定要读取的文件路径。
- 每次可以读取一个或多个字符。
- 可以读取 Unicode 字符集中的字符,通过指定字符编码来实现字符集的转换。
3.1 FileReader构造方法
FileReader(File file)
:创建一个新的FileReader
,参数为File
对象。FileReader(String fileName)
:创建一个新的FileReader
,参数为文件名。
// 使用File对象创建流对象
File file = new File("a.txt");
FileReader fr = new FileReader(file);// 使用文件名称创建流对象
FileReader fr = new FileReader("b.txt");
3.2 FileReader读取字符数据
- 读取字符:
read
方法,每次可以读取一个字符,返回读取的字符(转为 int 类型),当读取到文件末尾时,返回-1
。
FileReader fr = new FileReader("abc.txt");
int b;
while ((b = fr.read()) != -1) {System.out.println((char) b);
}
fr.close();
- 读取指定长度的字符:
read(char[] cbuf, int off, int len)
,并将其存储到字符数组中。其中,cbuf
表示存储读取结果的字符数组,off
表示存储结果的起始位置,len
表示要读取的字符数。
File textFile = new File("docs/约定.md");
try (FileReader reader = new FileReader(textFile)) {char[] buffer = new char[1024];int len;while ((len = reader.read(buffer, 0, buffer.length)) != -1) {System.out.print(new String(buffer, 0, len));}
}
4. 字符输出流(Writer)
java.io.Writer
是字符输出流的超类,定义了字符输出流的一些共性方法:
write(int c)
:写入单个字符。write(char[] cbuf)
:写入字符数组。write(char[] cbuf, int off, int len)
:写入字符数组的一部分。write(String str)
:写入字符串。write(String str, int off, int len)
:写入字符串的一部分。flush()
:刷新缓冲区。close()
:关闭流并刷新缓冲区。
FileWriter 是 Writer 的子类,用于将字符写入文件。
4.1 FileWriter构造方法
FileWriter(File file)
: 创建一个新的FileWriter
,参数为要读取的File对象。FileWriter(String fileName)
: 创建一个新的FileWriter
,参数为要读取的文件的名称。
4.2 FileWriter写入数据
- 写入字符:
write(int b)
方法,每次可以写出一个字符
FileWriter fw = new FileWriter("output.txt");
fw.write(72); // 写入字符'H'的ASCII码
fw.write(101); // 写入字符'e'的ASCII码
fw.write(108); // 写入字符'l'的ASCII码
fw.write(108); // 写入字符'l'的ASCII码
fw.write(111); // 写入字符'o'的ASCII码
fw.close();
- 写入字符数组:
write(char[] cbuf)
方法,将指定字符数组写入输出流。
FileWriter fw = new FileWriter("output.txt");
char[] chars = {'H', 'e', 'l', 'l', 'o'};
fw.write(chars); // 将字符数组写入文件
fw.close();
- 写入指定字符数组:
write(char[] cbuf, int off, int len)
方法,将指定字符数组的一部分写入输出流。
fw = new FileWriter("output.txt");char[] chars = {'H', 'e', 'l', 'l', 'o', ',', ' ', 'W', 'o', 'r', 'l', 'd', '!'};
fw.write(chars, 0, 5); // 将字符数组的前 5 个字符写入文件
- 写入字符串:
write(String str)
方法,将指定字符串写入输出流。
FileWriter fw = new FileWriter("output.txt");
String str = "沉默王二";
fw.write(str); // 将字符串写入文件
fw.close();
- 写入指定字符串:
write(String str, int off, int len)
方法,将指定字符串的一部分写入输出流。
String str = "沉默王二真的帅啊!";
try (FileWriter fw = new FileWriter("output.txt")) {fw.write(str, 0, 5); // 将字符串的前 5 个字符写入文件
} catch (IOException e) {e.printStackTrace();
}
5. 关闭与刷新
flush()
:刷新缓冲区,流对象可以继续使用。close()
:先刷新缓冲区,然后通知系统释放资源,流对象不可以再被使用。
FileWriter fw = new FileWriter("fw.txt");
fw.write('刷'); // 写出第1个字符
fw.flush();
fw.write('新'); // 继续写出第2个字符,写出成功
fw.close();
6. 续写与换行
续写和换行:操作类似于FileOutputStream操作,直接上代码:
// 使用文件名称创建流对象,可以续写数据
FileWriter fw = new FileWriter("JavaSE/resourses/fw.txt",true);
// 写出字符串
fw.write("沉默王二");
// 写出换行
fw.write("\r\n");
// 写出字符串
fw.write("是傻 X");
// 关闭资源
fw.close();
7. 文本文件复制
示例:使用 FileReader 和 FileWriter 复制文本文件
public class CopyFile {public static void main(String[] args) throws IOException {//创建输入流对象FileReader fr = new FileReader("JavaSE/resourses/a.txt");//创建输出流对象FileWriter fw = new FileWriter("JavaSE/resourses/copyaa.txt");/*创建输出流做的工作:* 1、调用系统资源创建了一个文件* 2、创建输出流对象* 3、把输出流对象指向文件* *///文本文件复制,一次读一个字符// copyMethod1(fr,fw);//文本文件复制,一次读一个字符数组copyMethod2(fr,fw);fr.close();fw.close();}/*** 文本文件复制,一次读一个字符* @param fr* @param fw* @throws IOException*/private static void copyMethod1(FileReader fr, FileWriter fw) throws IOException {int ch;while ((ch = fr.read()) != -1) {//读数据fw.write(ch);//写数据}fw.flush();}/*** 文本文件复制,一次读一个字符数组* @param fr* @param fw* @throws IOException*/private static void copyMethod2(FileReader fr, FileWriter fw) throws IOException {char[] chs = new char[1024];int len = 0;while ((len = fr.read(chs)) != -1) {//读数据fw.write(chs,0,len);//写数据}fw.flush();}
}
8. IO 异常处理
在实际开发中,建议使用 try...catch...finally
代码块处理异常,确保资源能够正确关闭。或者直接使用 try-with-resources
的方式。
示例:使用try…catch…finally 代码块处理异常
// 声明变量FileWriter fw = null;try {//创建流对象fw = new FileWriter("JavaSE/resourses/fw.txt");// 写出数据fw.write("二哥真的帅");} catch (IOException e) {e.printStackTrace();} finally {try {if (fw != null) {fw.close();}} catch (IOException e) {e.printStackTrace();}}
示例:使用 try-with-resources 处理异常
try (FileWriter fw = new FileWriter("fw.txt")) {fw.write("二哥真的帅");
} catch (IOException e) {e.printStackTrace();
}
9. 小结
字符流(Reader 和 Writer)是 Java I/O 中用于处理文本数据的抽象类。通过字符流,可以更方便地读取和写入字符数据,避免了字节流在处理多字节字符时的编码问题。常见的字符流子类包括 FileReader 和 FileWriter,它们分别用于从文件中读取和写入字符数据。在使用字符流时,需要注意字符编码的问题,并确保在操作完成后正确关闭流对象。
10.思维导图
11.参考链接
Java 字符流:Reader和Writer的故事