JAVA中的字符和字节

作者: 柠檬乌冬面 | 来源:发表于2017-04-24 09:01 被阅读149次

    文章大纲:
    1.为什么要编码?
    2.各种编码集介绍
    3.UTF-8编码规则介绍
    4.编码所涉及场景
    5.相关笔试题答案和分析

    为什么要编码?

    由于人类的语言有太多,因而表示这些语言的符号太多,无法用计算机中一个基本的存储单元—— byte 来表示,因此必须要经过拆分或一些翻译工作,才能让计算机能理解。所以编码的使命也就来了,让全球的人民可以互相沟通。这和国际通用语言英语大概是一个道理,总要有一个大家互相沟通的渠道。编码的规则有很多种,下面就一一介绍一下各种编码集

    各种编码集介绍
    • ASCII 码
      学过计算机的人都知道 ASCII 码,总共有 128 个,用一个字节的低 7 位表示,0~31 是控制字符如换行回车删除等;32~126 是打印字符,可以通过键盘输入并且能够显示出来。
    • ISO-8859-1
      128 个字符显然是不够用的,于是 ISO 组织在 ASCII 码基础上又制定了一些列标准用来扩展 ASCII 编码,它们是 ISO-8859-1~ISO-8859-15,其中 ISO-8859-1 涵盖了大多数西欧语言字符,所有应用的最广泛。ISO-8859-1 仍然是单字节编码,它总共能表示 256 个字符。
    • GB2312
      它的全称是《信息交换用汉字编码字符集 基本集》,它是双字节编码, 第一个字节称为“高位字节”,第二个字节称为“低位字节”。 “高位字节”使用了0xA1-0xF7(把01-87区的区号加上0xA0),“低位字节”使用了0xA1-0xFE(把01-94加上0xA0)。
      例如 “啊”字在大多数程序中,会以0xB0A1储存(与区位码对比:0xB0=0xA0+16,0xA1=0xA0+1)。
      其中从 A1-A9 是符号区,总共包含 682 个符号,
    给你来个图直观感受一下

    从 B0-F7 是汉字区,包含 6763 个汉字。

    给你来个图直观感受一下
    • GBK
      全称叫《汉字内码扩展规范》,是国家技术监督局为 windows95 所制定的新的汉字内码规范,它的出现是为了扩展 GB2312,加入更多的汉字,它的编码范围是 8140~FEFE(去掉 XX7F)总共有 23940 个码位,它能表示 21003 个汉字,它的编码是和 GB2312 兼容的,也就是说用 GB2312 编码的汉字可以用 GBK 来解码,并且不会有乱码。
    • GB18030
      全称是《信息交换用汉字编码字符集》,是我国的强制标准,它可能是单字节、双字节或者四字节编码,它的编码与 GB2312 编码兼容,这个虽然是国家标准,但是实际应用系统中使用的并不广泛。
    • UTF-16
      说到 UTF 必须要提到 Unicode(Universal Code 统一码),ISO 试图想创建一个全新的超语言字典,世界上所有的语言都可以通过这本字典来相互翻译。可想而知这个字典是多么的复杂,关于 Unicode 的详细规范可以参考相应文档。Unicode 是 Java 和 XML 的基础,下面详细介绍 Unicode 在计算机中的存储形式。
      UTF-16 具体定义了 Unicode 字符在计算机中存取方法。UTF-16 用两个字节来表示 Unicode 转化格式,这个是定长的表示方法,不论什么字符都可以用两个字节表示,两个字节是 16 个 bit,所以叫 UTF-16。UTF-16 表示字符非常方便,每两个字节表示一个字符,这个在字符串操作时就大大简化了操作,这也是 Java 以 UTF-16 作为内存的字符存储格式的一个很重要的原因。
    • UTF-8
      UTF-16 统一采用两个字节表示一个字符,虽然在表示上非常简单方便,但是也有其缺点,有很大一部分字符用一个字节就可以表示的现在要两个字节表示,存储空间放大了一倍,在现在的网络带宽还非常有限的今天,这样会增大网络传输的流量,而且也没必要。而 UTF-8 采用了一种变长技术,每个编码区域有不同的字码长度。不同类型的字符可以是由 1~6 个字节组成。
      UTF-8 有以下编码规则:
      如果一个字节,最高位(第 8 位)为 0,表示这是一个 ASCII 字符(00 - 7F)。可见,所有 ASCII 编码已经是 UTF-8 了。
      如果一个字节,以 11 开头,连续的 1 的个数暗示这个字符的字节数,例如:110xxxxx 代表它是双字节 UTF-8 字符的首字节。
      如果一个字节,以 10 开始,表示它不是首字节,需要向前查找才能得到当前字符的首字节
    UTF-8编码规则介绍

    如下图所示,这里UTF-8可变长编码用到了一个小技巧:用几位冗余信息告诉系统,当前字符有没有结束,是不是还需要继续往下读下一个字节。


    UTF-8转换表

    下图演示了字符串“I am 君山”用 UTF-8 编码的结果:


    君 = 541b = 0101 0100 0001 1011 (Unicode)

    我们把君的unicode从后往前数,每6个为一个字节(因为前2位固定为10),然后计算一下

    需要用3个字节编码,把0101010000011011切成3部分变成:

    0101 010000 011011

    分别套上UTF-8字符头:

    1110 0101 10 010000 10 011011 = e5 90 9b

    编码所涉及场景
    • URL 的编解码
      用户提交一个 URL,这个 URL 中可能存在中文,因此需要编码,如何对这个 URL 进行编码?根据什么规则来编码?有如何来解码?如下图一个 URL:
    URL各部分解释

    上图中 PathInfo 和 QueryString 出现了中文,当我们在浏览器中直接输入这个 URL 时,在浏览器端和服务端会如何编码和解析这个 URL 呢?为了验证浏览器是怎么编码 URL 的我们选择 FireFox 浏览器并通过 HTTPFox 插件观察我们请求的 URL 的实际的内容,以下是 URL:HTTP://localhost:8080/examples/servlets/servlet/ 君山 ?author= 君山在中文 FireFox3.6.12 的测试结果

    HTTPFox 的测试结果

    君山的编码结果分别是:e5 90 9b e5 b1 b1,be fd c9 bd,查阅上一届的编码可知,PathInfo 是 UTF-8 编码而 QueryString 是经过 GBK 编码,至于为什么会有“%”?查阅 URL 的编码规范 RFC3986 可知浏览器编码 URL 是将非 ASCII 字符按照某种编码格式编码成 16 进制数字然后将每个 16 进制表示的字节前加上“%”,所以最终的 URL 就成了上图的格式了。

    默认情况下中文 IE 最终的编码结果也是一样的,不过 IE 浏览器可以修改 URL 的编码格式在选项 -> 高级 -> 国际里面的发送 UTF-8 URL 选项可以取消。

    从上面测试结果可知浏览器对 PathInfo 和 QueryString 的编码是不一样的,不同浏览器对 PathInfo 也可能不一样,这就对服务器的解码造成很大的困难,下面我们以 Tomcat 为例看一下,Tomcat 接受到这个 URL 是如何解码的。

    解析请求的 URL 是在 org.apache.coyote.HTTP11.InternalInputBuffer 的 parseRequestLine 方法中,这个方法把传过来的 URL 的 byte[] 设置到 org.apache.coyote.Request 的相应的属性中。这里的 URL 仍然是 byte 格式,转成 char 是在 org.apache.catalina.connector.CoyoteAdapter 的 convertURI 方法中完成的

     protected void convertURI(MessageBytes uri, Request request) 
     throws Exception { 
            ByteChunk bc = uri.getByteChunk(); 
            int length = bc.getLength(); 
            CharChunk cc = uri.getCharChunk(); 
            cc.allocate(length, -1); 
            String enc = connector.getURIEncoding(); 
            if (enc != null) { 
                B2CConverter conv = request.getURIConverter(); 
                try { 
                    if (conv == null) { 
                        conv = new B2CConverter(enc); 
                        request.setURIConverter(conv); 
                    } 
                } catch (IOException e) {...} 
                if (conv != null) { 
                    try { 
                        conv.convert(bc, cc, cc.getBuffer().length - 
     cc.getEnd()); 
                        uri.setChars(cc.getBuffer(), cc.getStart(), 
     cc.getLength()); 
                        return; 
                    } catch (IOException e) {...} 
                } 
            } 
            // Default encoding: fast conversion 
            byte[] bbuf = bc.getBuffer(); 
            char[] cbuf = cc.getBuffer(); 
            int start = bc.getStart(); 
            for (int i = 0; i < length; i++) { 
                cbuf[i] = (char) (bbuf[i + start] & 0xff); 
            } 
            uri.setChars(cbuf, 0, length); 
     }
    

    从上面的代码中可以知道对 URL 的 URI 部分进行解码的字符集是在 connector 的 <Connector URIEncoding=”UTF-8”/> 中定义的,如果没有定义,那么将以默认编码 ISO-8859-1 解析。所以如果有中文 URL 时最好把 URIEncoding 设置成 UTF-8 编码。

    QueryString 又如何解析? GET 方式 HTTP 请求的 QueryString 与 POST 方式 HTTP 请求的表单参数都是作为 Parameters 保存,都是通过 request.getParameter 获取参数值。对它们的解码是在 request.getParameter 方法第一次被调用时进行的。request.getParameter 方法被调用时将会调用 org.apache.catalina.connector.Request 的 parseParameters 方法。这个方法将会对 GET 和 POST 方式传递的参数进行解码,但是它们的解码字符集有可能不一样。QueryString 的解码字符集是在哪定义的呢?它本身是通过 HTTP 的 Header 传到服务端的,并且也在 URL 中,是否和 URI 的解码字符集一样呢?从前面浏览器对 PathInfo 和 QueryString 的编码采取不同的编码格式不同可以猜测到解码字符集肯定也不会是一致的。的确是这样 QueryString 的解码字符集要么是 Header 中 ContentType 中定义的 Charset 要么就是默认的 ISO-8859-1,要使用 ContentType 中定义的编码就要设置 connector 的 <Connector URIEncoding=”UTF-8” useBodyEncodingForURI=”true”/> 中的 useBodyEncodingForURI 设置为 true。这个配置项的名字有点让人产生混淆,它并不是对整个 URI 都采用 BodyEncoding 进行解码而仅仅是对 QueryString 使用 BodyEncoding 解码,这一点还要特别注意。

    从上面的 URL 编码和解码过程来看,比较复杂,而且编码和解码并不是我们在应用程序中能完全控制的,所以在我们的应用程序中应该尽量避免在 URL 中使用非 ASCII 字符,不然很可能会碰到乱码问题,当然在我们的服务器端最好设置 <Connector/> 中的 URIEncoding 和 useBodyEncodingForURI 两个参数。

    还有其他的比如:HTTP Header 的编解码、POST 表单的编解码、HTTP BODY 的编解码等请直接去这篇文章里面看深入分析 Java 中的中文编码问题。就不在这里重复了

    相关笔试题答案和分析

    1.URLEncoding是一种应用于HTTP协议的编码方式,字符串“你好”基于UTF-8的URLEncoding编码为: “%E4%BD%A0%E5%A5%BD”
    其中E4、BD、A0为字符“你”的UTF-8编码的十六进制形式(3个字节),而E5、A5、BD为字符“好”的UTF-8编码的十六进制形式。
    下面的代码用程序的方式输出字符串“你好”的基于UTF-8的URLEncoding序列:
    String msg = "你好";
    空白处1
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < bs.length; i++) {
    空白处2
    sb.append("%").append(str);
    }
    System.out.println(sb.toString());
    空白处1及空白处2分别应填入的代码是()。
    A. byte[] bs = msg.getChars("utf-8");
    和 String str = Integer.toHexString(bs[i]& 0xff).toUpperCase();
    B. byte[] bs = msg.getBytes("utf-8");
    和 String str = Integer.toHexString(bs[i]).toUpperCase();
    C. byte[] bs = msg.getBytes("utf-8");
    和 String str = Integer.toHexString(bs[i] & 0xff).toUpperCase();
    D. byte[] bs = msg.getBytes();
    和 String str = Integer.toHexString(bs[i]).toUpperCase();

    找了很久,实在没找到太多相关的题目。如果大家有题目补充或者链接,欢迎告知。我把题目加入进来。

    答案解析:首先String的getChatrs方法,不只一个参数。我在IDE里面只加入一个参数,就像选项中的A这样。是会报错的。由此可知,答案就在BCD中的一个。我们再看看D和BC的区别,在getBytes()方法上,如果你不传参数,它默认的是使用操作系统默认的编码格式。那么这样就有可能会出错,在不同的默认系统环境下。所以指定了编码utf-8的是最保险的。那么我们看BC的答案,差别就在于有没有&0xff。那我们输出一下相应的结果:

    没有&0xff时的结果

    我们可以看到,输出的是32位(int)的二进制,然后前面都是符号位的1的补充,其实真正有用的只有最后8位。我们看到最终答案中只要去掉前面的6个F就是我们要的答案了,所以&0xff的原因也就是如此。0xff(32位二进制)=24个0 1111 1111 这样就保证了前24位为0,后8位不变。

    有&0xff时的结果

    参考文献:

    通过故事来串连各种编码集:Unicode 和 UTF-8 有何区别?
    深入分析 Java 中的中文编码问题
    Java 中字节流与字符流的区别?
    字符编码笔记:ASCII,Unicode和UTF-8
    在本文中的使用位置:第二段中讲解GB2312部分:GB2312区位码、编码表与编码规则

    相关文章

      网友评论

        本文标题:JAVA中的字符和字节

        本文链接:https://www.haomeiwen.com/subject/punmattx.html