#编码与字符编码 (懂编码的建议直接跳过)

  在计算机世界中,任何事物都是用二进制图片数字表示的,图片可以编码为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 一个Unicode几字节 java unicode编码占用字节_java 一个Unicode几字节

 

  这里我们可能会想,我直接码点是什么内存里就用这个数字表示不就好了吗?是的,朋友,在Java中,char类型存储的就是这个码点。但是考虑表示多个char的情况,如"ab",当直接使用码点表示时,

表示为0x0000006100000062,需要使用8个字节64位。这时你会觉得这样表示太蠢,想尝试压缩一下。因此对字符串进行编码实际上也是一种压缩。接下来看看java中单个char和多个char是如何存储的。

#Java的char存储的是什么,占几个字节

  先亮个论据,javadoc中Character类的一段话。

java 一个Unicode几字节 java unicode编码占用字节_java_02

 从中得出以下几条信息:

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表示。

java 一个Unicode几字节 java unicode编码占用字节_java_03

#使用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编码如下

java 一个Unicode几字节 java unicode编码占用字节_java_04

这里我就再次吐槽一波Windows,上次git下来公司一个其他团队的项目,居然打开是乱码,我第一反应是用Windows开发也就算了,至少改个IDE编码呀。估计这哥们编码应该是GBK.

 

1.首先创建一个文件,代码如下,保存为UTF-8格式,此处进行了编码。

public class Test{
    public static void main(String[] args) {
        System.out.println("你好,世界");
    }
}

2.使用file Test.java命令确认编码

java 一个Unicode几字节 java unicode编码占用字节_java 一个Unicode几字节_05

3.执行javac编译,这里先看一下这个参数

java 一个Unicode几字节 java unicode编码占用字节_java 一个Unicode几字节_06

 可以理解,使用javac编译源代码时是需要指定文件编码的,否则就是系统默认编码。这是因为文件也是字节流。

执行javac Test.java编译。

4.用sublime以UTF-8方式打开Test.class文件,看到如下内容。

java 一个Unicode几字节 java unicode编码占用字节_字节流_07

此处使用sublime对class文件以utf-8编码解码,发现中文显示正常,这说明字节码文件的确是以UTF-8编码存储字符串字面量。而这和我们源代码文件无关。

5.验证字节码的字面量编码与源代码无关,将源代码存为utf-16BE格式,进行编译。

java 一个Unicode几字节 java unicode编码占用字节_System_08

再次以UTF-8编码打开Test.class,

java 一个Unicode几字节 java unicode编码占用字节_java 一个Unicode几字节_09

 

 6.执行java Test

java 一个Unicode几字节 java unicode编码占用字节_字节流_10

 

  java程序执行时,首先会从UTF-8编码的字节流(来自字节码)构建一个String,这个String s在内存中是使用UTF-16存储的,进行System.out.println()操作实际上是执行

OutputStream.write(s.getBytes()),这个字节流使用的是默认编码utf8。

  在底层,jvm会将这个输出写出到操作系统的标准输出,本例中是由终端terminal将这个字节流显示为屏幕上一个个字符,因此终端在这个过程中又进行了一次解码。在本例中,

终端使用的编码是系统默认的UTF-8编码,如果我们执行以下命令更改系统默认编码。

7.编码和解码如此频繁,可见统一编码的重要性,只要一个环节不对,之后的环节就都是乱码的了。