20220424 Java核心技术 卷2 高级特性 7

国际化

Java 编程语言是第一种设计成为全面支持国际化的语言。从一开始,它就具备了进行有效的国际化所必需的一个重要特性:使用 Unicode 来处理所有字符串。支持 Unicode 使得在 Java 编程语言中,编写程序来操作多种语言的字符串变得异常方便

国际化一个程序所要做的事情绝不仅仅是提供 Unicode 支持。在世界的不同地方,日期、时间、货币甚至数字的格式都不相同

Locale 对象( Locale

不同的国家可以使用相同的语言

为了对格式化进行控制,可以使用 Locale 类。locale 由多达 5 个部分构成:

  1. 一种语言,由 2 个或 3 个小写字母表示,例如 en (英语)、 de (德语)和 zh (中文)
  2. 可选的一段脚本,由首字母大写的四个字母表示,例如 Latn (拉丁文)、 Cyrl (西里尔文)和 Hant (繁体中文字符) 这个部分很有用,因为有些语言,例如塞尔维亚语,可以用拉丁文或西里尔文书写, 而有些中文读者更喜欢阅读繁体中文而不是简体中文
  3. 可选的一个国家或地区,由 2 个大写字母或 3 个数字表示,例如 US(美国)和 CH(瑞士)
  4. 可选的一个变体,用于指定各种杂项特性,例如方言和拼写规则。变体现在已经很少使用了。过去曾经有一种挪威语的变体“尼诺斯克语”,但是它现在已经用另一种不同的代码 nn 来表示了。过去曾经用于日本帝国历和泰语数字的变体现在也都被表示成了扩展
  5. 可选的一个扩展。扩展描述了日历(例如日本历)和数字(替代西方数字的泰语数字)等内容的本地偏好。Unicode 标准规范了其中的某些扩展,这些扩展应该以 u- 和两个字母代码开头,这两个字母的代码指定了该扩展处理的是日历( ca )还是数字( nu ),或者是其他内容。例如,扩展 u-nu-thai 表示使用泰语数字。其他扩展是完全任意的,并且以 x- 开头,例如 x-java

locale 的规则

locale 是用标签描述的,标签是由 locale 的各个元素通过连字符连接起来的字符串,例如 en-US

在德国,你可以使用 de-DE 。瑞士有 4 种官方语言(德语、法语、意大利语和里托罗曼斯语)。在瑞士讲德语的人希望使用的 localede-CH 。这个 locale 会使用德语的规则,但是货币值会表示成瑞士法郎而不是欧元

如果只指定了语言,例如 de ,那么该 locale 就不能用于与国家相关的场景,例如货币

用标签字符串来构建 Locale 对象:

System.out.println(Locale.getDefault());    // zh_CN

Locale usEnglish = Locale.forLanguageTag("en-US");
System.out.println(usEnglish);    // en-US

String languageTag = Locale.US.toLanguageTag();
System.out.println(languageTag);    // en-US

Java SE 为各个国家预定义了 Locale 对象:

Locale.CANADA
Locale.CANADA_FRENCH
Locale.CHINA
Locale.FRANCE
Locale.GERMANY
Locale.ITALY
Locale.JAPAN
Locale.KOREA
Locale.PRC
Locale.TAIWAN
Locale.UK
Locale.US

Java SE 还预定义了大量的语言 Locale ,它们只设定了语言而没有设定位置:

Locale.CHINESE
Locale.ENGLISH
Locale.FRENCH
Locale.GERMAN
Locale.ITALIAN
Locale.JAPANESE
Locale.KOREAN
Locale.SIMPLIFIED_CHINESE
Locale.TRADITIONAL_CHINESE

静态的 getAvailableLocales 方法会返回由 Java 虚拟机所能够识别的所有 Locale 构成的数组

Locale[] availableLocales = Locale.getAvailableLocales();

除了构建一个 Locale 或使用预定义的 Locale 外,还可以有两种方法来获得一个 Locale 对象

Locale 类的静态 getDefault 方法可以获得作为本地操作系统的一部分而存放的默认 Locale 。可以调用 setDefault 来改变默认的 Java Locale ;但是,这种改变只对你的程序有效,不会对操作系统产生影响

对于所有与 Locale 有关的工具类,可以返回一个它们所支持的 Locale 数组

Locale[] numberFormatAvailableLocales = NumberFormat.getAvailableLocales();
Locale[] dateFormatAvailableLocales = DateFormat.getAvailableLocales();

为了测试,你也许希望改变你的程序的默认 Locale ,可以在启动程序时提供语言和地域特性。比如,下面的语句将默认的 Locale 设为 de-CH

java -Duser.language=de -Duser.region=CH MyProgram

Locale 中唯一有用的是那些识别语言和国家代码的方法,其中最重要的一个是 getDisplayName ,它返回一个描述 Locale 字符串。这个字符串并不包含前面所说的由两个字母组成的代码,而是以一种面向用户的形式来表现

String displayName = Locale.CHINA.getDisplayName();
System.out.println(displayName);    // 中文 (中国)

System.out.println(Locale.GERMANY.getDisplayName());    // 德文 (德国)
System.out.println(Locale.GERMANY.getDisplayName(Locale.GERMANY));    // Deutsch (Deutschland)


Locale locale = Locale.CHINA;
System.out.println(locale.toLanguageTag());     // zh-CN
System.out.println(locale.toString());      // zh_CN

所以为什么需要 Locale 对象。你把它传给 Locale 感知的那些方法,这些方法将根据不同的地域产生不同形式的文本

java.util.Locale 方法名称 方法声明 描述
构造器 public Locale(String language)
public Locale(String language, String country)
public Locale(String language, String country, String variant)
用给定的语言、国家和变量创建一个 Locale 。在新代码中不要使用变体,应该使用 IETF BCP 47 语言标签
forLanguageTag public static Locale forLanguageTag(String languageTag) 构建与给定的语言标签相对应的 Locale 对象
getDefault public static Locale getDefault() 返回默认的 Locale
setDefault public static synchronized void setDefault(Locale newLocale) 设定默认的 Locale
getDisplayName public final String getDisplayName() 返回一个在当前的 Locale 中所表示的用来描述 Locale 的名字
getDisplayName public String getDisplayName(Locale inLocale) 返回一个在给定的 Locale 中所表示的用来描述 Locale 的名字
getLanguage public String getLanguage() 返回语言代码,它是两个小写字母组成的 ISO-639 代码
getDisplayLanguage public final String getDisplayLanguage() 返回在当前 Locale 中所表示的语言名称
getDisplayLanguage public String getDisplayLanguage(Locale inLocale) 返回在给定 Locale 中所表示的语言名称
getCountry public String getCountry() 返回国家代码,它是由两个大写字母组成的 ISO-3166 代码
getDisplayCountry public final String getDisplayCountry() 返回在当前 Locale 中所表示的国家名
getDisplayCountry public String getDisplayCountry(Locale inLocale) 返回在当前 Locale 中所表示的国家名
toLanguageTag public String toLanguageTag() 返回该 Locale 对象的语言标签
toString public final String toString() 返回 Locale 的描述,包括语言和国家,用下划线分隔(比如,de_CH ) 应该只在调试时使用该方法

数字格式( NumberFormat

数字和货币的格式是高度依赖于 loca 。Java 类库提供了一个格式器 ( formatter )对象的集合,可以对 java.text 包中的数字值进行格式化和解析。你可以通过下面的步骤对特定 Locale 的数字进行格式化:

  1. 使用上一节的方法,得到 Locale 对象
  2. 使用一个“工厂方法”得到一个格式器对象
  3. 使用这个格式器对象来完成格式化和解析工作

工厂方法是 java.text.NumberFormat 类的静态方法,它们接受一个 Locale 类型的参数。总共有 3 个工厂方法: getNumberInstancegetCurrencyInstancegetPercentInstance 。这些方法返回的对象可以分别对数字、货币量和百分比进行格式化和解析

Locale loc = Locale.GERMAN;
NumberFormat currFmt = NumberFormat.getCurrencyInstance(loc);
double amt = 123456.78;
String result = currFmt.format(amt);

System.out.println(result);
/*
            Locale.CHINA    ¥123,456.78
            Locale.JAPAN    ¥123,457
            Locale.US       $123,456.78
            Locale.UK       £123,456.78
            Locale.GERMANY  123.456,78 €    带国家地区
            Locale.GERMAN   ¤ 123.456,78    
        */

如果要想读取一个按照某个 Locale 的惯用法而输入或存储的数字,那么就需要使用 parse 方法。parse 方法能够处理小数点和分隔符以及其他语言中的数字

String source = "123.456,78 €";
NumberFormat fmt = NumberFormat.getNumberInstance(Locale.GERMANY);
Number number = fmt.parse(source.trim());
double doubleValue = number.doubleValue();
System.out.println(doubleValue);    // 123456.78

parse 的返回类型是抽象类型的 Number 。返回的对象是一个 DoubleLong 的包装器类对象,这取决于被解析的数字是否是浮点数。如果不关心两者的差异,可以直接使用 Number 类中的 doubleValue 方法来读取被包装的数字

警告Number 类型的对象并不能自动转换成相关的基本类型,因此,不能直接将一个 Number 对象赋给一个基本类型,而应该使用 doubleValueintValue 方法

如果数字文本的格式不正确,该方法会抛出一个 ParseException 异常。例如,字符串以空白字符开头是不允许的(可以调用 trim 方法来去掉它) 。但是,任何跟在数字之后的字符都将被忽略,所以这些跟在后面的字符是不会引起异常的

getXxxInstance 工厂方法返回的类并非是 NumberFormat 类型的。NumberFormat 类型是 个抽象类,而我们实际上得到的格式器是它的一个子类。工厂方法只知道如何定位属于特定 locale 对象

可以用静态的 getAvailableLocales 方法得到一个当前所支持的 Locale 对象列表。这个方法返回一个 Locale 对象数组,从中可以获得针对它们的数字格式器对象

可以使用 Scanner 来读取本地化的整数和浮点数,可以调用 useLocale 方法来设置 locale

java.text.NumberFormat 方法名称 方法声明 描述
getAvailableLocales public static Locale[] getAvailableLocales() 返回一个 Locale 对象的数组,其成员包含有可用的 NumberFormat 格式器
getNumberInstance
getCurrencyInstance
getPercentInstance
public final static NumberFormat getNumberInstance()
public static NumberFormat getNumberInstance(Locale inLocale)
public final static NumberFormat getCurrencyInstance()
public static NumberFormat getCurrencyInstance(Locale inLocale)
public final static NumberFormat getPercentInstance()
public static NumberFormat getPercentInstance(Locale inLocale)
为当前的或给定的 locale 提供处理数字、货币量或百分比的格式器
format public final String format(double number)
public final String format(long number)
对给定的浮点数或整数进行格式化并以字符串的形式返回结果
parse public Number parse(String source) throws ParseException 解析给定的字符串并返回数字值,如果输入字符串描述了一个浮点数,返回类型就是 Double ,否则返回类型就是 Long 。字符串必须以一个数字开头;以空白字符开头是不允许的。数字之后可以跟随其他字符,但它们都将被忽略。解析失败时抛出 ParseException 异常
setParseIntegerOnly
isParseIntegerOnly
public void setParseIntegerOnly(boolean value)
public boolean isParseIntegerOnly()
设置或获取一个标志,该标志指示这个格式器是否应该只解析整数值
setGroupingUsed
isGroupingUsed
public void setGroupingUsed(boolean newValue)
public boolean isGroupingUsed()
设置或获取一个标志,该标志指示这个格式器是否会添加和识别十进制分隔符(比如,100000 )
setMinimumIntegerDigits
getMinimumIntegerDigits
setMaximumIntegerDigits
getMaximumIntegerDigits
setMinimumFractionDigit
getMinimumFractionDigits
setMaximumFractionDigit
getMaximumFractionDigits
public void setMinimumIntegerDigits(int newValue)
public int getMinimumIntegerDigits()
public void setMaximumIntegerDigits(int newValue)
public int getMaximumIntegerDigits()
public void setMaximumFractionDigits(int newValue)
public int getMaximumFractionDigits()
设置或获取整数或小数部分所允许的最大或最小位数

货币( Currency

为了格式化货币值,可以使用 NumberFormat.getCurrencyInstance 方法。但是,这个方法的灵活性不好,它返回的是一个只针对 货币的格式器。假设你为一个美国客户准备了一张货物单,货单中有些货物的金额是用美元表示的,有些是用欧元表示的,此时,你不能只是使用两种格式器:

NumberFormat dollarFormatter = NumberFormat.getCurrencyInstance(Locale.US);
NumberFormat euroFormatter = NumberFormat.getCurrencyInstance(Locale.GERMANY);

这样一来,你的发票看起来非常奇怪,有些金额的格式像 $100 000 ,另一些则像 100.000 €

处理这样的情况,应该使用 Currency 类来控制被格式器所处理的货币

NumberFormat euroFormatter = NumberFormat.getCurrencyInstance(Locale.US);
String format = euroFormatter.format(100000);
System.out.println(format);     // $100,000.00


euroFormatter.setCurrency(Currency.getInstance("CNY"));
format = euroFormatter.format(100000);
System.out.println(format);     // CNY100,000.00


Currency currency = Currency.getInstance(Locale.CHINA);
System.out.println(currency);   // CNY

货币标识符由 ISO 4217 定义,参考

java.util.Currency 方法名称 方法声明 描述
getInstance public static Currency getInstance(String currencyCode)
public static Currency getInstance(Locale locale)
返回与给定的 ISO 4217 货币代号或给定的 Locale 中的国家相对应的 Curreny 对象
toString
getCurrencyCode
public String toString()
public String getCurrencyCode()
获取该货币的 ISO 4217 代码
getSymbol public String getSymbol()
public String getSymbol(Locale locale)
根据默认或给定的 Locale 得到该货币的格式化符号。比如美元的格式化符号可能是 `
---------------------------------- ------------------------------------------------------------ ------------------------------------------------------------
getInstance public static Currency getInstance(String currencyCode)
public static Currency getInstance(Locale locale)
返回与给定的 ISO 4217 货币代号或给定的 Locale 中的国家相对应的 Curreny 对象
toString
getCurrencyCode
public String toString()
public String getCurrencyCode()
获取该货币的 ISO 4217 代码
US$ ,具体是哪种形式取决于 Locale
getDefaultFractionDigits public int getDefaultFractionDigits() 获取该货币小数点后的默认位数
getAvailableCurrencies public static Set<Currency> getAvailableCurrencies() 获取所有可用的货币

日期和时间( DateTimeFormatter

当格式化日期和时间时 ,需要考虑 4 个与 Locale 的问题:

  • 月份和星期应该用本地语言来表示
  • 年月日的顺序要符合本地习惯
  • 公历可能不是本地首选的日期表示方法
  • 必须要考虑本地的时区

java.time.format.DateTimeFormatter 类可以处理这些问题:

FormatStyle dateStyle = FormatStyle.FULL;
FormatStyle timeStyle = FormatStyle.FULL;
FormatStyle dateTimeStyle = FormatStyle.FULL;
DateTimeFormatter dateFormatter = DateTimeFormatter.ofLocalizedDate(dateStyle);
DateTimeFormatter timeFormatter = DateTimeFormatter.ofLocalizedTime(timeStyle);
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofLocalizedDateTime(dateTimeStyle);
// DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofLocalizedDateTime(dateStyle, timeStyle);

DateTimeFormatter usDateTimeFormatter = dateTimeFormatter.withLocale(Locale.US);

ZonedDateTime zonedDateTime = ZonedDateTime.now();
System.out.println(dateTimeFormatter.format(zonedDateTime));    // 2022年2月11日 星期五 下午02时25分31秒 CST
System.out.println(usDateTimeFormatter.format(zonedDateTime));  // Friday, February 11, 2022 2:25:31 PM CST

格式器都会使用当前的 Locale 。为了使用不同的 Locale ,需要使用 withLocale 方法

可以格式化 LocalDateLocalDateTimeLocalTimeZonedDateTime

这里使用的是 java.time 包中的 DateTimeFormatter 。还有一种来自于 Java 1.1 的遗留的 java.text.DateFormatter 类,它可以操作 DateCalendar 对象

可以使用 LocalDateLocalDateTimeLocalTimeZonedDateTime 的静态的 parse 方法来解析字符串中的日期和时间:

DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL);
String dateStr = dateTimeFormatter.format(LocalDate.now());

LocalDate localDate = LocalDate.parse(dateStr, dateTimeFormatter);

LocalDate parse = LocalDate.parse("2022-02-11");

警告:日期格式器可以解析不存在的日期,例如 November 31 ,它会将这种日期调整为给定月份的最后一天

有时,你需要显示星期和月份的名字,例如在日历应用中 此时可以调用 DayOfWeekMonth 枚举的 getDisplayName 方法:

System.out.println("======== Month : ");
for (Month month : Month.values()) {
    System.out.println(month.getDisplayName(TextStyle.FULL, Locale.getDefault()));
}

System.out.println("======== DayOfWeek : ");
for (DayOfWeek dayOfWeek : DayOfWeek.values()) {
    System.out.println(dayOfWeek.getDisplayName(TextStyle.FULL, Locale.getDefault()));
}

注意:星期的第一天可以是星期六、星期日或星期一,这取决于 Locale ,可以这样获取星期的第一天:

DayOfWeek firstDayOfWeek = WeekFields.of(Locale.getDefault()).getFirstDayOfWeek();
System.out.println(firstDayOfWeek);     // SUNDAY

排序和范化( Collator

使用 String 类中的 compareTo 方法对字符串进行比较, compareTo 方法使用的是字符串的 UTF-16 编码值,这可能导致与正常认知不同的结果

为了获得 Locale 敏感的比较符,可以调用静态的 Collator.getInstance 方法:

List<String> words = new ArrayList<>();
Locale locale = Locale.getDefault();
Collator coll = Collator.getInstance(locale);
words.sort(coll);   // Collator implements java.util.Comparator<Object>

coll.setStrength(Collator.PRIMARY);     // 强度
coll.setDecomposition(Collator.NO_DECOMPOSITION);       // 范化

排序器有几个高级设置项,你可以设置排序器的 强度 以此来选择不同的排序行为。字符间的差别可以被分为首要的( primary )、其次的( secondary )和再次的( tertiary )

Unicode 标准对字符串定义了四种 范化 形式(normalization form) :DKDCKC

java.text.Collator 方法名称 方法声明 描述
getAvailableLocales public static synchronized Locale[] getAvailableLocales() 返回 Locale 对象的一个数组,该 Collator 对象可用于这些对象
getInstance public static synchronized Collator getInstance()
public static Collator getInstance(Locale desiredLocale)
为默认或给定的 locale 返回一个排序器
compare public abstract int compare(String source, String target); 如果 sourcetarget 之前,则返回负值;如果它们等价,则返回 0 ,否则返回正值
equals public boolean equals(String source, String target) 如果它们等价,则返回 true ,否则返回 false
setStrength
getStrength
public synchronized void setStrength(int newStrength)
public synchronized int getStrength()
设置或获取排序器的强度。更强的排序器可以区分更多的词
setDecomposition
getDecomposition
public synchronized void setDecomposition(int decompositionMode)
public synchronized int getDecomposition()
设置或获取排序器的分解模式。分解越细,判断两个字符串是否相等时就越严格
getCollationKey public abstract CollationKey getCollationKey(String source); 返回一个排序器键,这个键包含一个对一组字符按特定格式分解的结果,可以快速地和其他排序器键进行比较
java.text.CollationKey 方法名称 方法声明 描述
compareTo abstract public int compareTo(CollationKey target); 如果这个键在 target 之前,则返回一个负值;如果两者等价,则返回 0 ,否则返回正值
java.text.Normalizer 方法名称 方法声明 描述
normalize public static String normalize(CharSequence src, Form form) 返回 src 的范化形式

消息恪式化( MessageFormat

Java 类库中有一个 MessageFormat 类,它与用 printf 方法进行格式化很类似,但是它支持 Locale ,并且会对数字和日期进行格式化

格式化数字和日期( numbertimedate

String msg = MessageFormat.format("On 【 {2} 】, a 【 {0} 】 destroyed 【 {1} 】 houses and caused 【 {3} 】 of damage. ", "hurrricane", 99, new GregorianCalendar(1999, 0, 1).getTime(), 10.0E8);
System.out.println(msg);	// On 【 99-1-1 上午12:00 】, a 【 hurrricane 】 destroyed 【 99 】 houses and caused 【 1,000,000,000 】 of damage.

一般来说,占位符索引后面可以跟一个类型( type )和一个风格( style ),它们之间用逗号隔开。类型可以是:

number 
time 
date 
choice

如果类型是 number ,那么风格可以是

integer
currency
percent

或者可以是数字格式模式,就像 $,##0 (参见 DecimalFormat 类)

如果类型是 timedate ,那么风格可以是

short
medium
long
full

或者是一个日期格式模式,就像 yyyy-MM-dd (参见 SimpleDateFormat 类)

警告: 静态的 MessageFormat.format 方法使用当前的 locate 对值进行格式化。要想用任意的 locale 进行格式化,需要将要格式化的值置于 Object[] 数组

String pattern = "On 【 {2,date,short} 】, a 【 {0} 】 destroyed 【 {1} 】 houses and caused 【 {3,number,currency} 】 of damage. ";
Locale locale = Locale.getDefault();
MessageFormat mf = new MessageFormat(pattern, locale);
String msg =
        mf.format(new Object[]{"hurrricane", 99, new GregorianCalendar(1999, 0, 1).getTime(), 10.0E8});

System.out.println(msg);    // On 【 99-1-1 】, a 【 hurrricane 】 destroyed 【 99 】 houses and caused 【 ¥1,000,000,000.00 】 of damage.
java.text.MessageFormat 方法名称 方法声明 描述
构造器 public MessageFormat(String pattern)
public MessageFormat(String pattern, Locale locale)
用给定的模式和 locale 构建一个消息格式对象
applyPattern public void applyPattern(String pattern) 给消息格式对象设置特定的模式
setLocale
getlocale
public void setLocale(Locale locale)
public Locale getLocale()
设置或获取消息中占位符所使用的 locale 。这个 locale 仅仅被通过调用 applyPattern 方法所设置的后续模式所使用
format public static String format(String pattern, Object ... arguments) 通过使用 args[i]作为占位符 {i} 的输入来格式 pattern 字符串
format public final StringBuffer format(Object arguments, StringBuffer result, FieldPosition pos) 格式化 MessageFormat 模式。 args 参数必须是一个对象数组。被格式化的字符串会被附加到 result 末尾,并返回 result 。如果 pos 等于 new FieldPosition(MessageFormat.Field.ARGUMENT),就用它的 beginIndexendIndex 属性值来设置替换占位符 {1} 的文本位置。如果不关心位置信息,可以将它设为 null
java.text.Format 方法名称 方法声明 描述
format 按照格式器的规则格式化给定的对象,这个方法将调用 format(obj, new StringBuffer(), new FieldPosition(1)).toString()

选择格式( choice

我们希望消息能够随占位符的值而变化,这样就能根据具体的值形成

no houses 
one house 
2 houses

choice 格式化选项就是为了这个目的而设计的,一个选择格式是由一个序列对构成的,每一个对包括

  • 一个下限( lower limit)
  • 一个格式字符串( format string )

下限和格式字符串由一个 # 符号分隔,对与对之间由符号 | 分隔

{1, choice, 0#no houses|1#one house|2#{1} houses}

文本文件和字符集

Java 编程语言自身是完全基于 Unicode 。但是, Windows 和 Mac OS 仍旧支持遗留的字符编码机制,例如西欧国家的 Windows-1252 和 Mac Roman ,以及中国台湾的 Big5

文本文件

最好是使用 UTF-8 来存储和加载文本文件

可以在读写文本文件时指定字符编码:

PrintWriter out = new PrintWriter(filename, "Windows-1252");

如果想要获得最佳的编码机制,可以通过下面的调用来获得“平台的编码机制”:

Charset platformEncoding = Charset.defaultCharset();

行结束符

这不是 Locale 的问题,而是平台的问题。在 Windows 中,文本文件希望在每行末尾使用 \r\n ,而基于 UNIX 的系统只需要一个 \n 字符

任何用 println 方法写入的行都会是被正确终止的。唯一的问题是你是否打印了包含 \n 字符的行。它们不会被自动修改为平台的行结束符

与在字符串中使用 \n 不同,可以使用 printf%n 格式说明符来产生平台相关的行结束符

out.printf("Hello%nWorld%n");

控制台

如果你编写的程序是通过 System.in / System.out / System.console 与用户交互的,那么就不得不面对控制台使用的字符编码机制与 CharSet.defaultCharset() 报告的平台编码机制有所差异的可能性

Charset charset = Charset.defaultCharset();
System.out.println(charset);    // UTF-8

Windows 上的 cmd 工具,可以通过【属性-选项-当前代码页】,查看使用的字符集

切换控制台的字符编码为 UTF-8

chcp 65001

这种命令还不足 Java 在控制台中使用 UTF-8,我们还必须使用非官方的 file.encoding 系统属性来设置平台的编码机制:

java -Dfile.encoding=UTF-8 MyProg

UTF-8 字节顺序标志

如果你的应用必须读取其他程序 建的 UTF-8 文本文件,那么你可能会碰到另一个问题。在文件中添加一个“宇节顺序标志”字符 U+FEFF 作为文件的第一个字符,是一种完全合法的做法。在 UTF-16 编码机制中,每个码元都是一个两字节的数字,字节顺序标志可以告诉读入器该文件使用的是“高字节在前”还是“低字节在前”的字节顺序。UTF-8 是一种单字节编码机制,因此不需要指定字节的顺序。但是如果一个文件以字节 0xEF 0xBB 0xBF (U+FEFF 的 UTF-8 编码)开头,那么这就是一个强烈暗示,表示该文件使用了 UTF-8 。正是因为这个原因,Unicode 标准鼓励这种实践方式。任何读入器都被认为会丢弃最前面的字节顺序标志

Oracle 的 Java 实现很固执地因潜在的兼容性问题而拒绝遵循 Unicode 标准。作为程序员,这对你而言意味着必须去执行平台并不会执行的操作。在读入文本文件时,如果开头碰到了 U+FEFF ,那么就忽略它

警告: 遗憾的是, JDK 的实现没有遵循这项建议。在向 javac 编译器传递有效的以字节顺序标志开头的 UTF-8 源文件时,编译会以产生错误消息 illegal character: 65279 而失败

源文件的字符编码

作为程序员,要牢记你需要与 Java 编译器交互,这种交互需要通过本地系统的工具来完成。例如,可以使用中文版的记事本来写你的 Java 源代码文件。但这样写出来的源码不是随处可用的,因为它们使用的是本地的字符编码( GB 或 Big5 ),这取决于你使用的是哪种中文操作系统)。

只有编译后的 class 文件才能随处使用,因为它们会自动地使用 modified UTF-8 编码来处理标识符和字符串。这意味着即使在程序编译和运行时,依然有 3 种字符编码参与其中:

  • 源文件:本地编码
  • 类文件:modified UTF-8
  • 虚拟机:UTF-16

你可以用 -encoding 标记来设定你的源文件的字符编码,例如

javac -encoding UTF-8 Myfile.java

为了使你的源文件能够到处使用,必须使用普通的 ASCII 编码。也就是说,你需要将所有非 ASCII 字符转换成等价的 Unicode 编码。JDK 包含一个工具 native2ascii ,可以用它来将本地字符编码转换成普通的 ASCII 这个工具直接将输入中的每一个非 ASCII 字符替换为一个 \u 加四位十六进制数字的 Unicode 值

native2ascii Myfile.java Myfile.temp
# 逆向转换
native2ascii -reverse Myfile.temp Myfile.java
# 指定编码
native2ascii -encoding UTF-8 Myfile.java Myfile.temp

资源包

在外部定义消息字符串,通常称之 为资源 ( resource )

Class 类的 getResource 方法可以找到相应的文件,打开它并返回资源的 URL 。通过将文件放置到 JAR 文件中,你就将查找这些资源文件的工作留给了类的加载器去处理,加载器知道如何定位 JAR 文件中的项。但是,这种机制不支持 locale

定位资源包

当本地化一个应用时,会产生很多 资源包( resource bundle )。每一个包都是一个属性文件或者是一个描述了与 locale 相关的项的类(比如消息、标签等) 。对于每一个包,都要为所有你想要支持的 locale 提供相应的版本

需要对这些包使用一种统一的命名规则。例如,为德国定义的资源放在一个名为 包名_de_DE 的文件中,而为所有说德语的国家所共享的资源则放在名为 包名_de 的文件中。一般来说,使用 包名_语言_国家 来命名所有和国家相关的资源,使用 包名_语言 来命名所有和语言相关的资源。最后,作为后备,可以把默认资源放到一个没有后缀的文件中。

加载一个资源包:

String bundleName = "my";
ResourceBundle currentResources = ResourceBundle.getBundle(bundleName, currentLocale);
String v1 = currentResources.getString("k1");
System.out.println(v1);

getBundle 方法试图加载匹配当前 locale 定义的语言和国家的包。如果失败,通过依次放弃国家和语言来继续进行查找,然后同样的查找被应用于默认的 locale ,最后,如果还不行的话就去查看默认的包文件,如果这也失败了,则抛出一个 MissingResourceException 异常。

这就是说, getBundle 方法会试图加载以下的包

包名_当前 Locale 的语言_当前 Locale 的国家_当前 Locale 的变量
包名_当前 Locale 的语言_当前 Locale 的国家
包名_当前 Locale 的语言
包名_默认 Locale 的语言_默认 Locale 的国家_默认 Locale 的变量
包名_默认 Locale 的语言_默认 Locale 的国家
包名_默认 Locale 的语言
包名

一旦 getBundle 方法定位了一个包,比如,包名_de_DE ,它还会继续查找 包名_de包名 这两个包。如果这些包也存在,它们在资源层次中就成为了 包名_de_DE 的父包。以后,当查找一个资源时,如果在当前包中没有找到,就去查找其父包。就是说,如果一个特定的资源在当前包中没有被找到,比如,某个特定资源在 包名_de_DE 中没有找到,那么就会去查询 包名_de包名

不需要把你的程序的所有资源都放到同一个包中。可以用一个包来存放按钮标签,用另一个包存放错误消息等

属性文件

对字符串进行国际化是很直接的,你可以把所有字符串放到一个属性文件中,这是一个每行存放一个键 - 值对的文本文件。

警告:存储属性的文件都是 ASCII 文件。如果你需要将 Unicode 字符放到属性文件中,那么请用 \uxxxx 编码方式对它们进行编码

可以使用 native2ascii 工具来产生这些文件

包类

为了提供字符串以外的资源,需要定义类,它必需扩展自 ResourceBundle 。应该使用标准的命名规则来命名你的类,比如

MyProgramResources.java
MyProgramResources_en.java
MyProgramResources_de_DE.java

警告:当搜索包时,如果在类中的包和在属性文件中的包中都存在匹配,优先选择类中的包

Locale currentLocale = Locale.getDefault();

String bundleName = "v2ch07.myclass";

ResourceBundle currentResources = ResourceBundle.getBundle(bundleName, currentLocale);
String v1 = currentResources.getString("k1");
System.out.println(v1);     // class_vv1
package v2ch07;

import java.util.ListResourceBundle;

public class myclass_zh_CN extends ListResourceBundle {

    private static final Object[][] contents = {{"k1", "class_vv1"}};


    @Override
    protected Object[][] getContents() {
        return contents;
    }
}
java.util.ResourceBundle 方法名称 方法声明 描述
getBundle public static final ResourceBundle getBundle(String baseName)
public static final ResourceBundle getBundle(String baseName, Locale locale)
在给定的 locale 或默认 locale 下以给定的名字加载资源绑定类和它的父类。如果资源包类位于一个 Java 包中,那么类的名字必须包含完整的包名。资源包类必须是 public 的,这样 getBundle 方法才能访问它们
getObject public final Object getObject(String key) 从资源包或它的父包中查找一个对象
getString public final String getString(String key) 从资源包或它的父包中查找一个对象并把它转型成字符串
getStringArray public final String[] getStringArray(String key) 从资源包或它的父包中查找一个对象并把它转型成字符串数组
getKeys public abstract Enumeration<String> getKeys(); 返回一个枚举对象,枚举出资源包中的所有键,也包括父包中的键
handleGetObject protected abstract Object handleGetObject(String key); 如果你要定义自己的资源查找机制,那么这个方法就需要被覆写,用来查找与给定的键相关联的资源的值
posted @ 2022-04-24 21:19  流星<。)#)))≦  阅读(96)  评论(0编辑  收藏  举报