在上一篇字符集详解中我们说到了产生乱码的原因,要么是读取数据时未读完整个汉字,要么是因为编码和解码的方式不统一,对于后者我们只需要规定好编码和解码的方式,而要解决读取数据时未读完整个汉字的问题,我们必须要有一个在遇到汉字时一次读取多个字节的方法,字符流应运而生。
其实,字符流的底层依然是字节流的方式,在读取英文等字符时一次读取一个字节,不同的是,当遇到中文时,会一次读取多个字节。我们已经学习了字节流中的文件 FileOutputStream 类和 FileInputStream 类,现在我们可以按照前面的方法继续学习。
首先,在字符流中有两个基本类,分别是字符输入流 Reader 类和字符输出流 Writer 类,这两个类都是抽象类,不能实例化对象,我们一般使用其子类创建对象完成数据的写出和读取。
我们可以使用字符输入流 FileReader 把本地文件中的数据读取到程序中,该类在使用时分为以下三个步骤:
示例:
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
public class Test {
public static void main(String[] args) throws IOException {
//创建流对象
FileReader fileReader = new FileReader("test.txt");
//读取数据
int b;
while((b=fileReader.read())!=-1){
System.out.print((char)b);
}
//释放资源
fileReader.close();
}
}
在 read() 方法中,程序会把读取到的数据进行解码并转换成 字符集表中对应的十进制数,其底层还是字节流的方式,只是在遇到中文时一次读取多个字节并解码转为字符集表中对应的十进制数。程序中的异常直接抛出即可。
同样,我们也可以往 read() 方法中传入一个数组,表示一次读取多个字符并存入数组中,与空参的 read() 方法不同的是,带参数的方法返回每次读取的数据个数,这个数字由数组的大小决定。读取到文件末尾时返回 -1。
示例:
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
public class Test {
public static void main(String[] args) throws IOException {
//创建流对象
FileReader fileReader = new FileReader("test.txt");
//读取数据
char[] chars = new char[2];
int len;
while((len=fileReader.read(chars))!=-1){
System.out.println(new String(chars,0,len));
}
//释放资源
fileReader.close();
}
}
读取数据是一个解码的过程。
使用字符输出流 FileWriter 往本地文件中写出数据有两种方式:
在创建 FileWriter 类对象时,参数既可以是 String 类型也可以是 File 类对象,如果传入String 类型,其底层同样会创建 File 类对象。写入数据时,如果文件不存在,则会创建文件并写入数据,但是要保证父级路径正确,如果文件存在,默认会清空 文件并写入数据,如果想要追加写入,则传入参数 true 。
示例:
import java.io.FileWriter;
import java.io.IOException;
public class Test {
public static void main(String[] args) throws IOException {
//创建流对象
FileWriter fileWriter = new FileWriter("test.txt",true);
String s="Java你好";
char[] chars=s.toCharArray();
//写入数据
fileWriter.write(97);//写出一个字符
fileWriter.write(s);//写出一个字符串
fileWriter.write(s,1,2);//写出一个字符串的一部分
fileWriter.write(chars);//写出一个字符数组
fileWriter.write(chars,2,3);//写出一个字符数组的一部分
//释放资源
fileWriter.close();
}
}
写出一个字符时,write() 方法传入的参数是要写入文件的字符在字符集中对应的十进制数,写出数据其实是一个编码的过程。
接下来我们一起探讨一下字符流的底层原理,
当我们创建一个文件字符输入流的对象时,其底层会在内存中创建一个缓冲区,该缓冲区是一个大小为 8192 的字节数组,在读取数据时,Java 首先会判断缓冲区中是否有数据,如果缓冲区中没有数据,则会从文件中读取数据,并且尽量充满缓冲区,如果缓冲区中有数据,则会从缓冲区中读取数据,直到读取完该区的数据,Java 会继续从文件中往缓冲区读取数据并且尽量充满缓冲区,直到读取到文件末尾,返回 -1。
使用Debug的方法查看缓冲区:
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
public class Test {
public static void main(String[] args) throws IOException {
//创建流对象
FileReader fileReader = new FileReader("test.txt");
//读取数据
fileReader.read();
//释放资源
fileReader.close();
}
}
同样的,在我们创建文件字符输出流的同时,其底层会在内存中创建一个大小为 8192 的缓冲区,该缓冲区是一个字节数组。在写出数据时,首先会把数据按照UTF-8 的编码方式编码,然后写到这个缓冲区中,那么什么时候数据会被写到文件中呢?
以下三种情况缓冲区数据会被写入文件:
我们下期见!