上一篇分享了《Java内存模型及GC原理(Java内存分配、GC基本原理)》,本篇文章中我们来聊一下JavaWeb中一些中文编码问题。

需要编码的原因

  • 计算机中存储的最小单元是一个字节,即8bit,所以能表示的字符范围是0~255个。
  • 人类要表示的符号太多,无法用一个字节来完全表示。
  • 要解决这个矛盾必须要有一个新的数据结构char,从char到byte必须编码。

    编码格式

  1. ASCII

    • ASCII码共有128个,用一个字节的低7位表示,031是控制字符,如换行、回车、删除等;32126是打印字符,可以通过键盘输入并能够显示出来。
  2. ISO-8859-1

    • 128个字符显然是不够用的,ISO组织在ASCII码的基础上又制定了一系列标准用来扩展ASCII编码,他们是ISO-8859-1~ISO-8859-15,其中ISO-8859-1涵盖了大多数西欧语言字符,所以应用最广泛。ISO-8859-1任然是单字节编码,它总共能表示256个字符。
  3. GB2312

    • 它的全称是《信息交换用汉字编码字符集基本集》,它是双字节编码,总的编码范围是A1F7,其中从A1A9是符号区,总共包含682个字符。从B0~F7是汉字区,包含6763个汉字。
  4. GBK

    • 全称《汉字内码扩展规范》,为了扩展GB2312加入了更多的汉字,它的编码是和GB2312是兼容的,也就是说GB2312编码的汉字可以用GBK来解码,并且不会有乱码。
  5. GB18030

    • 是我国强制标准,它可能是单字节、双字节、或者四字节编码,与GB2312兼容,应用并不广泛。
  6. UTF-16

    • 用两个字节来表示Unicode转化格式,它是定长的表示方法,不论什么字符都可以用两个字节表示,两个字节是16bit,所以叫UTF-16。每两个字节表示一个字符,这就大大简化了字符串的操作,这也是java以UTF-16作为内存字符存储格式的一个很重要的原因。
  7. UTF-8

    • UTF-16同意采用两个字符表示一个字节,但是很大一部分字符用一个字节就可以表示现在却要用两个字符表示,存储空间放大了一倍,而现在网络带宽还非常有限,这样会增大网络传输的流量,而且也没必要。而UTF-8采用了一种变长的技术,每个编码区域有不同的字码长度。不同类型的字符可以由1~6个字节组成。

java中需要编码的场景

I/O操作中存在的编码

  • 涉及编码的地方一般在字符到字节或者字节带字符的转换上,二需要这种转换的场景主要是I/O。
  • Reader类是java的i/o中读字符的父类,而inputstream类是读字节的父类,inputstreamreader类就是关联字节到字符的桥梁,它负责在I/O过程中处理读取字节到字符的转换,而具体字节到字符的解码又委托streamdecoder去做,在streamdecoder解码过程中必须有用户指定charset编码格式,如果没有指定charset,将使用本地环境中的默认字符集。
  • 写的情况也类似,字符的父类是writer,字节的父类是outputstream,通过outputstreamwriter转换字符到字节。StreamEncoder类负责将字符编码成字节,编码格式和默认编码规则与解码是一致的。
  • 强烈建议不要使用操作系统的默认编码,因为这样你的应用程序的编码格式就和运行环境绑定起来了,在跨环境是很可能出现乱码。

内存操作中的乱码

  • 内存中进行字符到字节的转换也很常见。

    String s = "这是一段中文字符";
    byte[] b = s.getBytes("utf-8");
    String n = new String(b,"utf-8");


    Charset charset = Charset.forName("utf-8");
    ByteBuffer byteBuffer = charset.encode(string);
    CharBuffer charBuffer = charset.decode(byteBuffer);

Java中如何编解码

  • 以字符串“I am 君山”为例。

  • 按照ISO-8859-1编码

  • ISO-8859-1是单字节编码,中文“君山”被转化成值是3f的byte,3f也就是“?”字符。所以经常会出现中文变成“?”,很可能就是错误地使用了ISO-8859-1编码导致的。

  • 按照GB2312编码

  • GB2312字符集有一个char到byte的码表,不同的字符编码就查这个码表找到与每个字符的对应字节,然后拼装成byte数组。

  • 按照GBK编码

    • GBK与GB2312编码结果是一样的;
    • 由此可以看出来GBK编码是兼容GB2312编码的,他们的编码算法是一样的。
    • 不同的是它们的码表长度不一样,GBK包含的汉字字符更多,所以只要是经过GB2312编码的汉字都可以用GBK进行解码,反之则不然。
  • 按照utf-16编码

  • 用utf-16编码将char数组放大了一倍,单字节范围内的字符在高位补0变成两个字节,中文字符也变两个字节。

    • 编码效率非常高,规则很简单。
  • 按照utf-8编码

  • UTF-16采用顺序编码,不能对单个字符的编码值进行校验,如果中间的一个字符码值损坏,后面所有的码值都将受到影响。

    • 而UTF-8不存在这些问题,UTF-8对单字节范围内字符任然用一个字节表示,对汉字采用三个字节表示。
    • UTF-8编码与GBK和GB2312不同,不用查码表,所以在编码效率上UTF-8的效率会更好,所以在存储中文字符时UTF-8编码比较理想。
  • 几种编码格式的比较

    • GB2312与GBK的编码规则类似,但是GBK范围更大,所以GB2312与GBK比较,应该选择GBK;
    • utf-16与utf-8都是处理Unicode编码,编码规则不太相同,相对来说utf-16编码效率最高,字符到字节相互转换更简单,进行字符操作也更好,它适合本地磁盘和内存之间使用,可以进行字符和字节中间的快速切换,java内存编码就采用utf-16编码;
    • 但是UTF-16不适合网络之间的传输,因为网络传输容易损坏字节流,一旦字节流损坏就很难恢复,相比较而言,utf-8更适合网络传输,单个字符的损坏不会影响后面其他字符,编码效率介于GBK和UTF-16之间;
    • UTF-8在编码效率上和安全性上做了平衡,是理想的中文编码方式。

Java Web中涉及的编解码

  • URL的编解码

    • 浏览器编码URL将非ASCII字符按照某种编码格式编码成16进制数字后在每个16进制表示的字节前加上“%”,所以就出现了如下情况:

        http://tanqingbo.cn/%E5%93%AA%E6%9C%AC%E4%B9%A6%E9%80%82%E5%90%88%E6%8E%A8%E8%8D%90%E7%BB%99%20Java%20%E5%88%9D%E5%AD%A6%E8%80%85%EF%BC%9F/
  • http Header的编码

    • header中传递参数,如:Cookie、redirectPath等,这些用户设置的值可能存在编码问题。
    • 对header进行解码实在调用request.getHeader时进行的,这个方法将byte到char的转化使用的是ISO-8859-1,不能手动设置Header的其他解码格式,如果有非ASCII字符肯定会有乱码;
    • 不要在header中传递非ASCII字符,如果一定要出传递,可以先将这些字符用org.apache.catalina.util.URLEncoder编码,然后再添加到header中。
  • 访问数据库都是通过客户端JDBC驱动来完成的,用JDBC来存取数据要和数据的内置编码保持一致,可以通过设置JDBC URL来指定,如:MySQL:

          jdbcUrl="jdbc:mysql://localhost:3306/boke?characterEncoding=utf-8"

    JS中的编码问题

  • 外部引入JS文件

    • 如果script没有设置charset,浏览器就会以当前这个页面的默认字符集解析这个JS文件,如果外部的JS文件的编码格式与当前页面的编码格式一致,那么可以不设置这个charset,但是如果script.js文件的编码格式与当前页面不一致,上面的那段中文输入就会变成乱码。
  • JS的URL编码

    • 实际上JS中处理URL编码有三个函数,只要掌握了这三个函数,基本上就能正确处理JS的URL乱码问题了;
    1. escape()
      • 这个函数是将非ascii字符转化成Unicode编码值,并且在编码值前加上“%u”;
      1. 解码通过unescape()函数;
      2. 通过将特殊字符换成Unicode编码值可以避免因为编码的字符集的不兼容而出现的信息丢失问题,在服务端通过解码参数就可以避免乱码的问题。
    • encodeURL()

      • 与escape()相比,encodeURL()是真正的JS用来对URL编码的函数,它可以将整个URL中的字符(除了一些特殊字符,如:符号、数字、字母)进行UTF-8编码,在每个值之前加上“%”;
      • 解码通过encodeURL函数。
    • encodeURLComponent()

      • encodeURLComponent()这个函数比encodeURL()编码还要彻底;
      • 通常用于将一个URL当做参数放在另一个URL中;
    • 其他需要编码的地方

      • XML文件可以通过设置投来制定编码格式:

          <?xml version"1.0" encoding="UTF-8">
      • Velocity(基于Java的模板引擎)设置编码格式:

          services.VelocityService.input.encoding=uft-8
      • Jsp设置编码格式:

          <%@page contentType="text/html; charset=utf-8">

        常见问题分析

  • 中文变成了看不懂的字符

  • 一个汉字变成一个问号

  • 一个汉字变成两个问号

  • 一种不正常的正确编码

    • 我们通过request.getParameter获取参数值时,直接调用:

        String value = request.getParameter(name);
    • 会出现乱码,但是用如下方式:

        String value = new String(request.getParameter(name).getBytes("ISO-8859-1"),"GBK");
    • 解析时取得的value会是正确的汉字字符。

码字不易,求分享、转发。😄

推荐阅读