总结:本文用比较浅显的语言来解释字符流、字节流、以及编码和解码。
计算机中,无论是文本、图片还是音视频,所有的文件都是以二进制形式(字节)存在。这句话看起来好像没什么用,但其实蕴含的信息有助于我们理解字节流、字符流、编解码等内容。例如一个视频文件(假定文件名是a.mp4),这个文件躺在你的硬盘中。那么它其实是以二进制形式存在,说的简单些就是0和1的组合。
在电脑里面: a.mp4 <----等同于------> 0100010001000...
但是我们用播放器(软件)打开这个视频,我们看到的不是01000100这样的冷冰冰的数字,而是图像和视频。这是因为播放器读取这个文件,并将其转化为我们肉眼看到的视频。文本也是同样的道理,假如有一个b.txt文件,我们使用软件打开以后,看到的是字符,而不是01000100这样的数字。这其实就是因为软件对文件进行了读取,并且进行了解码的过程。
接下来,还是举文本为例。我们将b.txt的内容清空然后重新输入内容。这个时候,可以理解成我们通过键盘输入了一些字符串。那么软件又会对我们输入的内容进行编码,也即是说将我们输入的内容转换成010000100010格式的文件。这再次印证上面所说的内容——所有的文件都是以二进制形式(字节)存在(涉及用户输入输出的内容见下文2.2)。
2.1 不涉及用户输入的文件操作
2.1.1 字节流
当操作不涉及用户的输入来操作文件(例如就想将从a.mp4复制出一个aa.mp4)时,使用字节流和字符流的原理都很好理解。
inputstream和outputstream(输入和输出流)就像是一个人的左右手。左手是inputstream,用于拿原始文件的内容,而outputstream就像是右手,将左手传递来的东西放入到右边。这样就实现将物件从左放到右(也就是实现了数据的传输)。使用字节流的方式进行操作时,“左手”(FileInputstream)先从文件中“拿取”原始的01010格式的内容,然后"右手"(FileOutputStream)原封不动将这些信息进行传递。
最原始的方法就是左手拿一个,然后交给右手,然后左手再拿一个,再给右手,这样一个一个字节的读取,但是速度很慢。所以可以人为在程序中设置缓冲区的大小,就好像在两边都增加一个箱子。等左边箱子装满了转移到右边。那么转移的次数就大大减少了,需要的时间也就缩短了。更便捷的做法是直接使用字节缓冲流(BufferedInputStream),它自身定义了一个大小为8192的字节数组作为缓冲区,无需自己再设置缓冲区大小。
2.1.2 字符流
接下来我们换一个工人,他的左手和右手分别对应字符流FileReader和FileWriter。虽然说是字符流,但是左手拿到的仍旧是010100形式的字节。由于右手只能够传递字节,这时候就需要在程序中手动设置,将左手拿到的字节转化成字符。(后面会有其他的字符流可以简化这个工作,甚至像bufferedReader之类的包装流可以直接读取字符格式,不需要再转化成字符流)。
从上面来看,字节流的操作明显要简单,而且还适用于音频、视频等多种格式的文件。而字符流应用于文本文件。那为什么还要字符流呢。注意,这是因为这里没有涉及用户输入等操作。
注:
经过装饰设计模式后的 BufferReader类,就有了readLine方法,这个方法就能够读取整行,并且返回的还是String格式。所以相应的其判断到文件末尾的方法也由上面的while(len=in.read()!= -1)变成了while((str=br.readLine()) != null)。也即是说,原来的bufferedInputStream文件拷贝过程是纯字节的过程,左手拿到的是二进制数组,右手出去的还是二进制数组(缓冲区内容就是二进制数组,默认8192个字节数组)。而现在bufferedreader是纯字节的过程。左手拿到的是字符串,右手出去的还是字符串。缓冲区内容现在不再是固定长度的字符串,而被规定为一行(这个说法不准确,可能本身也有固定的缓冲区)。
可见,其实bufferedreader是专门针对文本文档设计的。而Bufferedinputstream的适用范围要更广,不仅可以用于文本,还可以用于图片、视频等各种文件。
2.2 涉及用户输入输出的操作
当涉及用户输入输出时,情况就不一样了。举个例子,复制文件内容并且在cmd窗口打印出来,以String格式显示给用户,这就涉及用户输出。而当我们编辑文本并保存的时候,我们输入的是String格式的内容,但是文本最终需要保存为二进制文件(还是上面所说的,所有文件都以二进制形式存在)。这又涉及用户输入的问题。
例如当我们想将String格式的内容(例如下面的"what are you doing")来写入文件时,如果使用字节输出流FileOutputStream的话需要先将字符串转换为字节(二进制)数组,然后再进行写入。
String str = “what are you doing”;
byte[] b = str.getBytes();
for (int i=0;i<b.length;i++){
out.write(b[i])
}
而字符流不需要这么麻烦,同样上面的效果,只需要:
String str = “what are you doing”;
writer.write(str);
(当然,别忘了文件都是以二进制格式存在的,所以这里字符-->字节的过程只是在程序中被“省略”了而已)
2.3 编码与解码
我们用播放器或者文本打开一个文件,那么究竟这个软件做了什么,播放器读取这个文件,其实就是通过io输入流读取文本的字节(01001格式的数据),然后按照不同的“分割方式”进行解码。如果源文件是用utf-8编码的,那么就应该用utf-8进行解码,最后显示在我们面前的就是正常的字符。如果你用不匹配的解码方式,那么就可能变成乱码。但是你关闭了这个文件以后它还是不变的,因为本质上还是那些个字节(就好像你有很多把钥匙,但是只有一两把才能正确打开门,这里的锁和钥匙就对应编码与解码方式)。
如果我们使用某款文本编辑器对这个文件进行编辑。这时候就涉及到编码的问题了。还是那句话,计算机中所有的文件都必须以二进制形式存在。而文本编辑器读取的是我们输入的字符(String格式),为了转成二进制,就会以某种编码方式进行保存。
再来联想我遇到过的乱码情况,我创建了某个网站应用。用浏览器打开以后网页出现乱码,不能正常显示中文,这时候是什么问题呢。这时候应该就是浏览器的解码方式和文本编辑器的编码方式(假定我们通过某款编辑器编辑应用文件)不一致的问题。例如文本编辑器的编码方式为utf-8,而浏览器的解码方式不匹配所导致。
例如某个java文件,可以用sublime和文本编辑器正常显示,但是用Eclipse就出现乱码现象。
这就是因为这个java文件本身是以某种编码(例如GBK)方式保存成0100010的二进制文件的,而我们用新的文本编辑器打开就是“将这些二进制数字进行解码”。但是原来GBK的如果用utf-8来进行解码就无法正常显示了。
文本内容----->被保存为二进制文件----->我们用不同的软件打开------>解码方式不同,显示错误。
现在我们用一个笨一点的方法,在中间多加几个步骤:
文本内容----->被保存为二进制文件----->先用可以正常显示的文本编辑器打开,这时候又转成字符----->将能够正常显示字符的这个文件用不同编码(例如utf-8)方式另存为新的文档----->我们用不同的软件打开------>解码方式相同,显示正常。
用这个新的文件替换掉原来的文件,问题解决。