您的位置:首页 > 编程语言 > Java开发

中文化和国际化问题权威解析之二:Java国际化基础

2010-10-27 11:20 225 查看
我们知道
Unicode

为国际化(
I18n
)提供了坚实的基础。但是
Unicode

不等同于国际化。使用
Unicode


Java
语言,若是使用不当,同样达不到国际化的目的。让我们来看一下
Java
是怎样处理
Unicode

的。



Java
的字符类型


C
语言不同,
Java
的字符类型

char


是一个
16
位长的整数,而
C
语言的
char


8
位,等同于一个字节,只能表示单字节的字符(拉丁语系文字)。所以
Java
可以直接用一个
char

来表示一个
Unicode

字符(包括中文、英文、日文
……
),大大简化了字符和字符串的操作。



因为
Java
字符总是
Unicode

字符,所以在后文中,如果不加说明,

字符



char


都是指
16
位的
Unicode

字符,而

字节



byte


都是指
8
位字节。



编码(
encoding

然而,当今多数计算机系统,都是以字节为存储运算的基本单元。这就使得在
Java
中,用
Unicode

表示的字符串无法直接写到文件中或保存到数据库中。必须以某一种方式,将字符串转换成便于传输和存储的字节流才行。这种将
Unicode

字符转换成字节的操作,就叫做

字符编码


encoding
)。

前面说过
Unicode

有两种字节表示法:
UTF-8


UTF-16

。所以将
Unicode


UTF-8


UTF-16

编码是最直接和自然的事了。以上面的

我爱
Alibaba
あいう

为例,用
Big-endian

(高位字节在前,低位字节在后)的
UTF-16

编码,可以表示成:



我们也可以把同样的字符串转换成
UTF-8


UTF-8

是变长的编码,对于
ASCII

码字符,不需要改变,就已经是
UTF-8

了,但一个中文要用三个字节来表示:



使用
UTF-16


UTF-8

编码的数据,必须使用支持
Unicode

的软件来处理,例如支持
Unicode

的文本编辑器。目前存在的大量软件,不一定都支持
Unicode

。因此我们往往将
Unicode

转换成某一种本地字符集,例如:

英文可转换成
ISO-8859-1



中文可转换成
GB2312


GBK


BIG5

或是
GB18030

等。

日文可以转换成
SJIS


ISO-2022-JP

等。

韩文可以转换成
ISO-2022-KR

等。

本地字符集名目之多,无法全部列举。最重要是,大多数字符集只映射到
Unicode

中的部分字符,且字符集之间互相交错,互不兼容。



那么,如果在将
Unicode

转换到某一本地字符集时,发现这一编码字符集不包含这个字符,怎么办呢?例如:

我爱
Alibaba”
这个字符串(简体中文),如果转换成繁体中文的
BIG5

编码,就会变成:


?Alibaba”
。原来,
Unicode

规定,转换时碰到

看不懂

的字符,一律用
“?

0x3F


表示。



这就解释了一种常见的

乱码

情形:好端端的页面,显示在浏览器上却变成了无数个问号。原因就是
Java
在输出网页时,使用了错误的编码方式。后面将更详细地解释这个问题。



解码(
decoding

同样的,如果我们要从文件或数据库中读取文本数据,因为我们读到的是一个字节流,所以我们需要使用正确的编码方法,将字节流恢复成字符流。这个操作叫做

解码


decoding
)。

如果指定了错误的编码方法,那么就会得到不正确的字符流。和编码过程类似,
Unicode

规定,在解码时,发现

看不懂

的字节,一律用



0xFFFD


表示。例如:将

我爱
Alibaba”

UTF-8

的编码方式保存在一个文件中,用繁体中文编码
BIG5

读入,就会变成:

�����

libaba”
。因为
UTF-8

字节序列
E6 88 91 E7 88

不是一个合法的
BIG5

编码序列,而第六个字节
B1

和后面一个字节
41

(原本是字母
“A”
)碰巧可以构成一个
BIG5

字符







反过来说,是不是经过解码的字符序列中,不包含问号
“?”
,就代表解码方法是正确的呢?显然不是!

最典型的错误就是:用
ISO-8859-1

来解码中文文件。这导致了更隐蔽的错误。因为
ISO-8859-1

的字符编码正好和
Unicode

的最前面
256
个字符相同,换句话说,在
ISO-8859-1

编码之前加上
“00”
就变成了
Unicode

。正是由于这个特殊性,
ISO-8859-1

似乎成了

万能

的编码而被广泛地误用!

仍以

我爱
Alibaba”
为例,如果用
ISO-8859-1

解码此文件,我们可以得到一个看似

合法

的字符串:



很明显,使用
ISO-8859-1

解码中文文件的人,只是把
Unicode

字符看作是
16
位的

字节

而已。对
Java
而言,

我爱

这两个字符不代表中文字符

我爱

,只不过是
4
个欧洲字符和符号而已。



Java
对国际化的支持



Java I/O


Java
中,主要是通过输入输出流来进行编码和解码的。输入输出流分成两种:

字节流(
Octet Stream



java.io.InputStream


java.io.OutputStream

派生的,负责读写字节(
byte

)。

例如:
java.io.FileInputStream


java.io.ByteArrayInputStream



java.io.FileOutputStream


java.io.ByteArrayOutputStream

等。

字符流(
Character Stream



java.io.Reader


java.io.Writer

派生的,负责读写字符(
char

)。例如:
java.io.StringReader


java.io.StringWriter

等。



而联系这两种流的,分别是
OutputStreamWriter


InputStreamReader

。由这两个类来实现
Java
字符的编码和解码。

下面的完整的例子演示了
Java
如何把一个包含中文的字符串,以
GBK

编码的方式保存到一个文本文件中。

import
java.io.FileOutputStream;

import
java.io.IOException;

import
java.io.OutputStreamWriter;

public

class
TestEncoding {

public

static

void
main(String[] args) {

try
{

writeStringToFile(
"我爱Alibaba"
,
"c:/ilovealibaba.txt"
,
"GBK"
);

}
catch
(IOException e) {

e.printStackTrace();

}

}

public

static

void
writeStringToFile(String str, String filename,

String charset)
throws
IOException {

FileOutputStream ostream =
new
FileOutputStream(filename);

OutputStreamWriter writer =
new
OutputStreamWriter(ostream, charset);

try
{

writer.write(str);

}
finally
{

writer.close();

}

}

}



当然,除了输出到文件,事实上可以使用任何输出流,例如使用

ByteArrayOutputStream

将可将字节流保存在内存中。

下面的完整的例子演示了
Java
如何读取一个文件,并把文件的内容以
GBK

方式解码。

import
java.io.FileInputStream;

import
java.io.IOException;

import
java.io.InputStreamReader;

public

class
TestDecoding {

public

static

void
main(String[] args) {

try
{

System.out

.println(readStringFromFile(
"c:/ilovealibaba.txt"
,
"GBK"
));

}
catch
(IOException e) {

e.printStackTrace();

}

}

public

static
String readStringFromFile(String filename, String charset)

throws
IOException {

FileInputStream istream =
new
FileInputStream(filename);

InputStreamReader reader =
new
InputStreamReader(istream, charset);

StringBuffer string =
new
StringBuffer();

char
[] buffer =
new

char
[
128
];

int
count =
0
;

try
{

while
((count = reader.read(buffer)) != -
1
) {

string.append(buffer,
0
, count);

}

}
finally
{

reader.close();

}

return
string.toString();

}

}



当然也可以从任何输入流中获得字节,然后用同样的方法转换成字符。例如,通过

ByteArrayInputStream

,可以从内存中的
byte[]

数组中取得字节流。



字符串处理

另一种常见的编码和解码的方法,是通过
Java

String

类完成的。下面的程序演示了
Java
如何使用
String.getBytes()

方法,将字符串编码成指定形式的字节的。

import
java.io.UnsupportedEncodingException;

public

class
TestStringGetBytes {

public

static

void
main(String[] args) {

try
{

dumpBytes(
"我爱Alibaba"
,
"GBK"
);

}
catch
(UnsupportedEncodingException e) {

e.printStackTrace();

}

}

public

static

void
dumpBytes(String str, String charset)

throws
UnsupportedEncodingException {

byte
[] bytes = str.getBytes(charset);

// 显示bytes的内容,每行显示4个

for
(
int
i =
0
; i < bytes.length; i++) {

System.out.print(Integer.toHexString(bytes[i] &
0xFF
));

System.out.print(
" "
);

if
((i +
1
) %
4
==
0
) {

System.out.println();

}

}

System.out.println();

}

}

运行的结果为:

ce d2 b0 ae

41
6c
69

62

61

62

61

下面的程序,使用

String(bytes, charset)

构造函数,也实现了读取一个文件的内容,并以指定编码方式(
GBK

)解码成字符串的功能。

import
java.io.ByteArrayOutputStream;

import
java.io.FileInputStream;

import
java.io.IOException;

public

class
TestNewString {

public

static

void
main(String[] args) {

try
{

System.out.println(

readStringFromFile(
"c:/ilovealibaba.txt"
,
"GBK"
));

}
catch
(IOException e) {

e.printStackTrace();

}

}

public

static
String readStringFromFile(String filename, String charset)
throws
IOException {

FileInputStream istream =
new
FileInputStream(filename);

ByteArrayOutputStream ostream =
new
ByteArrayOutputStream();

byte
[] buffer =
new

byte
[
128
];

int
count =
0
;

try
{

while
((count = istream.read(buffer)) != -
1
) {

ostream.write(buffer,
0
, count);

}

}
finally
{

istream.close();

}

byte
[] stringBytes = ostream.toByteArray();

// 使用指定charset,将bytes[]转换成字符串

return

new
String(stringBytes, charset);

}

}

注意:

上面这段程序只是演示
String(bytes, charset)

构造函数,如果要读取大量的文本,这种方式的性能肯定不如前面使用
InputStreamReader

的程序示例。



其它和国际化相关的功能

java.util.ResourceBundle

通过
ResourceBundle

,我们可以把特定语言相关的信息放在程序之外。这样当我们要在已有产品的基础上,增加一种语言或地区的支持时,只需要增加一种
ResourceBundle

的实现即可。

数字、货币、日期、时间的格式化

中国人表示日期的习惯是:
“2003

5

24


星期六

,而美国人则习惯于:
“Saturday, May 24, 2003”

Java
程序代码可以不关心这些差别。在运行时刻,
Java
可以根据不同的语言或地区习惯,自动按不同的格式风格显示这些内容。

import
java.text.DateFormat;

import
java.util.Date;

import
java.util.Locale;

public

class
TestDateFormat {

public

static

void
main(String[] args) {

Date date =
new
Date();

System.out.println(DateFormat.getDateInstance(DateFormat.FULL, Locale.CHINA).format(date));

System.out.println(DateFormat.getDateInstance(DateFormat.FULL, Locale.US).format(date));

}

}

除了

DateFormat


java.text

包中还包括了很多其它格式化类。

1.

NumberFormat

2.

DecimalFormat

3.

DateFormat

4.

SimpleDateFormat

5.

MessageFormat

6.

ChoiceFormat

检测字符属性

前文提到,
Unicode

不仅定义了统一的字符集,而且为这些字符及编码数据提出应用的方法以及对语义数据进行补充。而
Java
可以直接查看
Unicode

所定义的这些字符属性。

传统的非国际化的程序常常这样检测一个字符是否属于字母、数字还是空白:

char
ch;

...

if
((ch >=
'a'
&& ch <=
'z'
) || (ch >=
'A'
&& ch <=
'Z'
)) {

// ch是一个字母

}

...

if
(ch >=
'0'
&& ch <=
'9'
) {

// ch是一个数字

}

...

if
((ch ==
' '
) || (ch ==
'/r'
) || (ch ==
'/n'
) || (ch ==
'/t'
)) {

// ch是一个空白

}

这样的程序没有考虑除了英文和其它少数几种语言之外的语言习惯。例如:西腊字母

“αβγ”
也应该算是字母,汉字中全角数字

123

也是数字,全角空格

 


U+3000
)也属于空白。正确的程序应该是这样的:

char
ch;

...

if
(Character.isLetter(ch)) {

...

if
(Character.isDigit(ch)) {

...

if
(Character.isSpaceChar(ch)) {

...

下面列出了

Character

中用来判定字符属性的方法:

1.

Character.isDigit

2.

Character.isLetter

3.

Character.isLetterOrDigit

4.

Character.isLowerCase

5.

Character.isUpperCase

6.

Character.isSpaceChar

7.

Character.isDefined

此外,
Unicode

还为每个统一字符定义了很多属性。我们可以通过
Character

相应方法取得这些属性。例如可以用下面的代码判定一个字符是否为

中日韩统一汉字



char
ch;

...

Character.UnicodeBlock block = Character.UnicodeBlock.of(ch);

if
(block == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS) {

// 是CJK统一汉字

}

更多

Character

类细节请参阅
Java API文档



字符串比较和排序

字符间的逻辑顺序不一定和
Unicode

编码的数值顺序一致。利用
java.text.Collator

可以比较两个
Unicode

字符串的逻辑顺序。

检测字符串的边界

在应用中,我们经常需要检测字符串的边界:检测字符(
character
)、词(
word
)、句子(
sentence
)、行(
line
)的边界。例如,显示一段文字,需要在屏幕的右边界处对文本断行。断行不是任意的。例如,你不能把一个英文单词拆开。

使用
java.text.BreakIterator

可以实现字符串边界的检测。

以上只是简单地列举了
Java
中和国际化相关的功能。具体描述这些内容,超出了本文的议题。可以从
Java
文档中取得更详细的信息:
Java国际化指南

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: