不要受 JDK 中固有的本地支持蒙蔽而放松警惕。即使 Java 语言充满了本地化特性,您的应用程序仍然可能成为以美国为中心的。许多国际化问题源自开发人员对自由文本用户输入、货币显示和日期/时间解析的无效假设。本文将展示这些假设如何会绊倒您,并帮助您让自己的应用程序更加世界通用。
Java 开发人员都熟悉资源绑定,但是通常会忽视把读取输入作为应用程序的一个国际化敏感的方面。要真正成为国际化的,应用程序应该能够接受各种语言和字符集的输入。决不假设文本字段输入总是符合 US-ASCII 的。
自从 JDK 1.4 以来,Java 语言已经提供了很长时间的正则表达式支持。正则表达式已经在很多常见框架(比如 Struts)中用于输入验证,现在已在 java.lang.String 中得到直接支持。但是简化模式匹配和输入验证的同一功能,却也会妨碍国际化。考虑下面这个常见的正则表达式:
/[a-zA-Z0-9 ]*/ |
该表达式显然是一个字母数字掩码,旨在防止特殊字符。因而这可以保护应用程序不接受非预期的输入。但是这类严格的匹配会导致无意中的后果。该正则表达式不仅会防止不需要的符号,也会防止许多包含拉丁字母之外的字符的单词。例如,它会拒绝许多原来拼写正确的名词,比如 España (Spain) 或 München (Munich)。令人惊讶的是,它甚至还会拒绝 Washington D.C. 的规划者 Pierre L'Enfant 的名字,因为它不允许撇号。国际化的应用程序需要具有宽的而不是窄的输入掩码。由于其 ASCII 限制,传统的正则表达式是有悖于国际化的,如下面这个例子所示:
if (inputString.matches("\\w*"))
|
标准表达式符号 \w(单词字符)与第一个例子是一样的。在本例中,单词 实际上只表示英语单词。支持国际化的输入需要超出标准的 regexp 匹配指示符。
当我第一次遇到这个问题时,我很高兴发现这已是一个众所周知的问题,并且有人在着手解决它了。有两种方式指定较宽的匹配,即使用 Posix 字符块 (character blocks) 和类别 (categories)。将它们指定为 \p{block | category}。例如,\p{L} 匹配任何 Unicode 字母。在本例中,字母 具有宽得多的含义,不仅包括拉丁字符,还包括日文片假名、朝鲜语 Hangul 以及更多的字符集。表 1 展示了 Posix 正则表达式类别的一些例子。
| \p{Lu} | 大写字母 |
| \p{Ll} | 小写字母 |
| \p{P} | 标点符号 |
类别适合于一般情况匹配,但是如果您需要更加特定的话,可以使用字符块。字符块可以让您显式地包含或拒绝 Unicode 某个区域中的字符。表 2 展示了 Posix 字符块的一些例子。
| [\p{InKatakana}*] | 匹配任何片假名字符 |
| [\p{InBasic Latin}\p{InLatin-1 Supplement}] | 匹配基本的和补充的拉丁字符 |
必须用正确的块名指定字符块。不幸的是,JDK 没有定义任何方便的常量,javadoc 也没有详细列出所有的可能性。块名取自 Unicode 标准,并且列出在 Unicode 站点的一个文件中(参阅 参考资料)。
开始使用 Unicode 正则表达式的最好方式是体验不同语言中的简单匹配。下面的示例代码用几种语言中的文本来测试标准正则表达式和 Unicode 正则表达式。若想要运行这个例子,必须将 VM 默认编码设置为 UTF-8 (-Dfile.encoding=UTF-8)。
public static void main(String[] args)
{
//category examples
doMatch("ü", "\\p{Ll}"); // Lowercase Unicode letter
doMatch("ü", "\\p{Lu}"); // uppercase Unicode letter
//character block examples
doMatch("한글", "\\p{InHangul Syllables}*"); // Korean
doMatch("カタカナ", "\\p{InKatakana}*"); // Japanese
// German spelling for Munich
// only matches the last two expressions
String s[] = {"Munich", "München"};
for (int i=0 ; i<s.length ; i++) {
doMatch(s[i], "[a-zA-Z0-9]*"); //explicit
doMatch(s[i], "\\w*"); // word character
doMatch(s[i], "\\p{Alpha}*"); // alphabetic character
doMatch(s[i], "[\\p{InBasic Latin}\\p{InLatin-1 Supplement}]*");
doMatch(s[i], "\\p{L}*"); // Unicode letter
}
}
public static void doMatch(String s, String regexp) {
if (s.matches(regexp))
System.out.println(s + " matches " + regexp);
else
System.out.println(s + " doesn't match " + regexp);
}
|
货币显示似乎微不足道,但通常是国际化过程中一个被忽略的领域。换算、小数格式、货币符号位置和消除二义性都是影响货币正确显示的因素。
一个错误的货币假设是,所有的金额都应该表示为带有两个小数位。$1.25 约等于 1,314.92 韩元 (Korean won),但是您永远也兑换不到这一金额。原因很简单,不可能给某人 0.92 韩元,因为韩国货币的最小面额是元。韩元 (KRW) 和日元 (JPY) 通常以不带任何小数位显示。JDK 在这一方面很有帮助,因为它有一个 java.util.Currency 类。要确定一种货币符合常规的小数位数,可以使用 getDefaultFractionDigits() 方法:
Currency c = Currency.getInstance("KRW");
int i = c.getDefaultFractionDigits();
|
另一个错误是使用句点 (.) 作为小数指示符和逗号 (,) 作为分组符号。与分数不一样,小数格式与人们看待货币金额是相关的。在有些国家,逗号用于指定小数位,空格或逗号可用作分隔符,如表 3 中的例子所示。
| 德国 | 1.234.567,25 |
| 法国 | 1 234 567,25 |
JDK 利用 NumberFormat 类提供小数格式规则和换算。如果小心使用,NumberFormat 可以简化货币处理(参阅 参考资料)。但是它也会引入新的问题,因为它作了一些非常宽的假设。下面这个简短的例子中就作了这样一个假设:
DecimalFormat format = (DecimalFormat)NumberFormat.getCurrencyInstance(); String amount = format.format(1.25); |
金额将被格式化为何种货币形式?不了解代码运行所在的系统的情况,就不可能预言系统包含的 amount 变量。NumberFormat 在幕后为您作出货币决策。它基于地区假设一种货币。这一表面上方便的假设是危险的,因为地区与货币之间的关系是脆弱的。在任何给定时间,某地有可能有两种货币是有效的。并且软件应用程序可能需要在同一时间处理多种货币。为了弥补这一问题,必须对格式对象应用 Currency 实例:
DecimalFormat format = (DecimalFormat)NumberFormat.getCurrencyInstance(); format.setCurrency(amountCurrency); String amount = format.format(1.25); |
通过态度鲜明地对待 Currency 类型,您将避免当应用程序重新部署到一个不同的地方或者当应用程序需要支持多种货币时遇到的问题。这也可以保护代码免受现实世界货币变化的影响,这种变化发生是非预期的,并且会使 JDK 用于映射地区到货币的最新规则失效。
Phileas Fogg 是 Around the World in 80 Days 的主角,他最近在关于时间的错误假设上名誉扫地。他向东旅行,并忠实地将自己的手表往前拨,以适应当地时间。当回到英格兰时,他没有考虑到为适应当地时间而采取的人为因素,并错误地认为已经过了整整 80 天。对于不完全了解时区对应用程序的含义的 Java 开发人员,也会面临类似的问题。
考虑一个这样的应用程序,它在租赁的汽车应该还给租赁点的两个小时前通知客户。逻辑相当简单:记录还车时间 (drop-off time),并在这个时间进入两小时窗口时通知客户。要计算通知窗口,需要两个可比较的日期 —— 还车时间和当前系统时间。假设用户通过下拉列表或自由文本输入指定期望的还车时间。不管哪种方式,数据都必须被解析为给予您一个 java.util.Date 的可比较实例。在 Java 语言中,日期通常使用 DateFormat 的实例来解析:
// sDropOff formatted as hh:mm Date dropOff = dateFormat.parse(sDropOff); |
parse() 方法作一个隐藏的假设。除非显式地指定,否则 sDropOff 按系统时区进行解析。Java 语言需要时区,因为它内部存储相对于 Greenwich Mean Time (GMT) 的 Date。这意味着有 24 个不同版本的 5:00 p.m.,其中四个版本只存在于美国内陆。如果还车位置与系统位于不同的时区,计算就会有问题。DateFormat 允许一个显式的时区:
// sDropOff formatted as hh:mm dateFormat.setTimeZone(dropOffTimeZone); Date dropOff = dateFormat.parse(sDropOff); |
时区具有两个主要的属性。第一个是它与 GMT 的偏移量,正负都有可能。第二个是它的夏令时 (DST) 规则集。这些规则指示时区是否参与 DST,如果是的话,又指示 DST 何时开始和结束。依赖于您需要往回走多远,这些规则可能很广泛,并且国家与国家之间、地区与地区之间会各不相同。为了证明 DST 规则有多复杂,可以尝试运行下面这个代码片段:
Calendar cal = Calendar.getInstance(); cal.setTimeZone(TimeZone.getTimeZone( "America/Chicago")); cal.clear(); cal.set(Calendar.YEAR, 1985); cal.set(Calendar.MONTH, Calendar.APRIL); cal.set(Calendar.DATE, 15); cal.set(Calendar.HOUR, 8); System.out.println( cal.getTime().toGMTString()); cal.set(Calendar.YEAR, 2005); System.out.println(cal.getTime().toGMTString()); |
注意,在 1985 年,8:00 a.m. 显示为 14:00 GMT,但是在 2005 年,它则是 13:00 GMT。在本例中,偏差是由美国国会在 1986 年通过的 Public Law 99-359 所引起的,因为这条法律将 DST 从 4 月的最后一个星期日改成了 4 月的第一个星期日。存在许多这个类型的 DST 规则的例子,并且还有增加的趋势。好消息是,Java 语言具有这些规则的一个全面的数据库,您可以充分利用这个数据库,以便您在准备处理时区时可以了解这些时区的名称。
Java 语言的时区规则来源是公共域时区数据库(参阅 参考资料)。(HP-UX、Solaris 和 Mac OS X 也使用这个数据库。)有效的时区通常按时区内的陆地和最大城市来命名。EST (Eastern Standard Time)、CST (Central Standard Time)、MST (Mountain Standard Time) 和 PST (Pacific Standard Time) 不是有效的时区指示符,但是为了 JDK 1.0 的向后兼容性,也是受支持的。表 4 展示了一些常见时区指示符。
| United States/Chicago | 与具有 DST 规则的 CST 相同 |
| United States/New York | 与具有 DST 规则的 EST 相同 |
| Asia/Tokyo | 涵盖日本 |
| Europe/Berlin | 涵盖德国 |
熟悉货币、时间和文本全球化将有助于避免问题,但是可用的最强大的全球化工具是测试。如果在测试应用程序时考虑到这些全球化问题,那么我们这里讨论的很多问题都可以快速排除。确保测试来自多个字符块的输入。(测试 East Asian 字符集的一个简单方式就是从 Web 站点复制并粘贴。)通过给服务器或客户机分配不同操作系统级别的日期/时间属性,用不同时区中的服务器和客户机测试应用程序。最后,测试货币金额和其他数字可以被配置为以非美国约定来显示。熟悉每一组国际化约定和字符是有帮助的,但并不是必需的。您只需要在必要时提供配置的可能性,以便在您想要为一个新的国际市场部署应用程序时可以节约时间和金钱。
- 您可以参阅本文在 developerWorks 全球站点上的 英文原文 。
- 在 Unicode Technical Standard #18 中了解 Unicode 正则表达式的更多内容。
- 在该文件中可以找到一系列 Unicode 字符块。
- 转到 Sources for Time Zone and Daylight Saving Time Data 以找到公共域时区数据库,Java 语言在该数据库中提供其夏令时规则。
- 教程“Java 国际化基础知识”(developerWorks,2002 年 4 月)是一个很好的资源,从中可以学习到关于所有可用的 Java SE 国际化支持机制的内容。
-
Unicode Input Method Editor (IME) 这个工具有助于尽可能早地在开发周期中识别全球化问题,并且可以提供一个简单的机制用于容易地再现全球化问题。
-
International Components for Unicode 是一个成熟的、广泛采用的用于 Unicode 支持、软件国际化和全球化的库集。ICU 是 IBM 发起、支持和使用的一个开放源码的开发项目。
- 阅读“创建国际化的 JSP 应用程序”,了解独特的服务器端问题及与国际化有关的解决方案。
- “Merlin 的魔力: 格式化数值和货币”讨论了
NumberFormat调用和用于发现本地货币代码的 Java 语言支持。 - 在 developerWorks Java 技术专区 可以找到关于 Java 编程各个方面的文章。
- 还请参阅 Java 技术专区教程页面,获得 developerWorks 上免费的集中讲述 Java 的教程的完整清单。