java面试程序员Java学习笔记

JAVA中的字符和字节

2017-04-24  本文已影响149人  柠檬乌冬面

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

为什么要编码?

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

各种编码集介绍
给你来个图直观感受一下

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

给你来个图直观感受一下
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各部分解释

上图中 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区位码、编码表与编码规则

上一篇下一篇

猜你喜欢

热点阅读