中文化和国际化问题权威解析之二:Java国际化基础
我们知道Unicode为国际化(I18n)提供了坚实的基础。但是Unicode不等同于国际化。使用Unicode的Java语言,若是使用不当,同样达不到国际化的目的。让我们来看一下Java是怎样处理Unicode的。
Java的字符类型和C语言不同,Java的字符类型“?

因为Java字符总是Unicode字符,所以在后文中,如果不加说明,“字符”或“
我们也可以把同样的字符串转换成UTF-8。UTF-8是变长的编码,对于ASCII码字符,不需要改变,就已经是UTF-8了,但一个中文要用三个字节来表示:
?

使用UTF-16或UTF-8编码的数据,必须使用支持Unicode的软件来处理,例如支持Unicode的文本编辑器。目前存在的大量软件,不一定都支持Unicode。因此我们往往将Unicode转换成某一种本地字符集,例如:

那么,如果在将Unicode转换到某一本地字符集时,发现这一编码字符集不包含这个字符,怎么办呢?例如:“我爱Alibaba”这个字符串(简体中文),如果转换成繁体中文的BIG5编码,就会变成:“我?Alibaba”。原来,Unicode规定,转换时碰到“看不懂”的字符,一律用“?(0x3F)”表示。
?

这就解释了一种常见的“乱码”情形:好端端的页面,显示在浏览器上却变成了无数个问号。原因就是Java在输出网页时,使用了错误的编码方式。后面将更详细地解释这个问题。
解码(decoding)同样的,如果我们要从文件或数据库中读取文本数据,因为我们读到的是一个字节流,所以我们需要使用正确的编码方法,将字节流恢复成字符流。这个操作叫做“解码”(decoding)。
如果指定了错误的编码方法,那么就会得到不正确的字符流。和编码过程类似,Unicode规定,在解码时,发现“看不懂”的字节,一律用“?(0xFFFD)”表示。例如:将“我爱Alibaba”以UTF-8的编码方式保存在一个文件中,用繁体中文编码BIG5读入,就会变成:“?????婢libaba”。因为UTF-8字节序列?

反过来说,是不是经过解码的字符序列中,不包含问号“?”,就代表解码方法是正确的呢?显然不是!
最典型的错误就是:用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中,主要是通过输入输出流来进行编码和解码的。输入输出流分成两种:
从从?

而联系这两种流的,分别是当然,除了输出到文件,事实上可以使用任何输出流,例如使用
当然也可以从任何输入流中获得字节,然后用同样的方法转换成字符。例如,通过
ByteArrayInputStream,可以从内存中的byte[]数组中取得字节流。
?
字符串处理另一种常见的编码和解码的方法,是通过Java的String类完成的。下面的程序演示了Java如何使用String.getBytes()方法,将字符串编码成指定形式的字节的。
运行的结果为:
下面的程序,使用
String(bytes, charset)构造函数,也实现了读取一个文件的内容,并以指定编码方式(GBK)解码成字符串的功能。注意:
上面这段程序只是演示String(bytes, charset)构造函数,如果要读取大量的文本,这种方式的性能肯定不如前面使用InputStreamReader的程序示例。?
其它和国际化相关的功能java.util.ResourceBundle
通过ResourceBundle,我们可以把特定语言相关的信息放在程序之外。这样当我们要在已有产品的基础上,增加一种语言或地区的支持时,只需要增加一种ResourceBundle的实现即可。
数字、货币、日期、时间的格式化
中国人表示日期的习惯是:“2003年5月24日 星期六”,而美国人则习惯于:“Saturday, May 24, 2003”。Java程序代码可以不关心这些差别。在运行时刻,Java可以根据不同的语言或地区习惯,自动按不同的格式风格显示这些内容。
除了
DateFormat,java.text包中还包括了很多其它格式化类。
?
1. NumberFormat
2. DecimalFormat
3. DateFormat
4. SimpleDateFormat
5. MessageFormat
6. ChoiceFormat
检测字符属性
前文提到,Unicode不仅定义了统一的字符集,而且为这些字符及编码数据提出应用的方法以及对语义数据进行补充。而Java可以直接查看Unicode所定义的这些字符属性。
传统的非国际化的程序常常这样检测一个字符是否属于字母、数字还是空白:
这样的程序没有考虑除了英文和其它少数几种语言之外的语言习惯。例如:西腊字母
“αβγ”也应该算是字母,汉字中全角数字“123”也是数字,全角空格“ ”(U+3000)也属于空白。正确的程序应该是这样的:
下面列出了
Character中用来判定字符属性的方法:?
1. Character.isDigit
2. Character.isLetter
3. Character.isLetterOrDigit
4. Character.isLowerCase
5. Character.isUpperCase
6. Character.isSpaceChar
7. Character.isDefined
此外,Unicode还为每个统一字符定义了很多属性。我们可以通过Character相应方法取得这些属性。例如可以用下面的代码判定一个字符是否为“中日韩统一汉字”:
更多
Character类细节请参阅Java API文档。
?
字符串比较和排序
字符间的逻辑顺序不一定和Unicode编码的数值顺序一致。利用java.text.Collator可以比较两个Unicode字符串的逻辑顺序。
检测字符串的边界
在应用中,我们经常需要检测字符串的边界:检测字符(character)、词(word)、句子(sentence)、行(line)的边界。例如,显示一段文字,需要在屏幕的右边界处对文本断行。断行不是任意的。例如,你不能把一个英文单词拆开。
使用java.text.BreakIterator可以实现字符串边界的检测。
以上只是简单地列举了Java中和国际化相关的功能。具体描述这些内容,超出了本文的议题。可以从Java文档中取得更详细的信息:Java国际化指南。