Java提供给我们软件国际化的解决方案,这些国际化API基于Unicode标准,并且包括文本、(货币)数字、日期以及用户自定义对象的适配,从而使得软件能够应用到任何国家或地区。国际化英文为“Internationalization”,通常简写成i18n
(实际开发经常使用简写)。更多有关Java国际化的内容可以参考Oracle的相关网站。
文字国际化
Java语言基于Unicode字符集。Unicode是一种国际字符集标准,支持世界上所有主要文字以及常见技术符号。早期Unicode规定所有字符固定16bit宽(也就是UCS-2),但是Unicode标准中的字符早已超过16bit所能表示的范围,现规定的代码点范围在U+0000到U+10FFFF之间。Java标准使用的UTF-16定义的编码方式允许使用一个或两个16bit单位来表示所有的Unicode代码点。
Java基本数据类型char是一个无符号的16bit整数,可以表示U+0000到U+FFFF范围内的码点(BMP平面字符),同时也可作为UTF-16的码元,即32bit编码中的一半(两个char拼成一个字的辅助平面字符,但这种字符使用频率很低)。
更多有关字符编码的知识可以参考我的这篇文章
Java平台中表示字符序列的各种类型如:char[]、java.lang.CharSequence的实现类(如String类)以及java.text.CharacterIterator的实现类都是UTF-16序列。大多数Java源代码是使用ASCII(一种7bit字符编码方式)或ISO-8859-1(西欧8bit字符编码)或GBK(中国国标扩展编码)所编写的,但在处理之前都被转换为UTF-16编码。
Character
类作为基本数据类型char的包装类,里面有很多确定字符属性的静态方法,如isLowerCase
、isTitleCase
和isDigit
等,在Java5之前这些方法只有char
作为参数,所以只接受U+0000到U+FFFF范围内的码点,Java5之后这些方法有了int
类型作为参数的重载方法,这样就能表示所有Unicode代码点。
区域识别与本地化
1. Locale—地区性
Java平台将语言和地区分开定义,但均使用Locale
类表示,Locale类通常代表特定地理位置,政权或文化区域。国际化的API中都有重载的方法要求提供一个Locale类的实例,而Locale
类中有许多的静态常量用于表示常见的国家和地区的本地化对象。比如:Local.CHINA
,Local.CANADA
,Local.JAPEN
。
官网有Java支持的所有国家或地区语言的列表
2. ResourceBundle—国际化资源
通常我们的应用或系统不会将字符(串)等信息硬编码编译到Java字节码中,而是将这些信息以资源文件的形式存储在磁盘中,这是我们通常需要ResourceBundle这个类。
ResourceBundle是一个抽象类,代表着程序中所使用资源集合(字符串或图片路径等)。将资源和Java类一起打包,可以利用Java类加载机制来查找资源,ResourceBundle中包含特定区域的资源对象,当一个程序需要该区域的资源,程序可以从资源包中加载它,这样开发人员编写代码,可以不用关注特定区域的资源问题(资源文件可以单独维护)。
Java平台资源文件主要是
properties
格式
ResourceBundle中有几个重载的静态方法getBundle
可以用来根据资源名(不包括区域信息的基本名称)来获取ResourceBundle对象,有ResourceBundle对象后即可通过getString
,getObject
等方法传入Key即可获取对应的Value。
示例:
比如你有两个特定区域的资源:MyResource_en.properties
和MyResource_zh.properties
:
# properties文件内容
key=value
java代码获取资源文件内容
java
// Java会根据当前系统默认区域获取对应资源,如果没有则使用无区域信息的默认资源
ResourceBundle res = ResourceBundle.getBundle("MyResource");
// 通过key-value的方式取得资源内的信息
String value = res.getString("key");
ResourceBundle的子类
ResourceBundle有两个子类:ListResourceBundle和PropertyResourceBundle。
1、ListResourceBundle是一个抽象类,需要我们继承并实现Object[][] getContents()
方法:
// 默认 (美国英语)
public class MyResources extends ResourceBundle {
public Object handleGetObject(String key) {
if (key.equals("okKey")) return "Ok";
if (key.equals("cancelKey")) return "Cancel";
return null;
}
public Enumeration<String> getKeys() {
return Collections.enumeration(keySet());
}
// 重写handleKeySet方法并在getKeys方法中调用keySet方法
// keySet可以获取handleKeySet的返回值
protected Set<String> handleKeySet() {
return new HashSet<String>(Arrays.asList("okKey", "cancelKey"));
}
}
// 中文
public class MyResources_zh extends MyResources {
public Object handleGetObject(String key) {
// 对于不需要重写的内容可以从父类中获取
if (key.equals("cancelKey")) return "取消";
return null;
}
protected Set<String> handleKeySet() {
return new HashSet<String>(Arrays.asList("cancelKey"));
}
}
可以看出JDK的sun包中有很多这种方式书写的资源:
2、PropertyResourceBundle就是ResourceBundle通过get方法获取资源的形式,它使用的是Properties
工具类获取.properties
文件中的信息。
日期与时间的处理
java.util.Date类代表着毫秒精度的日期时间,该类有几个方法可以获取日期中的年、月、日、时、分、秒等信息。但是由于该类依赖于时区,与国际化不兼容,大部分方法已经被弃用,通常我们使用Calendar类来转换日期和时间,并通过DateFormat类来格式化或解析日期时间字符串。
Calendar
Calendar是一个抽象类,它用于一个整数的计算机纪元点时间转换成年、月、日、星期等信息。GregorianCalendar
类是Calendar实现类,它根据格林尼治时间历法(公历)来实现。同时Calendar类提供了getInstance
静态工厂方法来创建Calendar实例对象。
TimeZone
TimeZone是一个抽象类,封装了一个相对于GMT(格林尼治标准时间)的偏移值。TimeZone.getTimeZone
工厂方法能通过指定zoneID来创建一个TimeZone实例对象。另外TimeZone还有一个SimpleTimeZone实现类可以创建TimeZone对象。
Calendar类及其子类会使用TimeZone进行本地时间与UTC(通用标准时间)之间的转换。
格式化
i18n中重要的一个部分就是文本格式化,这些功能主要在java.text
包中:
主要分为三类:
- DateFormat:日期格式化。根据区域不同使用不同的日期格式,可以自定义日期显示的格式。
- MessageFormat:信息格式化。根据区域不同使用不同语言的字符显示信息,比如谷歌的网站对于不同的国家会以不同的语言显示网页信息。
- NumberFormat:数字格式化。根据不同区域使用不同的数字格式,比如货币显示,西方国家会每隔三位加一个空格或逗号。
Format类主要有两个抽象方法需要子类去实现:
format
方法用于将对象格式化成字符串。parseObject
方法用于将字符串按格式解析成对象。
DateFormat和NumberFormat这两个是抽象类,它们都有静态工厂方法getXxxInstance来获取该类的实例对象,同时它们也提供了子类可以实例化。
日期时间格式化
DateFormat
DateFormat有几个静态工厂方法getXxxInstance,DateFormat提供了几个标准的时间格式:
/**
* Constant for full style pattern.
*/
public static final int FULL = 0;
/**
* Constant for long style pattern.
*/
public static final int LONG = 1;
/**
* Constant for medium style pattern.
*/
public static final int MEDIUM = 2;
/**
* Constant for short style pattern.
*/
public static final int SHORT = 3;
默认时期格式为:
/**
* Constant for default style pattern. Its value is MEDIUM.
*/
public static final int DEFAULT = MEDIUM;
通过getXxxInstance
方法可以传入这些常量设置日期或时间的格式,这几个getXxxInstance
方法最终都辗转调用下面这个方法:
private static DateFormat get(int timeStyle, int dateStyle, int flags, Locale loc)
其中timeStyle
代表时间的显示格式(时分秒部分),dateStyle
代表日期的显示格式(年月日部分),这两个参数就是上面四个常量;flags
参数有三种状态:1表示值显示时间部分,2表示只显示日期部分,3表示日期时间都显示;loc
参数用于指定国家和地区,从而应对不同国家的时间格式。
DateFormat底层使用Calendar和TimeZone(Calendar内部)来解析时间,可以通过这两个属性的getter/setter方法来设置日期和时区。
有了DateFormat对象并设置了日期时间后即可调用它的format(格式化日期)或parse(解析日期)方法。
DateFormat df = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG,
Locale.CHINA);
String timeString = df.format(System.currentTimeMillis());
System.out.println(timeString);
Date parse = df.parse(timeString);
System.out.println(parse);
System.out.println(parse.getTime());
输出结果:
2017年8月4日 上午11时04分31秒
Fri Aug 04 11:04:31 CST 2017
1501815871000
SimpleDateFormat
SimpleDateFormat是DateFormat的子类,可以由开发者自己定义解析格式。
示例:
SimpleDateFormat df = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
String timeStr1 = df.format(System.currentTimeMillis());
System.out.println(timeStr1);
df.applyPattern("yyyy年MM月dd日-HH时mm分ss秒S毫秒");
String timeStr2 = df.format(System.currentTimeMillis());
System.out.println(timeStr2);
输出结果:
2017/08/04 11:03:23
2017年08月04日-11时03分23秒119毫秒
官方API文档中给出了SimpleDateFormat的各种pattern的规则。
数字格式化
NumberFormat
NumberFormat和DateFormat类似,也提供了几个标准的数字格式:
private static final int NUMBERSTYLE = 0; // 默认数字风格
private static final int CURRENCYSTYLE = 1; // 货币风格
private static final int PERCENTSTYLE = 2; // 百分比风格
private static final int SCIENTIFICSTYLE = 3; // 科学计数风格
private static final int INTEGERSTYLE = 4; // 整数风格
我们不需要指定数字格式,NumberFormat已经为我们封装了这几个风格样式并提供给我们静态方法:
public final static NumberFormat getInstance(); // NUMBERSTYLE
public final static NumberFormat getInstance(Locale inLocale); // NUMBERSTYLE
public final static NumberFormat getNumberInstance(); // NUMBERSTYLE
public final static NumberFormat getNumberInstance(Locale inLocale); // NUMBERSTYLE
public final static NumberFormat getIntegerInstance(); // INTEGERSTYLE
public final static NumberFormat getIntegerInstance(Locale inLocale); // INTEGERSTYLE
public final static NumberFormat getCurrencyInstance(); // CURRENCYSTYLE
public final static NumberFormat getCurrencyInstance(Locale inLocale); // CURRENCYSTYLE
public final static NumberFormat getPercentInstance(); // PERCENTSTYLE
public final static NumberFormat getPercentInstance(Locale inLocale); // PERCENTSTYLE
/*public*/ final static NumberFormat getScientificInstance(); // SCIENTIFICSTYLE
/*public*/ static NumberFormat getScientificInstance(Locale inLocale); // SCIENTIFICSTYLE
示例:
NumberFormat[] formats = {
NumberFormat.getInstance(),
NumberFormat.getNumberInstance(),
NumberFormat.getIntegerInstance(),
NumberFormat.getCurrencyInstance(),
NumberFormat.getPercentInstance()
};
for (int i = 0; i < formats.length; i++) {
System.out.println(formats[i].format(100L));
System.out.println(formats[i].format(123.456F));
}
输出结果:
100
123.456
100
123.456
100
123
¥100.00
¥123.46
10,000%
12,346%
DecimalFormat
该类允许我们自定义十进制数字的格式化。
示例:
DecimalFormat format;
format = new DecimalFormat("####,####,###0.0# 圆整");
System.out.println(format.format(12345678.90F));
System.out.println(format.format(-12345678.90F));
format.applyPattern("0.0#####E0####");
System.out.println(format.format(1234567894987654321.90F));
System.out.println(format.format(12.34F));
format.applyPattern("00.0#E0##");
System.out.println(format.format(1234567894987654321.90F));
System.out.println(format.format(12.34F));
运行结果:
1234,5679.0 圆整
-1234,5679.0 圆整
1.2345679396E18
1.2340000153E1
12.3457E17
12.34E0
信息格式化
所谓“信息”格式化,其实就是字符串格式化,可以通过MessageFormat这个类构造一个字符串显示给终端用户。不过在这之前要说说JDK1.0开始就已经存在的老牌API——java.util.Formatter
。
java.util.Formatter
我们知道String类中有一个静态方法String.format(String format, Object... args)
和C语言中的sprintf
函数极为相似,这个方法底层就是用了java.util.Formatter
类来实现,而待会儿要讲的MessageFormat
类功能远比这个类功能强大(指的是格式化字符串这方面),所以在这之前我们先来说说java.util.Formatter
这个类。
我们知道C语言中有几个I/O流的函数:printf
,sprintf
,fprintf
(在C11标准中还提供了这几个方法的安全版本,防止溢出)。
而java.util.Formatter
类能实现类似于C语言中的这几个函数,只需要在构造方法中传入不同的输出流即可,下面列一个表把C语言中的函数与Java中的Formatter类的应用进行对比:
C语言函数 | Java API | 备注 |
---|---|---|
printf(const char *format, ...) |
System.out.format(String format, Object ... args) |
System.out是一个PrintStream对象,该对象底层使用java.util.Formatter 对象进行格式化输出。 |
sprintf( char *buffer, const char *format, ... ) |
String.format(String format, Object... args) |
String.format方法每次调用都会new一个Formatter对象。 |
fprintf( FILE *stream, const char *format, ... ) |
new Formatter(new File(filename)).format(String format, Object ... args) |
Formatter类可以直接封装输出流,你可以直接传入一个文件名或文件对象。 |
但是由于Java是面向对象的语言,format参数是Object类型,所以如果我们想要格式化输出自定义对象,则该对象必须实现java.util.Formattable
接口,但实际上JDK中没有任何类实现了该接口,这样做还不如重载Object.toString
方法。另外由于java中String是不可变的对象,所以String.format
方法并没有像sprintf
函数一样灵活性。
而相反MessageFormat类中有一个方法: