#编码与字符编码 (懂编码的建议直接跳过)
在计算机世界中,任何事物都是用二进制图片数字表示的,图片可以编码为JPG,PNG格式的字节流,音频,视频有MP3,MP4格式的字节流。这些JPG,MP3等都是一些众所周知的编码格式罢了,只要你
定义一个映射关系,可以正确地对文件进行编码解码,那么这就是一种编码格式。可能会有人认为一些文本文件是文本格式的,它们能用记事本直接打开,因此不是二进制格式的。这种说法并不正确,能打
开是大部分记事本默认的编码如GB2312,UTF-8,ISO等 都兼容了ASCII码,当你用记事本打开视频时,会出现很多奇怪的字符,这是因为你尝试用一种字符编码(ASCII)解码 (MP3).就像编码时你用
y=2(x)+1,解码不用x=(1-y)/2反而用 x=1+y一样。
这篇文章,里面描述从ASCII码,ISO-8859-1到GBK,GB2312,Unicode, UTF-8,UTF-16,UTF-32等编码的具体实现方式。
#Unicode编码
Unicode,又称万国码,它尝试将世界上所有出现的字符都用同一格式去表示。Unicode定义了字符和码点(字符表示值)的映射关系。如用61表示A,附上一个Unicode码点查询
再次强调Unicode只是定义字符和码点的映射关系,这个映射关系是一对一的,但并没有落地到这个码点如何用对应的二进制表示。
这里我们可能会想,我直接码点是什么内存里就用这个数字表示不就好了吗?是的,朋友,在Java中,char类型存储的就是这个码点。但是考虑表示多个char的情况,如"ab",当直接使用码点表示时,
表示为0x0000006100000062,需要使用8个字节64位。这时你会觉得这样表示太蠢,想尝试压缩一下。因此对字符串进行编码实际上也是一种压缩。接下来看看java中单个char和多个char是如何存储的。
#Java的char存储的是什么,占几个字节
先亮个论据,javadoc中Character类的一段话。
从中得出以下几条信息:
1.Unicode码点可以简单分为两类,
1.BMP码 Basic Multilingual Plane ,码点值为0-0xffff,即最大值65535,
2.补充码 ,(BMP) 和 supplementary characters. 码点值>65535
2.Java中用两个字节表示char,在char中存储的是字符的Unicode码点,char只能表示 BMP子符。
这里我也疑惑很久Java是如何表示单个的补充码字符,后来明白Java给出的方式就是 “表示不了” ,这样也很合理,65536个字符几乎已经涵盖世界主流语言用到的所有字符了,没必要为了极少出现的字
符而构建一个复杂的char类型。
3.对于char数组和String及相关类,java使用utf-16编码存储这些字符的码点。
其实String底层也是存了个char数组,本质上就是char数组。
4.对于补充码,java采用pair的方式存储,即用两个char来表示。
前面已经提到了,java单个char只能表示BMP码,如果要处理补充码时,则使用字符串或字符数字表示。用代码演示如下
char c=65535;
char[] c1=Character.toChars(65536);
仔细观察Character代码,发现并没有将int型的码点转为char型的方法,这正是因为这个码点可能需要用两个char表示。
#使用Character相关的API验证以上结论
/**定义两个码点,一个是BMP,一个是补充码
*/
int codepoint1=97;
int codepoint2=65536;
System.out.println(Character.isBmpCodePoint(codepoint1));
System.out.println(Character.isSupplementaryCodePoint(codepoint2));
/**
* 将字符用char表示,验证java单个字符,2个字节(具体的表示值是utf-16)表示unicode字符只能表示unicode中bmp
* 从api就可以看出,java并没有codepoint toChar的相关方法,是因为无法保证所有码点都能用单个char表示
*/
System.out.println(Character.toChars(codepoint1).length); //一个
System.out.println(Character.toChars(codepoint2).length); //两个
/**
* 验证无论是补充码还是BMP都是一个字符,只是对补充码存储是用4个字节-一个pair表示罢了
*/
String s1=new String(Character.toChars(codepoint1));
String s2=new String(Character.toChars(codepoint2));
System.out.println(s1);//屏幕上出现一个字符
System.out.println(s2);//屏幕上出现一个字符
#String与编码的关系
这里再重复一遍本篇反复提到的一个结论,java存储单个char时直接码点值,存储char数组或是String相关对象时,编码格式是UTF-16。(String底层就是存的char数组),因此,String存储字符串时,
存储它的UTF-16编码值。获得一个String对象,无外乎三种方式:
1.直接使用字面量构建。
String s="abc" 或是String s=new String("abc"),在编译后的字节码中,使用UTF-8编码存储这个字面量,在内存中存储时会转换为uft16格式。
在内存中存储时会转换为uft16格式,这是我个人的理解和猜测,由于来自字面量的字节流一定是UTF-8的,还可能只是简单标记一下这是来自字面量,然后直接存储它的utf-8字节流。
2.从字符数组构建
其实String类有一个属性char[],也就是String底层就是char数组。
3.从字节流构建
字节流就是对字符串以一种格式进行编码得到二进制数,字节流是需要区分编码格式的。字节流与String转换的API如下:
byte[] bytes1=s1.getBytes();//s1用utf-8编码的字节流
byte[] bytes2=s2.getBytes(StandardCharsets.UTF_16BE);//s2用uft-16编码的字节流,UTF-16,16BE,LE的区别不是本文重点,UTF-16多出额外字节表示大小端顺序
byte[] bytes3=s2.getBytes("UTF-32");//s2用uft-32编码的字节流
System.out.println(bytes1.length);// 1
System.out.println(bytes2.length);// 4
System.out.println(bytes3.length);// 4
/**
* 在对String对象和字节流进行转换时,需要指定编码,默认使用utf-8
*/
String ds1=new String(bytes1);
String ds2=new String(bytes2,StandardCharsets.UTF_16BE);
String ds3=new String(bytes3,"UTF-32");
String ds4=new String(bytes3); //错误解码
/**
* 解码得到字符串都是只包含一个unicode字符,码点是
* 再次回顾一下,字符用码点值对应,码点不能直接用char存储,因为补充码需要两个char,即一个pair表示,只能用char[] 或String
*/
System.out.println(ds1.codePointCount(0,ds1.length())+":"+ds1.codePointAt(0));
System.out.println(ds2.codePointCount(0,ds2.length())+":"+ds2.codePointAt(0));
System.out.println(ds3.codePointCount(0,ds3.length())+":"+ds3.codePointAt(0));
/**
* 由于解码使用错误的编码格式获得到了错误的4个unicode字符
*/
System.out.println(ds4.codePointCount(0,ds4.length()));
以上API说明字节流和String转换过程中需要指定编码,如果不指定,则默认使用系统默认编码,一般是UTF-8编码,具体和操作系统的local有关。
结论:String底层存储的就是一个UTF-16编码的字节流,只要是字节流,就是“被编码”的,因此进行字节流和String转换时需要指明编码。
#总结
通过这波分析,得出的重要结论有:
Java 使用两个字节表示 char,char能表示的字符有限,存储时char存储码点信息,与编码无关。
Java 使用UTF-16 编码格式存储字符数组,字符串。(选择UTF-16是出于多种因素的考量)
字节流一定是被某种字符集编码的,打开字节流应当使用正确的编码格式打开,这里的字节流是广义的。
#附加 ——验证字节码的字面量的编码是UTF-8
让我们实际操作一波并研究下从文本编辑器写下System.out.println("你好,世界");这行代码并运行时到屏幕正确打印"你好,世界”这个过程中,经历了多少次编码和解码。
环境介绍:文本编辑器Sublime,Linux 系统,默认locale编码如下
这里我就再次吐槽一波Windows,上次git下来公司一个其他团队的项目,居然打开是乱码,我第一反应是用Windows开发也就算了,至少改个IDE编码呀。估计这哥们编码应该是GBK.
1.首先创建一个文件,代码如下,保存为UTF-8格式,此处进行了编码。
public class Test{
public static void main(String[] args) {
System.out.println("你好,世界");
}
}
2.使用file Test.java命令确认编码
3.执行javac编译,这里先看一下这个参数
可以理解,使用javac编译源代码时是需要指定文件编码的,否则就是系统默认编码。这是因为文件也是字节流。
执行javac Test.java编译。
4.用sublime以UTF-8方式打开Test.class文件,看到如下内容。
此处使用sublime对class文件以utf-8编码解码,发现中文显示正常,这说明字节码文件的确是以UTF-8编码存储字符串字面量。而这和我们源代码文件无关。
5.验证字节码的字面量编码与源代码无关,将源代码存为utf-16BE格式,进行编译。
再次以UTF-8编码打开Test.class,
6.执行java Test
java程序执行时,首先会从UTF-8编码的字节流(来自字节码)构建一个String,这个String s在内存中是使用UTF-16存储的,进行System.out.println()操作实际上是执行
OutputStream.write(s.getBytes()),这个字节流使用的是默认编码utf8。
在底层,jvm会将这个输出写出到操作系统的标准输出,本例中是由终端terminal将这个字节流显示为屏幕上一个个字符,因此终端在这个过程中又进行了一次解码。在本例中,
终端使用的编码是系统默认的UTF-8编码,如果我们执行以下命令更改系统默认编码。
7.编码和解码如此频繁,可见统一编码的重要性,只要一个环节不对,之后的环节就都是乱码的了。