编码的那点事儿
什么是编码?
对于普通人来说,编码总是与一些秘密的东西相关联(加密与解密);对于程序员们来说,编码大多数是指一种用来在机器与人之间传递信息的方式.
但从广义上来讲, 编码是从一种信息格式转换为另一种信息格式的过程,解码则是编码的逆向过程 .接下来举几个使用到编码的例子:
- 当我们要把想表达的意思通过一种语言表达出来,其实就是在脑海中对信息进行了一次编码,而对方如果也懂得这门语言,那么就可以用这门语言的解码方法(语法规则)来获得信息(日常的说话交流其实就是在编码与解码).
- 程序员写程序时,其实就是在将自己的想法通过计算机语言进行编码,而编译器则通过生成抽象语法树,词义分析等操作进行解码,最终交给计算机执行程序(编译器产生的解码结果并不是最终结果,一般为汇编语言,但汇编语言只是CPU指令集的助记符,还需要再进行解码).
- 计算机只有两种状态(0和1),要想存储和传输多媒体信息,就需要用到编码和解码.
- 对数据进行压缩,其本质就是以减少自身占用的空间为前提进行重新编码.
了解了编码的含义,我们接下来重点探究
Java
中的字符编码.
本文作者为: SylvanasSun .转载请务必将下面这段话置于文章开头处(保留超链接).
本文首发自 SylvanasSun Blog ,原文链接: sylvanassun.github.io/2017/08/20/…
常见的字符集
字符集就是字符与二进制的映射表 ,每一个字符集都有自己的编码规则,每个字符所占用的字节也不同(支持的字符越多每个字符占用的字节也就越多).
-
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中字符的编解码
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,这个结果显然已经破坏了原有的值.
IO中的字符编码
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中的字符编码
在
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);}}
为什么Char在Java中占用两个字节?
众所周知,在
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
内码的字符串,不过这些细节都是对外隐藏的..
参考文献
- ASCII – Wikipedia
- ISO/IEC 8859-1 – Wikipedia
- GBK – Wikipedia
- UTF-16 – Wikipedia
- UTF-8 – Wikipedia
- Java 语言中一个字符占几个字节? – RednaxelaFX的回答