一、字符流产生的原因
如果通过已经学习的字节流,实现如下功能:
向文本中写入数字或英文字符,然后将其读入内存,并在控制台上显示。
向文本中写入中文字符,将其读入内存,并在控制台上显示。
是否两次都能正确的显示呢?
第一次能够正确显示,第二次会出现乱码。
通过刚刚的例子,得出结论: 在某些情况下,用字节流来操作中文不太方便。
核心原因在于:数据单位不一致。
那如何解决这个问题呢?
很显然,我们需要流中数据的逻辑单位不再是字节而是字符。因此,就产生了字符流。
由此我们就可以看到,字节流和字符流的区别:
两种流中数据的逻辑单位不同!
二、编码表
在正式开始学习字符流之前,我们首先得学习编码表相关知识。
1.字符在计算机中怎样表示?
字符在计算机中是以二进制数值的形式来存储和表示。
2.如何完成字符和其所对应的编码值之间的转化?
通过编码表。一个字符对应的数值是由编码表来规定的。
编码表:由字符及其对应的数值组成的一张表。是一个规定了字符和其数值对应关系的一个映射的集合。
3.对于字符数据最常见的操作——编解码操作
编解码操作(只针对字符及字符相关数据,比如单个字符,字符串)
- 编码:字符——>数值(通常是二进制)
把字符存储到计算机中的时候,就会对字符做编码得到字符对应的数值并存储。字符一定是根据某一个编码表(字符集)得到的数值。 - 解码:数值——>字符
知道数值根据某一个编码表(字符集)查找到数值对应的字符。
比如:当我们要输出一个字符的时候,就要根据指定字符集把数值转化为对应的字符显示出来。
注意:对同一个字符,编解码所使用的字符集一定要一致,以保证对字符编码后,通过解码能够得到原来被编码的字符,而不是其他字符。
所有的乱码问题,产生的核心原因都是编码和解码所使用的字符集不一致。
4.Unicode编码
Unicode:国际标准码,融合了多种文字。几乎包括了目前已知的所有字符,并且给每个字符分配了一个唯一的编码值。
Unicode编码以两个字节表示一个字符。
但是,Unicode编码表(字符集),只存在于逻辑中。因为Unicode编码表虽然规定了字符对应的唯一编码值,但是该编码值在存储到计算机中的时候,有不同的存储表示方式,所以出现了Unicode编码表的所谓变种,比如UTF-8,UTF-16。
UTF-8:可变长度来表示一个字符。
UTF-8不同,它定义了一种“区间规则”,这种规则可以和ASCII编码保持最大程度的兼容:
它将Unicode编码为00000000-0000007F的字符,用单个字节来表示(单个字节表示的就是ASCII表中所有的字符)。
它将Unicode编码为00000080-000007FF的字符用2个字节表示 。
它将Unicode编码为00000800-0000FFFF的字符用3字节表示 。
1字节 0xxxxxxx
2字节 110xxxxx 10xxxxxx
3字节 1110xxxx 10xxxxxx 10xxxxxx
UTF-16:用两个字节表示一个字符,和Unicode很像,但是有一些区别。
jvm内存使用的字符集是UTF-16。
5.常见编码表:
计算机只能识别二进制数据,早期由来是电信号。
为了方便应用计算机,让它可以识别各个国家的文字。
就将各个国家的文字用数字来表示,并一一对应,形成一张表。
- ASCII:美国标准信息交换码。用一个字节的7位可以表示。
00000000 - 01111111 128个字符。 - ISO8859-1:拉丁码表。欧洲码表。
几乎等价于latin-1。
用一个字节的8位表示。256个字符。 - 针对简体中文:
GB2312:中国的中文编码表。
GBK:中国的中文编码表升级,融合了更多的中文文字符号。
GB18030:GBK的取代版本。 - 针对繁体中文:
BIG-5码 :通行于台湾、香港地区的一个繁体字编码方案,俗称“大五码”。
6.Java语言层面的编解码:
只有字符数据才有编解码,对于byte,short,int,boolean等数据类型都不存在编解码。
编码:字符——>编码值(基于某个字符集)
解码:编码值——>字符(基于某个字符集)
java语言层面:
- 编码:对一个字符串编码 字符串——>多个字节值
String类:byte[ ] getBytes(String charsetName)
charsetName指定编码所使用的字符集 - 解码:对于一个字节数组 字节数组——>字符串
String(byte[ ],int offfset,int len,String charsetName)
charsetName:指定解码时使用的字符集的名称
在学习编码之前,我们已经做过编解码了
编码:byte[ ] getBytes()
解码:String(byte[ ],int offfset,int len)
默认字符集的概念:从开发者的角度:当我们自己编解码的时候,如果不指定字符集,编解码默认使用的字符集,即默认字符集。
1)在IDEA中,默认字符集是UTF-8(IDEA其实是设置过的)。
jvm的默认字符集是UTF-16。
2)jdk默认使用的默认字符集,是GBK,这个默认字符集和操作系统有关。
常识:gbk字符集中两个字节表示一个中文字符
UTF-8字符集中,中文字符通常用三个字节表示一个中文字符,也有四个字节表示一个中文字符。
三、什么是字符流
通过编码表相关知识我们可以得出以下结论:
字符的存储和传输天然与二进制数据密切相关
字符流需要在二进制的基础上,添加基于特定编码表的字符编解码
因此我们得出结论:
字符流 = 字节流 + 编码表(根据指定编码表,编解码的过程)
四、转换流
接下来,我们利用Writer向文本中写入中文字符串。
但是考虑到Writer是抽象类,无法直接实例化,于是我们使用其子类OutputStreamWriter间接实例化。
完成以上功能,做两件事:
1.创建Writer对象
OutputStreamWriter的构造方法:
- public OutputStreamWriter(OutputStream out)
创建使用默认字符编码 (默认字符集,在IDEA UTF-8) 的OutputStreamWriter。 - public OutputStreamWriter(OutputStream out,String charsetName)
创建使用指定字符集的 OutputStreamWriter。
2.通过字符输出流的写方法(write),真正完成数据传输
Writer写字符数据的方法:
- public void write(int c)
写入单个字符。要写入的字符包含在给定整数值的 16 个低位中,16 高位被忽略。 - public void write(char[] cbuf)
写入字符数组。 - public void write(char[] cbuf,int off,int len)
写入字符数组的某一部分。 - public void write(String str)
写入字符串。 - public void write(String str,int off,int len)
写入字符串的某一部分。
字符流操作要注意的问题
1.对于字符流而言,字符流自带小缓冲区(为了实现编解码功能).
void flush()
刷新该流的缓冲。如果该流已保存缓冲区中各种 write() 方法的所有字符,则立即将它们写入预期目标。
2.void close()
关闭此流。close()方法会先使用flush()刷新该流的缓冲。
接着,我们使用Reader,将刚刚写入文本的中文数据,读取到内存中并显示。但是考虑到,Reader是抽象类,我们只能使用其子类InputStreamReader。
1.InputStreamReader的构造方法:
- public InputStreamReader(InputStream in)
创建一个使用默认字符集的 InputStreamReader。 - public InputStreamReader(InputStream in,String charsetName)
创建使用指定字符集的 InputStreamReader。
2.Reader读数据的方法:
- public int read()
读取单个字符
返回值:
a.作为整数读取的字符,范围在 0 到 65535 之间 (0x00-0xffff)
b.如果已到达流的末尾,则返回 -1 - public int read(char[] cbuf)
将字符读入数组
返回值:
a.读取的字符数
b.如果已到达流的末尾,则返回 -1 - public int read(char[] cbuf,int off ,int len)
将字符读入数组的某一部分
cbuf - 目标缓冲区
off - 开始存储字符处的偏移量
len - 要读取的最多字符数
返回值:
a.读取的字符数
b.如果已到达流的末尾,则返回 -1
练习:把当前项目目录下的图片和视频内容复制到指定目录下,能否复制成功?
不能,这种非文本数据,只能用字节流传输。
对于图片和视频数据,它们都有自己特殊的编码格式。它们所使用的编码格式和基于字符集对字符数据进行的编解码没有任何关系。
所以,当我们使用字符输入流来读取图片和视频数据的时候,当字符输入流试图对图片和视频数据,进行基于字符集进行解码的时候,会发现有一些二进制数值,无法对应到字符集中的字符(遇到不认识的字符)。此时,字符输入流,要么丢弃这些不认识码值,要么把这些没有在字符集中匹配到的编码值替换成特殊编号对应的字符 ???。
其实,这意味着,字符输入流,在读取视频或图片数据的时候,就已经修改了原来的视频和图片。
五、转换流的简化
1.在使用转化流的时候,通常需要经过2步:
a.创建底层的字节流
b.包装字节流,指定字符集并完成转换流的创建
稍显麻烦,而大多数时候,我们所依赖的字符集都是本地字符集,所以,为了简化我们的书写,转换流提供了对应的子类。
2.构造方法:
FileWriter
- public FileWriter(String fileName)
fileName代表目标文件的路径名字符串。 - public FileWriter(String fileName, boolean append)
fileName代表目标文件的路径名字符串, append如果为true就可以实现字符输出流对文本文件的追加写入。
FileReader
- public FileReader(String fileName)
fileName 目标文件的路径名字符串 - public FileReader(File file)
file 目标文件的File对象
转化流 VS (FileReader 和 FileWriter)
a.从创建流对象的角度: FileReader和FileWriter更加的方便。
b.从指定编解码字符集的角度: FileReader和FileWriter无法指定编解码使用的字符集,但是转化流可以。
六、字符缓冲流
同在字节流中引入缓冲流的原因相同,出于效率的考虑,在字符流中,我们同样引入缓冲流。缓冲字符流中定义了,缓冲流子类独有的方法。
构造方法:
- BufferedWriter(Writer out)
创建一个使用默认大小输出缓冲区的缓冲字符输出流。 - BufferedReader(Reader in)
创建一个使用默认大小输入缓冲区的缓冲字符输入流。
缓冲流子类自己定义的子类特有的方法:
- BufferedWriter
void newLine() // 在一行字符串之后,添加一个系统相关的换行符 - BufferedReader
String readLine() //读取一个文本行,包含该行内容的字符串,不包含任何行终止符,如果已到达流末尾,则返回null
七、标准输入输出流
标准输入/输出流
System类中的字段in 输入流
代表了系统的标准输入,默认的输入设备是键盘
System.in的类型是InputStream
System类中的字段out
代表了系统的标准输出,默认的输出设备是显示器(控制台窗口)
System.out的类型是PrintStream是OutputStream的子类
#个人学习记录,如发现有错误之处,欢迎与我交流