20220424 Java核心技术 卷2 高级特性 7
国际化
Java 编程语言是第一种设计成为全面支持国际化的语言。从一开始,它就具备了进行有效的国际化所必需的一个重要特性:使用 Unicode 来处理所有字符串。支持 Unicode 使得在 Java 编程语言中,编写程序来操作多种语言的字符串变得异常方便
国际化一个程序所要做的事情绝不仅仅是提供 Unicode 支持。在世界的不同地方,日期、时间、货币甚至数字的格式都不相同
Locale
对象( Locale
)
不同的国家可以使用相同的语言
为了对格式化进行控制,可以使用 Locale
类。locale
由多达 5 个部分构成:
- 一种语言,由 2 个或 3 个小写字母表示,例如
en
(英语)、de
(德语)和zh
(中文) - 可选的一段脚本,由首字母大写的四个字母表示,例如 Latn (拉丁文)、 Cyrl (西里尔文)和 Hant (繁体中文字符) 这个部分很有用,因为有些语言,例如塞尔维亚语,可以用拉丁文或西里尔文书写, 而有些中文读者更喜欢阅读繁体中文而不是简体中文
- 可选的一个国家或地区,由 2 个大写字母或 3 个数字表示,例如
US
(美国)和CH
(瑞士) - 可选的一个变体,用于指定各种杂项特性,例如方言和拼写规则。变体现在已经很少使用了。过去曾经有一种挪威语的变体“尼诺斯克语”,但是它现在已经用另一种不同的代码
nn
来表示了。过去曾经用于日本帝国历和泰语数字的变体现在也都被表示成了扩展 - 可选的一个扩展。扩展描述了日历(例如日本历)和数字(替代西方数字的泰语数字)等内容的本地偏好。Unicode 标准规范了其中的某些扩展,这些扩展应该以
u-
和两个字母代码开头,这两个字母的代码指定了该扩展处理的是日历(ca
)还是数字(nu
),或者是其他内容。例如,扩展u-nu-thai
表示使用泰语数字。其他扩展是完全任意的,并且以x-
开头,例如x-java
locale
是用标签描述的,标签是由 locale
的各个元素通过连字符连接起来的字符串,例如 en-US
在德国,你可以使用 de-DE
。瑞士有 4 种官方语言(德语、法语、意大利语和里托罗曼斯语)。在瑞士讲德语的人希望使用的 locale
是 de-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
的数字进行格式化:
- 使用上一节的方法,得到
Locale
对象 - 使用一个“工厂方法”得到一个格式器对象
- 使用这个格式器对象来完成格式化和解析工作
工厂方法是 java.text.NumberFormat
类的静态方法,它们接受一个 Locale
类型的参数。总共有 3 个工厂方法: getNumberInstance
、 getCurrencyInstance
、getPercentInstance
。这些方法返回的对象可以分别对数字、货币量和百分比进行格式化和解析
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
。返回的对象是一个 Double
或 Long
的包装器类对象,这取决于被解析的数字是否是浮点数。如果不关心两者的差异,可以直接使用 Number
类中的 doubleValue
方法来读取被包装的数字
警告: Number
类型的对象并不能自动转换成相关的基本类型,因此,不能直接将一个 Number
对象赋给一个基本类型,而应该使用 doubleValue
或 intValue
方法
如果数字文本的格式不正确,该方法会抛出一个 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
方法
可以格式化 LocalDate
、LocalDateTime
、LocalTime
、ZonedDateTime
这里使用的是 java.time
包中的 DateTimeFormatter
。还有一种来自于 Java 1.1 的遗留的 java.text.DateFormatter
类,它可以操作 Date
和 Calendar
对象
可以使用 LocalDate
、LocalDateTime
、LocalTime
、ZonedDateTime
的静态的 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 ,它会将这种日期调整为给定月份的最后一天
有时,你需要显示星期和月份的名字,例如在日历应用中 此时可以调用 DayOfWeek
和 Month
枚举的 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) :D
、 KD
、 C
、 KC
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); |
如果 source 在 target 之前,则返回负值;如果它们等价,则返回 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
,并且会对数字和日期进行格式化
格式化数字和日期( number
、 time
、 date
)
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
类)
如果类型是 time
或 date
,那么风格可以是
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) ,就用它的 beginIndex 和 endIndex 属性值来设置替换占位符 {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); |
如果你要定义自己的资源查找机制,那么这个方法就需要被覆写,用来查找与给定的键相关联的资源的值 |