IO
编码与乱码
编码基础
计算机存储的信息都是用二进制表示的,我们在屏幕上看到的英文、汉字、符号都是二进制数转换后的结果,按照某种规则,将字符转换成二进制存储到计算机中,称为编码。反之,将存储在计算机中的二进制按照某种规则进行解析出来,称为解码。按照A规则进行编码,就必须按照A规则进行解码,这样才能正确的显示文本,否者就会出现乱码的现象。
字符编码
一套自然语言的字符与二进制之间的对应的规则,主要分为Unicode编码和非Unicode编码。
非Unicode编码
ASCII码-拉丁字符
世界上虽然有各种各样的字符,但计算机发明之处并没有考虑那么多,基本上只考虑了美国的需求。美国大概需要128个字符,所以就规定了128个字符的二进制表示,这个方法是一个标准,称为ASCII编码,即美国信息交换标准代码。
128个字符刚好可以用7位表示,计算机最小的存储单位是byte
字节,即8位,ASCII码中最高位为0,使用剩下的7位来表示128个字符,这7位可以看作数字0-127,ASCII规定了0-127中每个数字代表的含义,其中数字32-126表示的字符都是可以打印字符。0-31和127表示的都是不可打印字符。这些字符一般用于控制目的,大部分都是不常用的。
ASCII小结
- 有128个字符,一个字节占8位所以使用一个字节表示字符
- 最高位使用0表示,使用剩下的7位表示128个字符
- 其中32-126表示的字符都是可打印字符,0-31和127表示的都是不可打印字符
ISO 8859-1-拉丁字符
ISO-8859-1又称Latin-1
,它也是使用一个字节表示一个字符,其中0-127与ASCII一样,128-256又规定了不同的含义在128-256中,128-159表示一些控制字符,这些字符不常用,160-255表示西欧字符。
ISO-8859-1虽然号称是标准,用于西欧国家,但它连欧元符号都没有,因为欧元比较晚,而标准比较早。实际运用更广泛的是Windows-1252
编码,这个编码和ISO 8859-1基本是一样的,区别只在于数字128-159。Windows-1252使用其中的一些数字表示可打印字符,这个编码加入了欧元符号以及一些其他常用的字符。基本上Windows-1252已经取代了ISO 8859-1,在很多应用程序中,即使文件声明为ISO 8859-1解析的时候依然会被当做Windows-1252编码。
ISO 8859-1小结
- ISO 8859-1又称Latin-1,使用一个字节表示一个字符,其中0-127与ASCII一样
- 在ISO 8859-1中,128-159表示一些控制字符,160-255表示西欧字符
- 由于ISO 8859-1不支持欧元符号,实际运用更广泛的是Windows-1252编码
- ISO 8859-1与Windows-1252的区别在于数字128-159,Windows-1252表示可打印字符,加入了欧元符号以及一些常用的字符。
- 在很多应用程序中,即使文件声明为ISO 8859-1解析的时候依然会被当做Windows-1252编码。
扩展ASCII码 ,西欧编码
GB2312
美国和西欧字符使用一个字节表示就够了,但中文明显不够,中文字符集的第一个标准是GB2312,GB2312针对的是常用的中文字符,GB2312固定使用2个字节表示汉字,在这两个字节中,最高位都是1,如果是0,就会被当作ASCII码来解析。GB2312中,高位字节范围是0xA1-0xF7
,低字节范围是0xA1-0xFE
。
GB2312小结
GBK
GBK建立在GB2312基础上,向下兼容GB2312,也就是说GB2312编码的字符和二进制表示,在GBK编码中是完全一样的。GBK增加了许多汉字,其中包括繁体。
GBK同样使用固定的两个字节表示字符,其中高位字节范围是0x81-0xFE
,低位字节范围是0x40-0x7E
和0x80-0xFE
。
需要注意的是,地位字节是从0x40
开始的也就是64开始的,也就是说低位字节的最高位可能为0,那怎么知道它是汉字的一部分,还是ASCII码呢?其实很简单,因为汉字是用固定两个字节表示的,在解析二进制的时候,如果第一个字节的最高位为1(ASCII码最高位为0,使用剩下7位表示128个字符包括0),那么就将下一个字节读取进来解析为一个汉字,而不用考虑它的最高位,解析完后,跳到第三个字节继续接续。
GBK小结
GB18030
GB18030向下兼容GBK,增加了很多字符,包括了少数名族字符,以及一些中日韩统一字符。
用两个字节已经表示不了GB18030中的所有字符,GB18030使用变长编码,有的字符是两个字节,有的字符是四个字节。在两个字节编码中,字节表示范围与GBK一样。在四个字节范围中,第一个字节的值为0x81-0xFE
,第二个字节的值为0x30-0x39
,第三个字节的值为0x81-oxFE
,第四个字节的值为0x30-0x39
。
解析二进制的时候,如何知道是按两个字节解析还是四个字节解析呢?看第二个字节的范围,如果是0x30-0x39
就是四个字节表示,因为两个字节编码中第二个字节都比这个大。
GB18030小结
Big5
Big5是针对繁体中文的,广泛用于台湾和香港等地区。Big5包括很多繁体,和GB2312类似,一个字符固定使用两个字节表示。在这两个字节中,高位字节范围是0x81-0xFE
,低位字节范围是0x40-0x7E
和0xA1-0xFE
。
编码汇总
- ASCII码基础,使用一个字节表示,最高位设为0,其他7为表示128个字符。其他编码都兼容ASCII,最高位使用1来区分。
- 西欧主要使用Windows-1252,使用一个字节表示,增加了额外128个字符。
- 我国内地的三个主要编码GB2312、GBK、GB18030有时间先后关系,表示的字符越来越多,且后面的字符集都完全兼容前面的字符集,GB2312和GBK都是用两个字节表示,而GB18030使用变长字节表示,使用两个字节或四个字节。
- 香港和台湾等地区主要使用Big5编码。
- 如果文本中的字符都是ASCII字符,那么采用上面任意一种编码解析结果都是一样的。
Uniocde编码
每个国家的计算机厂商都对自己国家的字符进行编码,在编码的时候基本忽略了其他国家的字符和编码,甚至忽略了同一国家的其他计算机厂商,这样造成的结果是,出现太多编码,且互不兼容。
Unicode做了一件事,就是给世界上所有字符都分配了一个唯一的字符编号,这个编号从0x000000
-0x10FFFF
,包括110多万。但大部分常用字符都在0x000000
-0xFFFF
之间,即65535个数字之内(两个字节表示16位,2的17次方-1)。每个字符都有一个Unicode编号,这个编号一般写成16进制,在前面加上U+
。大部分中文字符的编号范围为U+4E00-U+9FFF
,例如,”马”的Unicode是”U+9A6C”。
简单理解,Unicode做了一件事,就是给所有字符分配了一个唯一的数字编号。它并没有规定字符对应的二进制怎么表示,这与上面介绍的其他编码不同,其他编码都即规定了能表示那些字符,又规定了每个字符对应的二进制表示,而Unicode本身只规定了每个字符的数字编号。
编号对应到二进制表示方案有三种:UTF-8
、UTF-16
、UTF-32
。
UTF-32
就是字符编号的整数二进制表示,占用4个字节。
但有一个细节,就是字符的排列顺序,如果第一个字节是整数二进制的最高位,最后一个字节是整数二进制的最低位,那这种字节序就叫”大端”(Big Ending BE),否则就叫”小端”(Letter Ending LE)。对应的编码方式分别是UTF-32
和UTF-32LE
。
每个字符都用四个字节表示,非常浪费空间,实际采用的也较少。
UTF-16
UTF-16使用变长字节表示。
对于编号在U+0000-U+FFFF
的字符(65535个数字之内,常用字符集),直接使用两个字节表示。需要说明的是,U+D800-U+DBFF
的编号其实是没有定义的。
字符集在U+10000-U+10FFFF
的字符(也叫增补字符集),需要4个字节表示。前面两个字节叫高代理项,范围是U+D800-U+DBFF
;后面两个字节叫低代理项,范围是U+DC00-U+DFFF
。数字编号和这个二进制之间有一个转换算法,区分是两个字节还是四个字节表示一个字符就看前面两个字节的编号范围。如果是U+D800-U+DBFF
,就是四个字节,否则就是两个字节。UTF-16和UTF-32一样也有字节序问题,如果高位存放在前面就叫大端(BE),编码就叫UTF-16BE
,否则就叫小端,编码就叫UTF-16LE。UTF-16常用于系统内部编码,UTF-16比UTF-32节省了很多空间,但是任何一个字符都至少两个字节表示,就美国和西欧而言,还是比较浪费。
UTF-8
UTF-8使用变长字节表示,每个字节使用的字节个数与其Unicode编号大小有关,编号小的使用的字节就少,编号大的使用的字节就多,使用的字节个数为1-4个不等。
编号范围 | 二进制格式 |
---|---|
0x00-0x7F(0-127) | 0xxxxxxx 一个字节 |
0x80-0x7FF(128-2047) | 110xxxxx 10xxxxxx |
0x800-0xFFFF(2048-655535) | 1110xxxxx 10xxxxxx 10xxxxxx |
0x10000-0x10FFFF(655535以上) | 11110xxxxx 10xxxxxx 10xxxxxx 10xxxxxx |
x表示可以用的二进制位,而每个字节开头的0或1是固定的。
小于128的,编码与ASCII码一样,最高位为0。其他编码的第一个字节都有特殊含义,最高位有几个连续的1就表示用几个字节表示,而其他的字节都已10开头。
对于一个Unicode编号,具体怎么编码呢?首先将其看作整数,转化为二进制形式(去掉最高位的0),然后将二进制从右至左依次填入对应的二进制格式x中,填完后,如果对应的二进制格式还没有填的x,则设为0。
我们来看一个例子,”马”的Unicode编号是”0x9A6C”,整数编号是39532,其对应的UTF-8的二进制格式是:1110xxxxx 10xxxxxx 10xxxxxx
。整数39532的二进制格式是1001 101001 10100
将这个二进制从右至左一次填入二进制格式中,结果就是UTF-8
编码11101001 10101001 1010100
十六进制表示为0xE9A9AC
。和UTF-16/UTF-32
不同,UTF-8是兼容ASCII码的,对大部分中文而言,一个中文汉字需要用三个字节表示。
Unicode小结
Unicode给世界上所有字符分配了一个唯一的数字编号,编号范围高达110万个,但大部分字符范围在65535以内。Unicode本身没有规定怎么把这个编号对应到二进制形式中。
UTF-32/UTF-16/UTF-8都在做一件事,就是把Unicode编号对应到二进制形式,其对应方法不同而已。UTF-32使用4个字节,UTF-16大部分都是两个字节,少部分是四个字节,它们不兼容ASCII码,都有字节顺序问题。UTF-8使用1-4个字节表示,兼容ASCII码,英文字符使用一个字节,中文字符大多使用三个字节。
为什么会出现乱码
如何从乱码中恢复
Character基本原理
文件基础
文件如何存储
File对象
Java把文件和文件夹路径表示抽象成File类。
构造方法
public File(File parent, String child);
File(String pathname);
File(String parent, String child);
File(URI uri);
IO基本概念
读写
把磁盘中的数据加载到内存,再显示到显示器上称为读,从程序的角度来说也叫输入。
把内存中的数据写入磁盘,称为写,从程序的角度来说也叫输出。
按照流的方向分为:输入流和输出流
按照流的读写的单位分为:字节流和字符流
按照流的功能分为:包装流和基础流
字节输入流-InputStream
字节输出流-OutpuStream
字符流
为什么会出现字符流
字节流操作中文不是很方便,一个中文占用(2-4)个字节,所以Java提供字符流,字符流的本质上是字节+编码
,那为什么字符流能识别中文?汉字在存储的时候,无论使用哪种编码方式,第一字节都是负数。
字符流抽象基类
字符流有两个抽象基类,分别是Reader
和Writer
分别对应读和写,它们的构造方法可以传入字符集来表示以什么字符来解析。
Reader
Writer
转换流
有时候需要将字节流转换成字符流,还需要对文件进行编码解码的,Java提供了转换流,分别是InputStreamReader
和OutputStreamWriter
。
- InputStreamReader:表示将读取的字节按照指定字符集解析成字符。
- OutputStreamWrtier:表示将字符按照指定的字符集解析成二进制表示输出。
需要注意的是使用OutputStreamWriter
所得到的字节在写入底层输出流之前会积累在缓冲区。因此可以使用flush
刷新流将字节,可以调用close
方法,close方法在关闭流之前会刷新。
Close和Flush的区别
- 调用flush仅仅是刷新缓冲区,还可以操作文件。
- 而调用close方法,会先刷新,然后释放资源,释放完资源之后不能再操作。
缓冲流
将读写的字节或字符缓冲到缓冲区,提供高效读写,可以指定缓冲区的大小。
缓冲流方法
- ReadLine
- newLine
- flush
字节流与字符流的区别
- 字节流也成为万能流,可以复制(读和写)任意二进制文件
- 字符流适用于文本文件
序列化
- 序列化:将对象的数据写入磁盘或者在网络中传输,序列化包括对象的类型,对象的数据,对象中存储的属性等信息称为序列化。
- 反序列化:将保存的数据读取出来,保存到对象中,称为反序列化。
要实现序列话和反序列化就需要ObjectInputStream
和ObjectOutputStream
。
注意点
- 实现序列化的对象必须实现Serializable接口。
- Serializable是一个标记接口,类似的接口还有Closeable、RandomAccess。
- 如果某个成员属性不想被序列化可以使用transient修饰符修饰。
- 私有静态属性不会被序列化。
- 如果类包含无参构造方法或包含未知数据类型会抛出InvalidCalssException。
如果类的版本与流中类的版本不一致也会抛出InvalidClassException
- 给对象所属类加上序列化版本ID
private static final long serialVersionUID = 42L;
Properties
Properties是Map体系的集合类,可以保存到流中或者从流中加载。
// 设置集合的键值对,都是String类型,底层调用的是HashTable的put方法
// 强制使用String类型
Object setProperties(String key,String value);
// 使用此属性列表中指定的键搜索属性
String getProperty(String key):
// 从该属性列表中返回不可修改的键集,其中键及其对应的值是字符串
// 如果修改了属性会抛出UnsupportedOperationException异常
Set<String> StringPropertyNames();
保存到流或从流中加载
void load(InputStream is);
void load(Reader reader);
void store(OutputStream out);
void store(Writer writer);
- 推荐使用字符流,写入的中文字符是Unicode码
- 如果使用字节流,可以使用转换流。
打印流
package com.shaw.file.standard;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
/**
* 从控制台读取
* System类中有两个静态成员变量
* public static final InputStream in: 标准输入流 通常该流对应于键盘输入或由主机环境或用户指定的另一个输入源
* public static final PrintStream out: 标准输出流 通过该流对应于显示输出或主机环境或用户指定的另一个输出源
*/
public class Demo03 {
public static void main(String[] args) throws IOException {
// 将字节转换为字符
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String msg = br.readLine();
System.out.println("msg = " + msg);
// 通常该流对应于键盘输入
// 等待程序输入读取字节
// InputStream is = System.in;
// int b;
// while ((b = is.read()) != -1) {
// System.out.println((char) b);
// }
// 自己实现键盘录入
// 把字节流转换为字符流
// Reader is = new InputStreamReader(new BufferedInputStream(System.in), StandardCharsets.UTF_8);
// 读取一行
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8));
// char[] c = new char[1024];
// int length = -1;
// while ((length = is.read(c)) != -1) {
// String s = new String(c, 0, length);
// System.out.println(s);
// }
Scanner scanner = new Scanner(System.in);
// this(new InputStreamReader(source), WHITESPACE_PATTERN);
String s = reader.readLine();
System.out.println(s);
// 自己录入数据太麻烦了,Java提供了一个类共我们使用
// public static final PrintStream out: 标准输出流 通过该流对应于显示输出或主机环境或用户指定的另一个输出源
// 字节打印流 System.out 是一个字节输出流
// 输出语句的本质是一个输出流
// PrintStream out = System.out;
// PrintStream 类有的方法,System.out都可以使用
PrintStream out = System.out;
// 显示输出
out.println(1);
out.println("hello");
// 打印流
// printStream
// printWriter
// 只负责输出数据,不负责读取数据
// 有自己的特有方法
PrintStream ps = new PrintStream(new FileOutputStream("5555.txt"));
PrintStream printStream = new PrintStream(new BufferedOutputStream(new FileOutputStream("555555.txt")), true);
// 写数据
// 字节输出流有的方法
// write
// 使用父类的方法写数据,会转码 使用自己的特有方法写数据,查看的数据原样输出
ps.print(1);
ps.print('a');
ps.print("hshshshhs");
ps.println("hello world");
ps.println("你好啊");
ps.close();
// printWriter extends Writer
// 使用父类的方法写数据 需要刷新缓冲区 使用自己特有的方法写数据
// 可以通过构造方法,传递autoFlush 为 true
// autoFlush: 再调用 print println format 时候自动刷新
// println 写入数据之后会加入换行符
PrintWriter printWriter = new PrintWriter(new FileWriter("pp.txt"), true);
// 相当于下面这样
printWriter.println("hello");
//printWriter.write("hello");
//printWriter.write("\r\n");
//printWriter.flush();
}
}