对于普通人来讲,编码老是与一些秘密的东西相关联(加密与解密);对于程序员们来讲,编码大多数是指一种用来在机器与人之间传递信息的方式.html
但从广义上来说,编码是从一种信息格式转换为另外一种信息格式的过程,解码则是编码的逆向过程.接下来举几个使用到编码的例子: java
当咱们要把想表达的意思经过一种语言表达出来,其实就是在脑海中对信息进行了一次编码,而对方若是也懂得这门语言,那么就能够用这门语言的解码方法(语法规则)来得到信息(平常的说话交流其实就是在编码与解码).git
程序员写程序时,其实就是在将本身的想法经过计算机语言进行编码,而编译器则经过生成抽象语法树,词义分析等操做进行解码,最终交给计算机执行程序(编译器产生的解码结果并非最终结果,通常为汇编语言,但汇编语言只是CPU指令集的助记符,还须要再进行解码).程序员
了解了编码的含义,咱们接下来重点探究Java
中的字符编码.github
本文做者为: SylvanasSun.转载请务必将下面这段话置于文章开头处(保留超连接).
本文首发自SylvanasSun Blog,原文连接: sylvanassun.github.io/2017/08/20/…web
字符集就是字符与二进制的映射表,每个字符集都有本身的编码规则,每一个字符所占用的字节也不一样(支持的字符越多每一个字符占用的字节也就越多).spring
ASCII : 美国信息交换标准码(American Standard Code for Information Interchange).学过计算机的都知道大名鼎鼎的ASCII
码,它是基于拉丁字母的字符集,总共记有128个字符,主要目的是显示英语.其中每一个字符占用一个字节(只用到了低7位).编程
ISO-8859-1 : 它是由国际标准化组织(International Standardization Organization)在ASCII
基础上制定的8位字符集(仍然是单字节编码).它在ASCII
空置的0xA0-0xFF
范围内加入了96个字母与符号,支持了欧洲部分国家的语言.数组
GBK : 若是咱们想要让电脑上显示汉字就必需要有支持汉字的字符集,GBK就是这样一个支持汉字的字符集,全称为<<汉字内码扩展规范>>,它的编码方式分为单字节与双字节: 00–7F
范围内是第一个字节,与ASCII
保持一致,以后的双字节中,前一字节是双字节的第一位(范围在81–FE
,不包含80
和FF
),第二字节的一部分在40–7E
,其余部分在80–FE
.(这里再也不介绍GB2313
与GB18030
,它们都是互相兼容的.)浏览器
UTF-16 : UTF-16
是Unicode(统一码,一种以支持世界上多国语言为目的的通用字符集)
的一种实现方式,它把Unicode
的抽象码位映射为2~4
个字节来表示,UTF-16
是变长编码(UTF-32是真正的定长编码
),但在最开始之前UTF-16
是用来配合UCS-2(UTF-16的子集,它是定长编码,用2个字节表示全部Unicode字符)
使用的,主要缘由仍是由于当时Unicode
只有不到65536个字符,2个字节就足以应对一切了.后来,Unicode
支持的字符不断膨胀,2个字节已经不够用了,致使一些只支持UCS-2
当作内码的产品很尴尬(Java
就是其中之一).
UTF-8 : UTF-8
也是基于Unicode
的变长编码表,它使用1~6
个字节来为每一个字符进行编码(RFC 3629
对UTF-8
进行了从新规范,只能使用原来Unicode
定义的区域,U+0000~U+10FFFF
,也就是说最多只有4个字节),UTF-8
彻底兼容ASCII
,它的编码规则以下:
在U+0000~U+007F
范围内,只须要一个字节(也就是ASCII
字符集中的字符).
在U+0080~U+07FF
范围内,须要两个字节(希腊文、阿拉伯文、希伯来文等).
在U+0800~U+FFFF
范围内,须要三个字节(亚洲汉字等).
其余的字符使用四个字节.
Java
提供了Charset
类来完成对字符的编码与解码,主要使用如下函数:
public static Charset forName(String charsetName)
: 这是一个静态工厂函数,它根据传入的字符集名称来返回对应字符集的Charset
类.public final ByteBuffer encode(CharBuffer cb) / public final ByteBuffer encode(String str)
: 编码函数,它将传入的字符串或者字符序列进行编码,返回的ByteBuffer
是一个字节缓冲区.public final CharBuffer decode(ByteBuffer bb)
: 解码函数,将传入的字节序列解码为字符序列.private static final String text = "Hello,编码!";
private static final Charset ASCII = Charset.forName("ASCII");
private static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1");
private static final Charset GBK = Charset.forName("GBK");
private static final Charset UTF_16 = Charset.forName("UTF-16");
private static final Charset UTF_8 = Charset.forName("UTF-8");
private static void encodeAndPrint(Charset charset) {
System.out.println(charset.name() + ": ");
printHex(text.toCharArray(), charset);
System.out.println("----------------------------------");
}
private static void printHex(char[] chars, Charset charset) {
System.out.println("ForEach: ");
ByteBuffer byteBuffer;
byte[] bytes;
if (chars != null) {
for (char c : chars) {
System.out.print("char: " + Integer.toHexString(c) + " ");
// 打印出字符编码后对应的字节
byteBuffer = charset.encode(String.valueOf(c));
bytes = byteBuffer.array();
System.out.print("byte: ");
if (bytes != null) {
for (byte b : bytes)
System.out.print(Integer.toHexString(b & 0xFF) + " ");
}
System.out.println();
}
}
System.out.println();
}复制代码
有的读者可能会对以上代码中的b & 0xFF
产生疑惑,这是为了解决符号扩展问题.在Java
中,若是一个窄类型强转为一个宽类型时,会对多出来的空位进行符号扩展(若是符号位为1,就补1,为0则补0).只有char
类型除外,char
是没有符号位的,因此它永远都是补0.
代码中调用了函数Integer.toHexString()
,变量b
在运算以前就已经被强转为了int
类型,为了让数值不受到破坏,咱们让b
对0xFF
进行了与运算,0xFF
是一个低八位都为1的值(其余位都为0),而byte
的有效范围只在低八位,因此结果为前24位(除符号位)都变为了0,低八位保留了原有的值.
若是不作这项操做,那么b
又刚好是个负数的话,那这个强转后的int
的前24位都会变为1,这个结果显然已经破坏了原有的值.
Reader
与Writer
是Java
中负责字符输入与输出的抽象基类,它们的子类实现了在各类场景中的字符输入输出功能.
在使用Reader
与Writer
进行IO
操做时,须要指定字符集,若是不显式指定的话会默认使用当前环境的字符集,但我仍是推荐显式指定一致的字符集,这样才不会出现乱码问题(Reader
与Writer
指定的字符集不一致或更改了环境致使字符集不一致等).
public static void writeChar(String content, String filename, String charset) {
OutputStreamWriter writer = null;
try {
FileOutputStream outputStream = new FileOutputStream(filename);
writer = new OutputStreamWriter(outputStream, charset);
writer.write(content);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (writer != null)
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static String readChar(String filename, String charset) {
InputStreamReader reader = null;
StringBuilder sb = null;
try {
FileInputStream inputStream = new FileInputStream(filename);
reader = new InputStreamReader(inputStream, charset);
char[] buf = new char[64];
int count = 0;
sb = new StringBuilder();
while ((count = reader.read(buf)) != -1)
sb.append(buf, 0, count);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (reader != null)
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return sb.toString();
}复制代码
在Web
开发中,乱码也是常常存在的一个问题,主要体如今请求的参数和返回的响应结果,最头疼的是不一样的浏览器的默认编码甚至还不一致.
Java
以Http
的请求与响应抽象出了Request
和Response
两个对象,只要保持请求与响应的编码一致就能避免乱码问题.
Request
提供了setCharacterEncoding(String encode)
函数来改变请求体的编码,通常经过写一个过滤器来统一对全部请求设置编码.
request.setCharacterEncoding("UTF-8");复制代码
Response
提供了setCharacterEncoding(String encode)
与setHeader(String name,String value)
两个函数,它们均可以设置响应的编码.
response.setCharacterEncoding("UTF-8");
// 设置响应头的编码信息,同时也告知了浏览器该如何解码
response.setHeader("Content-Type","text/html;charset=UTF-8");复制代码
还有一种更简便的方式,直接使用Spring
提供的CharacterEncodingFilter
,该过滤器就是用来统一编码的.
<filter>
<filter-name>charsetFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>charsetFilter</filter-name>
<url-pattern>*</url-pattern>
</filter-mapping>复制代码
CharacterEncodingFilter
的实现以下:
public class CharacterEncodingFilter extends OncePerRequestFilter {
private String encoding;
private boolean forceEncoding = false;
public CharacterEncodingFilter() {
}
public void setEncoding(String encoding) {
this.encoding = encoding;
}
public void setForceEncoding(boolean forceEncoding) {
this.forceEncoding = forceEncoding;
}
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if(this.encoding != null && (this.forceEncoding || request.getCharacterEncoding() == null)) {
request.setCharacterEncoding(this.encoding);
if(this.forceEncoding) {
response.setCharacterEncoding(this.encoding);
}
}
filterChain.doFilter(request, response);
}
}复制代码
众所周知,在Java
中一个char
类型占用两个字节,那么这是为何呢?这是由于Java
使用了UTF-16
看成内码.
内码(Internal Encoding
)就是程序内部所使用的编码,主要在于编程语言实现其char
和String
类型在内存中使用的内部编码.与之相对的就是外码(External Encoding
),它是程序与外部交互时使用的字符编码.
值得一提的是,当初UTF-16
是配合UCS-2
使用的,后来Unicode
支持的字符不断增多,UTF-16
也再也不只看成一个定长的2字节编码使用了,也就是说,Java
中的一个char
其实并不必定能表明一个完整的UTF-16
字符.
String.getBytes()
能够将该String的内码转换为指定的外码并返回这个编完码的字节数组(无参数版使用当前平台的默认编码).
public static void main(String[] args) throws UnsupportedEncodingException {
String text = "码";
byte[] bytes = text.getBytes("UTF-8");
System.out.println(bytes.length); // 输出3
}复制代码
Java
还规定char
与String
类型的序列化是使用UTF-8
看成外码的,Java
中的Class
文件中的字符串常量与符号名也都规定使用UTF-8
.这种设计是为了平衡运行时的时间效率与外部存储的空间效率所作的取舍.
在SUN JDK6
中,有一条命令-XX:+UseCompressedString
.该命令可让String
内部存储字符内容可能用byte[]
也可能用char[]
: 当整个字符串全部字符处于ASCII
字符集范围内时,就使用byte[]
(使用了ASCII
编码)来存储,若是有任一字符超过了ASCII
的范围,就退回到使用char[]
(UTF-16
编码)来存储.可是这个功能实现的并不理想,因此没有包含在Open JDK6
/Open JDK7
/Oracle JDK7
等后续版本中.
JavaScript
也使用了UTF-16
做为内码,其实现也普遍应用了CompressedString
的思想,主流的JavaScript
引擎中都会尽量使用ASCII
内码的字符串,不过这些细节都是对外隐藏的..