字符流
前面针对字节流和字节缓存流做了一个比较全面的探索。字节流以字节(8bit)为单位读取数据,且可以处理所有的数据,包括文本,音频等,这里就要抛出一个问题了,既然字节流这么方便,只是读取数据比较麻烦,那我们完全可以包装字节流进行快速的一些处理,为什么还要衍生出字符流?
字符流的优势
- 字符流提供了针对字符类型的数据的很好的API操作
- 字节流读取中文的时候,存在乱码的情况
- 字符流会使用缓冲区
字符流的API之后详细去看,先看一下中文乱码和缓冲区这两个。首先,字节流读取中文的时候为什么可能出现中文乱码的情况。有以下两种原因:
- 我们数组转String是有默认的charset(“UTF-8”),由于中文在不同的编码中对应的字节以及长度都是不一样的,如若文件格式和转换所使用的编码不一致,就会出现乱码的情况,比如
@Test
public void ioDemo1() {
File file = new File("D:" + File.separator + "a.txt");
if (file.exists()) {
log.info(file.getAbsolutePath());
InputStream inputStream = null;
try {
inputStream = new FileInputStream(file);
inputStream = new FileInputStream("D:\\a.txt");
new BufferedInputStream(inputStream);
int n = 0;
byte[] bytes =new byte[1024];
StringBuilder sb = new StringBuilder();
while ((n = inputStream.read(bytes)) != -1) {
sb.append(new String(bytes, 0, n));
}
System.out.print(sb.toString());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
我们a.txt里面内容为 ‘’aaa中‘’,数组大小为1024,一次可以全部读取完,但是,由于我们文件使用的是GBK的编码,但是转换String使用的默认的utf-8的编码,导致输出是乱码的aaa�й�
。
- 还有一种可能,我们这里将文件改为UTF-8格式的,这样格式一致了,但是我们将数组大小改为4:
:byte[] bytes =new byte[4];
我们运行发现还是乱码:aaa�����
。这里是什么原因呢?正如上面说的,字节流读取的时候,是以字节(8bit)为单位读取数据,英文字母和数字都是一个字节,而中文在UTF-8中是占三个字节的,而字节流读取数据的时候,,如果刚好读取到某个中文的第一个字节或者第二个字节,然后数组满了,返回了,这个时候,就会出现乱码。
既然我们知道字节流读取会出现乱码的情况,并且知道原因,那怎么解决呢?其实很简单,读取的io流和转换为String的Charset保持一致,然后,根据各个charset针对中文的定义,判断是否读取到中文,如果是,根据规则将后面的剩余部分找出来一起拼凑:
{
inputStream = new FileInputStream(file);
inputStream = new FileInputStream("D:\\a.txt");
new BufferedInputStream(inputStream);
int n = 0;
byte[] bytes =new byte[4];
StringBuilder sb = new StringBuilder();
while ((n = inputStream.read(bytes)) != -1) {
//判断最后一位是否是中文一部分
int a = 0;
if(n == bytes.length && bytes[bytes.length - 1] < 0){
//判断缺几个字节
for(int i = 0; i< bytes.length; i++){
if(bytes[i] < 0){
a++;
}
}
}
a = a% 3;
if(a > 0){
byte[] bytes1 =new byte[3 - a];
for(int i = 0; i< (3 - a); i++){
bytes1[i] = (byte)inputStream.read();
}
byte[] bytes2 = Arrays.copyOfRange(bytes, n -a, n);
byte[] bytes3 = ArrayUtils.addAll(bytes2, bytes1);
//无需拼接的部分
sb.append(new String(bytes, 0, n -a));
//拼接的中文
sb.append(new String(bytes3));
}else{
sb.append(new String(bytes, 0, n));
}
}
System.out.print(sb.toString());
}
整个实现代码还是比较冗余的,所以字符流应运而生,对于文本和字符数据的处理,建议使用字符流,一来不存在乱码的问题,二来使用缓冲区,效率高,三来也有比较多简洁便捷的API。缓冲区也是我们一直提到的一个知识点,其实缓冲区是固定长度的数据容器,也是内存中的一部分。有一个单独的缓冲区类型来保存每种类型的基本值的数据,除了布尔类型值。比如创建一个8个字节的缓冲区:ByteBuffer bb = ByteBuffer.allocate(8);
,缓冲区和数组很类似,有容量,有position(指针),只是它内部有一些自己针对读和写的处理方法。
基础的字符流
和字节流一样,Reader 是所有的输入字符流的父类,它是一个抽象类,是将磁盘(文件)中的数据读入内存中。Writer:是所有的输出字符流的父类,它是一个抽象类,是将内存中的数据按照字符形式写入磁盘(文件)中。
字符流的构造方法以及基类中的对于数据的读取基本和字节流一致,具体的构造函数和API可以看一篇文章的介绍,或者自行阅读,比较简单,代码的写法也和字节流相似:
{
File file = new File("D:" + File.separator + "a.txt");
if (file.exists()) {
FileReader fileReader = null;
try {
fileReader = new FileReader(file);
char [] ch=new char[1024];
int len=0;
while((len=fileReader.read(ch))!=-1){
System.out.print(new String(ch,0,len));
}
fileReader.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
}
catch (IOException e) {
e.printStackTrace();
}
}
}
字符缓冲流
-
BufferedReader:字符输入缓冲流,从字符输入流中读取文本,缓冲各个字符,从而实现字符、数组和行的高效读取。BufferedReader 由Reader类扩展而来,也是装饰者模式的典型应用,BufferedReader 的构造函数需要传一个reader进来:
public BufferedReader(Reader in) { this(in, defaultCharBufferSize); }
,后面那个表示的是默认的缓冲区的大小(8192)。他提供了一个readLine的逐行读取文本的方法:String readLine(boolean ignoreLF) throws IOException
,对于文本数据和字符数据的处理很方便。 -
BufferedWriter:字符输出缓冲流,原理和BufferedReader类似。 该类提供了 newLine() 方法,与BufferedReader的readLine呼应,它使用平台自己的行分隔符概念(由系统属性 line.separator 定义)。其实也是对于write方法的一次封装。
字符缓冲流对于我们工作中与文本或者字符数据打交道,对于他们的处理有一个很好的帮助,能够大大简化我们的代码量,例如,这里用readLine和newLine方法去实现上一次的文件的copy,代码会很简洁:
@Test
public void ioDemo3() {
File file = new File("D:" + File.separator + "a.txt");
if (file.exists()) {
BufferedReader bufferedReader = null;
BufferedWriter bufferedWriter = null;
try {
bufferedReader = new BufferedReader(new FileReader("D:\\a.txt"));
bufferedWriter = new BufferedWriter(new FileWriter("E:\\a.txt"));
String str=null;
while((str=bufferedReader.readLine())!=null){
bufferedWriter.write(str);
bufferedWriter.newLine();//写入一个行分隔符。行分隔符字符串由系统属性 line.separator 定义,并且不一定是单个新行 ('\n') 符。
bufferedWriter.flush();//缓冲输出流一定要flush
}
bufferedWriter.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
}
catch (IOException e) {
e.printStackTrace();
}finally {
try {
bufferedWriter.close();
bufferedWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
至此,字符流我们基本有了一个了解,对于其用法和常用的API也有了运用,对于长期与磁盘,文件打交道的程序员来说,这些知识虽然很基础,但是也是必不可少的。下一次温习,我们将针对遗留的读取数据的pos,也就是指针和reset,mark这些方法做一些学习和分享。