前言

上次对计算机中的“字符集”和“编码”分别进行了总结,并指出二者之间的区别,不要搞混了,不清楚的再回到上一章看一下。今天再总结下java中是如何使用字符集(主要是Unicode字符集,其他常用字符集都只有一种编码规则),以及是如何使用utf-8、utf-16、utf-32对Unicode字符集进行编码的。

java中的char类型

java中的char类型占用两个字节、用于定义字符,这些字符只覆盖了Unicode字符集中的第0个平面中定义的符号(该平面中定义的符号 都是地球人最常用的65536个),也就是说其他16个平面中的符号是没办法有java的char类型表示的。

  1. char c0 = 'A';
  2. char c1='天';
  3. char c2='星';
  4. char c3 = 'XXX';//编译错误

提示:这个字iteye无法识别,导致文章提交被截断,文中所有’XXX’都是表示这个字。

前三个char类型赋值没有问题,第四个赋值直接编译错误。’XXX’这个字的Unicode码值是:10 10001000 10111011,可以看出使用两个字节是放不下的。如果使用utf-16进行编码,编码后的二进制值如下(需要4个字节,编码格式可以参考上一篇文章):

1101100 00110001 01101110 010111011

这也就是为什么不能把'XXX'赋值给java char类型的原因。

Java中字符串转字节

Java中的字符串(String)在网络传输或者存储硬盘的真实内容是 通过字符集编码转换后的二进制数字。在String类中定义了几个getBytes重载方法,来获取字符串对应的字节数组,并且默认使用的是Unicode字符集的utf-8编码:测试代码如下:

  1. byte[] bytes = "XXX".getBytes();
  2. System.out.println("默认编码:"+new BigInteger(1,bytes).toString(2));
  3. byte[] bytes3 = "XXX".getBytes("utf-8");
  4. System.out.println("使用utf-8编码:"+new BigInteger(1,bytes3).toString(2));

打印内容为:

  1. 默认编码:11110000101010001010001010111011
  2. 使用utf-8编码:11110000101010001010001010111011

可以发现使用不带参数和带"utf-8"参数的结果是完全一致的。即可说明:getBytes不带参数的默认方法使用的是Unicode字符集的utf-8编码规则进行编码的。"XXX"不是常用的汉字,这里使用了4字节,常用汉字使用utf-8编码一般三个字节。

在上一章中讲到过utf-16是使用2或者4字节进行存储,我们来看下在java语言中的表现:

  1. byte[] bytes1 = "XXX".getBytes("Unicode");
  2. System.out.println("使用Unicde字符集的默认编码:"+new BigInteger(1,bytes1).toString(2));
  3. byte[] bytes2 = "XXX".getBytes("utf-16");
  4. System.out.println("使用utf-16编码:"+new BigInteger(1,bytes2).toString(2));
  5. byte[] bytes4 = "XXX".getBytes("ASCII");
  6. System.out.println("使用ASCII字符集:"+new BigInteger(1,bytes4).toString(2));

打印结果为:

  1. 使用Unicde字符集的默认编码:111111101111111111011000011000101101110010111011
  2. 使用utf-16编码:111111101111111111011000011000101101110010111011
  3. 使用ASCII字符集:111111

注意这里区别:

调用getBytes("Unicode")方法 表示使用Unicode字符集的默认编码方式进行编码。这个默认编码方式一般由操作系统指定,我的是win7,显示跟utf-8编码相同;

调用getBytes("utf-16")方法 表示使用默认Unicode字符集的utf-16编码方式进行编码。

另外我们上一章说过,utf-16使用2或者4个字节存储,"XXX"不在第0平面,应该占用4个字节。但实际打印出来的是11111110 11111111 11011000 01100010 11011100 10111011,一共6个字节,怎么多了两个字节呢?我们还可以发现无论是什么字符串,通过调用getBytes("utf-16")方法,打印出来的前面都会固定多两个字节:11111110 11111111,换算成16进制就是0x FEFF。这里有一个大端序和小端序的概念。

大端序和小端序

前一篇文章也提到过utf-8、utf-16、utf-32有一个区别就是一次最少读入的字节数,utf-8是一次最少读入一个字节(8个bit),utf-16是2两字节,utf-32是4个字节。其中utf-16和utf-32一次需要读入多个字节,根据读取顺序的不同,分为大端序和小端序。

大端序:简单的理解就是从左往右依次读入两个(utf-16)或者4个(utf-32)字节

小端序:简单的理解就是从右往做反向依次读入两个(utf-16)或者4个(utf-32)字节

大端序编码:UTF-16Be、UTF-32Be;小端序:UTF-16Le、UTF-32Le;另外在java中还可以使使用UnicodeBig、UnicodeLittle,表示使用默认的UTF-16大、小端序编码,与UTF-16Le、UTF-32Le基本等效,区别就是UnicodeBig、UnicodeLittle的前面会自动加上两字节,用于表示大小端序。

UnicodeBig自动在编码后的最前面加上:1111111011111111 换成16进制为 FEFF;

UnicodeLittle自动在编码后的最前面加上:1111111111111110 换成16进制为FFFE。

测试代码如下:

  1. byte[] bytes2 = "XXX".getBytes("utf-16");
  2. System.out.println("使用utf-16编码:"+new BigInteger(1,bytes2).toString(2));
  3. byte[] bytes6 = "XXX".getBytes("utf-16le");
  4. System.out.println("使用utf-16小端序编码:"+new BigInteger(1,bytes6).toString(2));
  5. byte[] bytes7 = "XXX".getBytes("utf-16be");
  6. System.out.println("使用utf-16大端序编码:"+new BigInteger(1,bytes7).toString(2));
  7. byte[] bytesx = "XXX".getBytes("utf-32be");
  8. System.out.println("使用utf-32大端序编码:"+new BigInteger(1,bytesx).toString(2));
  9. byte[] bytesy = "XXX".getBytes("utf-32le");
  10. System.out.println("使用utf-32小端序编码:"+new BigInteger(1,bytesy).toString(2));
  11. byte[] bytesz = "XXX".getBytes("utf-32");
  12. System.out.println("使用utf-32编码:"+new BigInteger(1,bytesz).toString(2));
  13. byte[] bytes8 = "XXX".getBytes("UnicodeBig");
  14. System.out.println("使用Unicode大端序编码:"+new BigInteger(1,bytes8).toString(2));
  15. byte[] bytes9 = "XXX".getBytes("UnicodeLittle");
  16. System.out.println("使用Unicode小端序编码:"+new BigInteger(1,bytes9).toString(2));

运行结果为:

  1. 使用utf-16编码:111111101111111111011000011000101101110010111011
  2. 使用utf-16小端序编码:1100010110110001011101111011100
  3. 使用utf-16大端序编码:11011000011000101101110010111011
  4. 使用utf-32大端序编码:101000100010111011
  5. 使用utf-32小端序编码:10111011100010000000001000000000
  6. 使用utf-32编码:101000100010111011
  7. 使用Unicode大端序编码:111111101111111111011000011000101101110010111011
  8. 使用Unicode小端序编码:111111111111111001100010110110001011101111011100

用BigInteger没有打印出完整的二进制,它把前面是0的全部去掉了。另外我们可以发现在在不指定大小端序的情况下默认是使用的大端序,在上一节中直接使用getBytes("utf-16")或者getBytes("Unicode")时就可以看出,前面多的两个字节刚好就是用来表示默认是“大端序”FEFF。

为什么要有大小端序呢,原因很简单:不同的操作系统的不同实现罢了,有些操作系统默认是大端序(比如我的win7下),有些默认又是小端序。所以最好指定清楚,否则容易引起乱码。

另外在window下utf-8还有带与不带BOM的区别,也就是在字符串的最前面会多出FEFF,在其他系统上显示就会有乱码,在linux操作系统下很少见到使用,这里就不深入了。

Java解码--字节数组转字符串

Java中解码,直接调用String的带字节数组的构造方法解码,生成人类能识别的符号。如果人类不能识别就是我们所谓的乱码。同时该构造方法还有另外一个参数,表示使用的字符集编码,看一个例子:

  1. byte[] b16 = "天星".getBytes("utf-16");
  2. String n8=new String(b16,"utf-8");
  3. System.out.println("使用utf-8解码 utf-16编码的字符串:"+n8);
  4. byte[] b8 = n8.getBytes("utf-8");
  5. String n16 = new String(b8,"utf-16");
  6. System.out.println("还原:"+n16);

这个例子首先使用utf-16编码对字符串进行编码;

然后使用了utf-8编码进行解码,此时由于编解码不一致,发现打印出来有乱码;

接着试图把这个乱码还原回去,再使用utf-16进行解码。

也许你期望的应该是没有乱码,可以还原,但真实的打印结果如下:

  1. 使用utf-8解码 utf-16编码的字符串:��Y)f­
  2. 还原:뷯뾽天星

发现还是有乱码,没有真正还原。这是为什么呢?这其实就是默认大端序会加两个字节引起的的问题,我们把程序改下,指定在16进制时使用大端序:

  1. byte[] b16 = "天星".getBytes("utf-16Be");
  2. String n8=new String(b16,"utf-8");
  3. System.out.println("使用utf-8解码 utf-16编码的字符串:"+n8);
  4. byte[] b8 = n8.getBytes("utf-8");
  5. String n16 = new String(b8,"utf-16Be");
  6. System.out.println("还原:"+n16);

运行结果:

  1. 使用utf-8解码 utf-16编码的字符串:Y)f­
  2. 还原:天星

发现已经神奇的还原了。

导致乱码的根本原因是编码和解码使用了不同的字符集,或者相同的字符集下使用了不同的编码规则进行编解码。但反过来确不成立,也就是说在有些情况下即便是使用了不同的字符集或编码规则进行编解码,也不会出现乱码,比如我上一个例子。这就要求我们对字符集的编码规则要相当熟悉,并能灵活运用。

理论上在编码时只要可以保证数据不丢失,都可以先还原回去,再使用正确的字符集编码进行解码,可以得到正常的结果。比如下面的例子:

  1. byte[] b16 = "天星".getBytes("ASCII");
  2. String n8=new String(b16,"utf-8");
  3. System.out.println("使用utf-8解码 utf-16编码的字符串:"+n8);
  4. byte[] b8 = n8.getBytes("utf-8");
  5. String n16 = new String(b8,"ASCII");
  6. System.out.println("还原:"+n16);

运行结果是乱码,主要原因就是在"天星".getBytes("ASCII")这一步产生了数据丢失,后面再牛逼的人也无力回天了。但如果把“天星”改成“abc”是可以还原的,因为这些字符本身就是ASCII中定义的字符。这也算是英文的优势吧。

总结

关于java中的字符集和编码就总结到这里,这次总结的起因是要分析 恶意用户的恶意请求参数。他使用了utf-16进行编码,而我们日志服务器默认是使用utf-8进行解码,从而导致乱码。但如果熟悉了我上面讲解的内容,相信你也可以把真实的内容还原回来。当然中间还使用另外一些加密手段,这里就不再一一详解了。

提示:这个字iteye无法识别,导致文章提交被截断,文中所有’XXX’都是表示这个字。

java中的字符集和编码的更多相关文章

  1. WEB开发中的字符集和编码

    html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,bi ...

  2. Ruby:字符集和编码学习总结

    背景 Ruby直到1.9版本才很好的支持了多字节编码,本文简单总结了今天学习的关于Ruby编码方面的知识. 字符串可以使用不同的编码 在.NET中字符串的编码是一致的,Ruby允许字符串有不同的编码, ...

  3. .NET:字符集和编码学习总结

    背景 一直没有深入的学习字符集和编码的知识(现在也没有深入),今天查阅了一些资料,弄明白了一些事情,本文就简单记录一下. 字符集和编码 字符集是指一些符号组成的集合,编码是对指定字符集如何表示为字节的 ...

  4. Java中面向字符的输入流

    Java中面向字符的输入流 2016-12-04 Java程序员联盟 Java程序员联盟 Java程序员联盟 微信号 javalm 功能介绍 莫道君行早,更有早行人 全心敲代码,天道自酬勤 字符流是针 ...

  5. 在Java中如何进行BASE64编码和解码

    在Java中如何进行BASE64编码和解码 //在Java中如何进行BASE64编码和解码 package me.xzh.study.sun.misc.BASE64; import sun.misc. ...

  6. java中的字符编码方式

    1. 问题由来 面试的时候被问到了各种编码方式的区别,结果一脸懵逼,这个地方集中学习一下. 2. 几种字符编码的方式 1. ASCII码 我们知道,在计算机内部,所有的信息最终都表示为一个二进制的字符 ...

  7. java中的字符,字节和编码

    1. 编码问题的由来,相关概念的理解 1.1 字符与编码的发展 从计算机对多国语言的支持角度看,大致可以分为三个阶段:   系统内码 说明 系统 阶段一 ASCII 计算机刚开始只支持英语,其它语言不 ...

  8. java中处理字符编码(网页与数据库)(转)

    首先声明一下,此文章时从网上转载的.如下的某些方法是确实管用,但是从中发现了有一点不足,就是原文笔者没考虑使用不同Web Server时出现的情况,比如文章里我用红色字体画出来的部分代码在Tomcat ...

  9. mysql中对字符集和校对规则的认识

    字符集:指符号和字符编码的集合.校对规则:比较字符编码的方式.GBK2312:主要包括简体中文字符及常用符号,对于中文字符采用双字节编码的格式,也就是说一个汉字字符在存储占两个字节.GBK:包括有中. ...

随机推荐

  1. iOS多线程的初步研究(一)-- NSThread

    对于多线程的开发,iOS系统提供了多种不同的接口,先谈谈iOS多线程最基础方面的使用.产生线程的方式姑且分两类,一类是显式调用,另一类是隐式调用. 一.显示调用的类为NSThread.一般构造NSTh ...

  2. Sphinx-实战

    配置完成后, 有了数据源.索引文件存储位置等, 便可以使用 indexer 工具创建索引, 收集要被检索的数据 -c 指定配置文件 默认使用 etc/sphinx.conf --all 对所有索引重新 ...

  3. 【前端】CSS

    CSS介绍 CSS(Cascading Style Sheet,层叠样式表)定义如何显示HTML元素.l 当浏览器读到一个样式表,它就会按照这个样式表来对文档进行格式化(渲染). CSS语法 CSS实 ...

  4. Unable to locate package错误

    W: GPG error: http://nginx.org precise Release: The following signatures couldn't be verified becaus ...

  5. Vue基础及脚手架环境搭建

    From:http://www.jianshu.com/p/dc5057e7ad0d 一.vue基础 “Vue2.0”跟俺一起全面入坑 01 “Vue2.0”跟俺一起全面入坑 02 “Vue2.0”跟 ...

  6. nopi使用 设置列样式 宽高 设置分页符

    HSSFWorkbook book = new HSSFWorkbook(); ISheet sheet = book.CreateSheet("test_01"); sheet. ...

  7. <<Python基础教程>>学习笔记 | 第11章 | 文件和素材

    打开文件 open(name[mode[,buffing]) name: 是强制选项,模式和缓冲是可选的 #假设文件不在.会报以下错误: >>> f = open(r'D:\text ...

  8. maven打包classes为jar

    <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-p ...

  9. 转-linux下配置socks5代理

    简介: 在Linux下有各种各样的代理程序可用,象最常用的Squid,是http/https代理,也能代理ftp请求,但它实际上 是个HTTP代理程序,不是ftp代理,但它能处理ftp代理请求,就象浏 ...

  10. Redis-ha(sentinel)搭建

    服务器描述:本次搭建是用来测试,所以是在一台服务器上搭建三个redis服务(一主两从) 服务角色 端口 Redis.conf名称 sentinel配置文件名称 sentinel端口 redis日志路径 ...