国际化与格式化
全球化的Internet需要全球化的软件。全球化软件,以为着同一种版本的产品能够容易地适用于不同地区的市场,软件的全球化意味着国际化和本地化。
国际化的英文是Internationalization,因为这个单侧太长,有时也简称为I18N,一个国际化很好的语言在不同区域使用时,会呈现出本地语言的提示。这个过程称为Localization,即本地化,可简称为L10N。
Java国际化思路
Java程序的国际化思路是将程序中的标签、提示等信息放在资源文件中,程序需要支持哪些国家、语言环境,就对应提供相应的资源文件。资源文件是key-value对,每个资源文件中的key是不变的,但value则随不同的国家、语言而改变。
Java程序的国际化主要通过如下三个类实现:
- java.util.ResourceBundle:用于加载国家、语言资源包。
- java.util.Locale:用于封装特定的国家/区域、语言环境。
- java.text.MessageFormat:用于格式化带占位符的字符串。
为了实现程序的国际化,必须先提供程序所需要的资源文件。资源文件的内容是很多key-value对,其中key是程序使用的部分,而value则是程序界面的显示字符串。
资源文件的命名可以有如下三种形式。
- baseName_language_country.properties
- baseName_language.properties
- baseName.properties
其中baseName是资源文件的基本名,用户可随意指定;而language和country都不可随意变化,必须是Java所支持的语言和国家。
Java支持的国家和语言
事实上,Java不可能支持所有的国家和语言,如果需要获取Java支持的国家和语言,则可调用Locale类的getAvailableLoacle()方法,该方法返回一个Locale数组,该数组里包含了Java所支持的国家和语言。
下面的程序简单地示范了如何获取Java所支持的国家和语言:
import java.util.Locale;
public class LocaleList {
public static void main(String[] args){
//返回Java所支持的全部国家的语言数组
Locale[] localeList=Locale.getAvailableLocales();
//遍历数组的每个元素,以此获取所支持的国家和语言
for (int i=0; i<localeList.length; i++){
//输出所支持的国家和语言
System.out.println(localeList[i].getDisplayCountry()
+ "=" + localeList[i].getCountry() + " "
+ localeList[i].getDisplayLanguage()
+ "=" + localeList[i].getLanguage());
}
}
}
完成程序国际化
对于如下最简单的程序:
public class RawHello
{
public static void main(String[] args)
{
System.out.println("Hello World");
}
}
这个程序的执行结果也很简单——肯定是打印出简单的“Hello World”字符串,不管在哪里执行都不会有任何改变!为了让该程序支持国家化,肯定不能让程序直接输出“Hello World”字符串,这种写法直接输出一个字符串常量,永远不会有任何改变。为了让程序可以输出不同的字符串,此处绝不可使用该字符串常量。
为了让上面输出的字符串常量可以改变,我们将需要输出的各种字符串(不同的国家/语言环境对应不同的字符串)定义在资源包中。
我们为上面程序提供如下两个文件。
第一个文件:mess.properties,该文件的内容为:
#资源文件的内容是key-value对
hello=您好!
第二个文件:mess_en_US.properties,该文件的内容为:
#资源文件的内容是key-value对
hello=Welcome You!
对于包含非西欧字符的资源文件,Java提供了一个工具来处理该文件:native2ascii,这个工具可以在%JAVA_HOME%/bin路径下找到。使用该工具的语法格式如下:
native2ascii 源资源文件 目的资源文件
在命令窗口输入如下命令:
native2ascii mess.properties mess_zh_CN.properties
上面的命令将生成一个mess_zh_CN.properties文件,该文件才是我们需要的资源文件,该文件看上去包含很多乱码,其实是非西欧字符的Unicode编码方式。
我们看到这两份文件文件名的baseName是相同的:mess。前面已经介绍了资源文件的三种命名方式,其中baseName后面的国家、语言必须是Java所支持的国家、语言组合。将上面的Java程序修改成如下形式:
import java.util.Locale;
import java.util.ResourceBundle;
public class Hello {
public static void main(String[] args){
//取得系统默认的国家、语言环境
Locale myLocale= Locale.getDefault();
//根据指定的国家、语言环境加载资源文件
ResourceBundle bundle=ResourceBundle.getBundle("mess", myLocale);
//打印从资源文件中取得的消息
System.out.println(bundle.getString("hello"));
}
}
上面程序中的打印语句不再是直接打印“Hello World”字符串,而是打印从资源包中读取的信息。如果在中文环境下运行该程序,将打印“您好!”;如果在“控制面板”中将机器的语言环境设置成美国,然后再次运行该程序,将打印“Welcome You”。
从上面程序可以看出,如果希望程序完成国际化,只需要将不同的国家/语言(Locale)的提示信息分别以不同的文件存放即可。例如,简体中文的语言资源文件就是Xxx_zh_CN.properties文件,而美国英语的语言资源文件就是Xxx_en_US.properties文件。
Java程序国际化的关键类是ResourceBundle,它有一个静态方法:getBundle(String baseName, Locale locale),该方法将根据Locale加载资源文件,而Locale封装了一个国家、语言,例如,简体中文环境可以用简体中文的Locale代表,美国英语环境可以用美国英语的Locale代表。
使用MessageFormat处理包含占位符的字符串
上面程序中输出的消息是一个简单消息,如果需要输出的消息中必须包含动态的内容,例如,这些内容必须是从程序中取得的。比如如下字符串:‘
您好,yeeku!今天是2011-5-30 下午11:55.
在上面的输出字符串中,yeeku是浏览者的名字,必须动态改变,后面的时间也必须动态改变。在这种情况下,我们可以使用带占位符的消息,如下:
msg=Hello,{0}!Today is {1}.
使用MessageFormat类,该类包含一个有用的静态方法。
- format(String pattern , Object… values):返回后面的多个参数值填充前面的pattern字符串,其中pattern字符串不是正则表达式,而是一个带占位符的字符串。
借助于上面的MessageFormat类的帮助,将国际化程序修改成如下形式:
import java.text.MessageFormat;
import java.util.Date;
import java.util.Locale;
import java.util.ResourceBundle;
public class HelloArg
{
public static void main(String[] args)
{
//定义一个Locale变量
Locale currentLocale=null;
//如果运行程序指定了两个参数
if (args.length==2)
{
//使用运行程序的两个参数构造Locale实例
currentLocale=new Locale(args[0] , args[1]);
}
else
{
//否则直接使用系统默认的Locale
currentLocale=Locale.getDefault();
}
//根据Locale加载语言资源
ResourceBundle bundle=ResourceBundle
.getBundle("myMess" , currentLocale);
//取得已加载的语言资源文件中msg对应消息
String msg=bundle.getString("msg");
//使用MessageFormat为带占位符的字符串传入参数
System.out.println(MessageFormat.format(msg
, "yeeku" , new Date()));
}
}
上面的程序中可以看出,对于带占位符的消息字符串,只需要使用MessageFormat类的format方法为消息中的占位符指定参数即可。
使用类文件代替资源文件
使用属性文件简单、快捷,但有时候我们希望以类文件作为资源文件。Java允许使用类文件代替资源文件,即将所有的key-value对存入class文件,而不是属性文件。
使用类文件来代替资源文件必须满足如下条件。
- 类的名字必须为baseName_language_country,这与属性文件的命名相似。
- 该类必须继承ListResourceBundle,并重写getContents()方法,该方法返回Object数组,该数组的每一项都是key-value对。
下面的类文件可以代替上面的属性文件:
import java.util.ListResourceBundle;
public class myMess_zh_CN extends ListResourceBundle
{
//定义资源
private final Object myData[][]=
{
{"msg","{0},你好!今天的日期是{1}"}
};
//重写getContents()方法
public Object[][] getContents()
{
//该方法返回资源的key-value对
return myData;
}
}
上面文件是一个简体中文语言环境的资源文件,该文件可以代替myMess_zh_CN.properties文件;如果需要代替美国英语语言环境的资源文件,则还应该提供一个myMess_en_US类。
如果系统同时存在资源文件、类文件,系统将以类文件为主,而不会调用资源文件。对于简体中文的Locale,ResourceBundle搜索资源文件的顺序是:
- baseName_zh_CN.class
- baseName_zh_CN.properties
- baseName_zh.class
- baseName_zh.properties
- baseName.class
- baseName.properties
系统按上面的顺序搜索资源文件,如果前面的文件不存在,才会使用下一个文件。如果一直找不到对应的文件,系统将抛出异常。
使用NunberFormat格式化数字
NumberFormat也是一个抽象基类,所以无法通过它的构造器来创建NumberFormat对象,它提供了如下几个工厂方法来得到NumberFormat对象。
- getCurrencyInstance():返回默认Locale的货币格式器。也可以在调用该方法时传入指定的Locale,则获取指定Locale的货币格式器。
- getIntegerInstance():返回默认Locale的整数格式器。也可以在调用该方法时传入指定的Locale,则获取指定Locale的整数格式器。
- getNumberInstance():返回默认Locale的通用数值格式器。也可以在调用该方法时传入指定的Locale,则获取指定Locale的通用数值格式器。
- getPercentInstance():返回默认Locale的百分数格式器。也可以在调用该方法时传入指定的Locale,则获取指定Locale的百分数格式器。
一旦取得了NumberFormat对象后,就可以调用它的format()方法来格式化数值,包括整数和浮点数。如下例子程序示范了NumberFormat的3种数字格式化器的用法:
import java.text.NumberFormat;
import java.util.Locale;
public class NumberFormatTest
{
public static void main(String[] args)
{
//需要被格式化的数字
double db=1234000.567;
//创建四个Locale,分别代表中国、日本、德国、美国
Locale[] locales={Locale.CHINA, Locale.JAPAN
, Locale.GERMAN, Locale.US};
NumberFormat[] nf=new NumberFormat[12];
//为上面四个Locale创建12个NumberFormat对象
//每个Locale分别有通用数值格式器、百分数格式器、货币格式器
for (int i=0 ; i < locales.length ; i++)
{nf[i * 3]=NumberFormat.getNumberInstance(locales[i]);nf[i * 3 + 1]=NumberFormat.getPercentInstance(locales[i]);nf[i * 3 + 2]=NumberFormat.getCurrencyInstance(locales[i]); }
for (int i=0 ; i < locales.length ; i++)
{
switch (i)
{
case 0:
System.out.println("-------中国的格式--------");
break;
case 1:
System.out.println("-------日本的格式--------");
break;
case 2:
System.out.println("-------德国的格式--------");
break;
case 3:
System.out.println("-------美国的格式--------");
break;
}
System.out.println("通用数值格式:"
+ nf[i * 3].format(db));
System.out.println("百分比数值格式:"
+ nf[i * 3 + 1].format(db));
System.out.println("货币数值格式:"
+ nf[i * 3 + 2].format(db));
}
}
}
其结果如下:
-------中国的格式--------
通用数值格式:1,234,000.567
百分比数值格式:123,400,057%
货币数值格式:¥1,234,000.57
-------日本的格式--------
通用数值格式:1,234,000.567
百分比数值格式:123,400,057%
货币数值格式:¥1,234,001
-------德国的格式--------
通用数值格式:1.234.000,567
百分比数值格式:123.400.057%
货币数值格式:¤ 1.234.000,57
-------美国的格式--------
通用数值格式:1,234,000.567
百分比数值格式:123,400,057%
货币数值格式:$1,234,000.57
德国的小数点比较特殊,它们采用逗号(,)作为小数点;中国、日本使用¥作为货币符号,而美国则采用$作为货币符号。细心的读者可能会发现,NumberFormat其实也有国际化的作用!没错,同样的数值在不同国家的写法是不同的,而NumberFormat的作用就是把数值转换成不同国家的本地写法。
使用DateFormat格式化日期、时间
与NumberFormat相似的是,DateFormat也是一个抽象类,它也提供了几个工厂方法用于获取DateFormat对象。
- getDateInstance():返回一个日期格式器,它格式化后的字符串只有日期,没有时间。该方法可以传入多个参数,用于指定日期样式和Locale等参数;如果不指定这些参数,则使用默认参数。
- getTimeInstance():返回一个时间格式器,它格式化后的字符串只有时间,没有日期。该方法可以传入多个参数,用于指定时间样式和Locale等参数;如果不指定这些参数,则使用默认参数。
- getDateTimeInstance():返回一个日期、时间格式器,它格式化后的字符串既有日期,也有时间。该方法可以传入多个参数,用于指定日期样式、时间样式和Locale等参数;如果不指定这些参数,则使用默认参数。
上面3个方法可以指定日期样式、时间样式参数,它们是DateFormat的4个静态常量:FULL、LONG、MEDIUM和SHORT,通过这4个样式参数可以控制生成的格式化字符串:
import java.text.DateFormat;
import java.util.Date;
import java.util.Locale;
public class DateFormatTest {
public static void main(String[] args){
//需要被格式化的时间
Date dt=new Date();
//创建两个Locale,分别代表中国、美国
Locale[] locales={Locale.CHINA, Locale.US};
DateFormat[] df=new DateFormat[16];
//为上面两个Locale创建16个DateFormat对象
for (int i=0 ; i < locales.length ; i++) {
df[i * 8]=DateFormat.getDateInstance(DateFormat.SHORT, locales[i]);
df[i * 8 + 1]=DateFormat.getDateInstance(DateFormat.MEDIUM, locales[i]);
df[i * 8 + 2]=DateFormat.getDateInstance(DateFormat.LONG, locales[i]);
df[i * 8 + 3]=DateFormat.getDateInstance(DateFormat.FULL, locales[i]);
df[i * 8 + 4]=DateFormat.getTimeInstance(DateFormat.SHORT, locales[i]);
df[i * 8 + 5]=DateFormat.getTimeInstance(DateFormat.MEDIUM , locales[i]);
df[i * 8 + 6]=DateFormat.getTimeInstance(DateFormat.LONG , locales[i]);
df[i * 8 + 7]=DateFormat.getTimeInstance(DateFormat.FULL , locales[i]);
}
for (int i=0 ; i < locales.length ; i++) {
switch (i)
{
case 0:
System.out.println("-------中国日期格式--------");
break;
case 1:
System.out.println("-------美国日期格式--------");
break;
}
System.out.println("SHORT格式的日期格式:"
+ df[i * 8].format(dt));
System.out.println("MEDIUM格式的日期格式:"
+ df[i * 8 + 1].format(dt));
System.out.println("LONG格式的日期格式:"
+ df[i * 8 + 2].format(dt));
System.out.println("FULL格式的日期格式:"
+ df[i * 8 + 3].format(dt));
System.out.println("SHORT格式的时间格式:"
+ df[i * 8 + 4].format(dt));
System.out.println("MEDIUM格式的时间格式:"
+ df[i * 8 + 5].format(dt));
System.out.println("LONG格式的时间格式:"
+ df[i * 8 + 6].format(dt));
System.out.println("FULL格式的时间格式:"
+ df[i * 8 + 7].format(dt));
}
}
}
运行程序,输出如下:
-------中国日期格式--------
SHORT格式的日期格式:19-1-27
MEDIUM格式的日期格式:2019-1-27
LONG格式的日期格式:2019年1月27日
FULL格式的日期格式:2019年1月27日 星期日
SHORT格式的时间格式:下午12:37
MEDIUM格式的时间格式:12:37:38
LONG格式的时间格式:下午12时37分38秒
FULL格式的时间格式:下午12时37分38秒 CST
-------美国日期格式--------
SHORT格式的日期格式:1/27/19
MEDIUM格式的日期格式:Jan 27, 2019
LONG格式的日期格式:January 27, 2019
FULL格式的日期格式:Sunday, January 27, 2019
SHORT格式的时间格式:12:37 PM
MEDIUM格式的时间格式:12:37:38 PM
LONG格式的时间格式:12:37:38 PM CST
FULL格式的时间格式:12:37:38 PM CST
上面程序共创建了16个DateFormat对象,分别为中国、美国两个Locale各创建8个DateFormat对象,分别是SHORT、MEDIUM、LONG、FULL 4种样式的日期格式器、时间格式器。
获得了DateFormat之后,还可以调用它的setLenient(boolean lenient)方法来设置该格式器是否采用严格语法。举例来说,如果采用不严格的日期语法(该方法的参数为true),对于字符串"2004-2-31"将会转换成2004年3月2日;如果采用严格的日期语法,解析该字符串时将抛出异常。
DateFormat的parse()方法可以把一个字符串解析成Date对象,但它要求被解析的字符串必须符合日期字符串的要求,否则可能抛出ParseException异常。例如,如下代码片段:
String str1="2007-12-12";
String str2="2007年12月10日";
//下面输出 Wed Dec 12 00:00:00 CST 2007
System.out.println(DateFormat.getDateInstance().parse(str1));
//下面输出 Mon Dec 10 00:00:00 CST 2007
System.out.println(DateFormat.getDateInstance(LONG).parse(str2));
//下面抛出 ParseException异常
System.out.println(DateFormat.getDateInstance().parse(str2));
上面代码中最后一行代码解析日期字符串时引发ParseException异常,因为“2007年12月10日”是一个LONG样式的日期字符串,必须用LONG样式的DateFormat实例解析,否则将抛出异常。
使用SimpleDateFormat格式化日期
前面介绍的DateFromat的parse()方法可以把字符串解析成Date对象,但实际上DateFormat的format()方法不够灵活——它要求被解析的字符串必须满足特定的格式!为了更好的格式化日期、解析日期字符串,Java提供了SimpleDateFormat类。
SimpleDateFormat是DateFormat的子类,正如它的名字所暗示的,它是“简单”的日期格式武器。实际上SimpleDateFormat比DateFormat更简单,功能更强大。
SimpleDateFormat可以非常灵活地格式化Date,也可以用于解析各种格式的日期字符串。创建SimpleDateFormat对象时需要传入一个pattern字符串,这个pattern不是正则表达式,而是一个日期模板字符串:
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class SimpleDateFormatTest {
public static void main(String[] args) throws ParseException {
Date d=new Date();
//创建一个SimpleDateFormat对象
SimpleDateFormat sdf1=new SimpleDateFormat("Gyyyy年中第D天");
// 将d格式化成日期,输出:公元2007年中第354天
String dateStr=sdf1.format(d);
System.out.println(dateStr);
//一个非常特殊的日期字符串
String str="07###三月##21";
SimpleDateFormat sdf2=new SimpleDateFormat("y###MMM##d");
//将日期字符串解析成日期,输出:Wed Mar 21 00:00:00 CST 2007
System.out.println(sdf2.parse(str));
}
}
从上面程序中可以看出,使用SimpleDateFormat可以将日期格式化成形如“公元2007年中第354天”这样的字符串,也可以把形如“07###三月##21”这样的字符串解析成日期,功能非常强大。SimpleDateFormat把日期格式化成怎样的字符串,以及能把怎样的字符串解析成Date,完全取决于创建该对象时指定的pattern参数,pattern是一个使用日期字段占位符的日期模板。
java编程练习
Formatter类的格式化输出
相信大家对System.out.printlu()方法都很熟悉,这是一种可以控制输出格式的方法。而在JDK 1.4之后,如果需要格式化输出,还可以使用Formatter类。Formatter类比print()方法功能更强,不仅可以用于控制台的输出,也可以用于GUI窗口程序的输出。
1.
新建项目FormatterUsage,并在其中创建一个FormatterUsage.java文件。在该类的主方法中分别使用Formatter类实现将输出项输出到自带存储区、指定缓冲区和直接输出三种情况:
package FormatterUsage;
import java.util.Formatter;
public class FormatterUsage {
public static void main(String[] args){
Object[] ob=new Object[2]; //创建Object数组
//给数组赋值
ob[0] = Integer.valueOf(51);
ob[1] = Integer.valueOf(1293);
Formatter fmt=null; //以默认的存储区为目标,创建对象
System.out.println("第一种输出方法:");
//以默认的存储区为目标,创建对象
fmt = new Formatter();
Object[] ob1 = new Object[2];
ob1[0] = Double.valueOf(1112.12675456);
ob1[1] = Double.valueOf(0.1258989);
// 格式化输出数据,输出到自己的存储区
fmt.format("输出到自带存储区,每个输出项占8个字符位:%4.3f %5.2f\n", ob1);
System.out.print(fmt); // 再从对象的存储区中输出到屏幕
System.out.println("\n第二种输出方式:");
fmt = new Formatter(System.out); // 以标准输出设备为目标,创建对象
// 格式化输出数据,并输出到标准输出设备
fmt.format("直接输出,每个输出项占5个字符位:%5d%5d\n\n", ob);
System.out.println("第三种输出方式:");
StringBuffer buf = new StringBuffer();
fmt = new Formatter(buf); // 以指定的字符串为目标,创建对象
// 格式化输出数据,输出到buf中
fmt.format("输出到指定的缓冲区,每个输出项占8个字符位:%8d%8d\n\n", ob);
System.out.print(buf); // 再从buf中输出到屏幕
}
}
本实例主要是对Format类技术的应用。Format类的使用和其他类一样,先要创建一个对象。它提供了10多个构造方法,其定义如下:
Formatter():构造一个新对象
Formatter(Appendable a):用指定目标构造一个新对象
Formatter(File file):用指定的File构造一个新对象
Formatter(Locale l):用指定的Locale构造一个新对象
Formatter(OutputStream os): 用指定的输出流构造一个新对象
使用时间格式转换符输出时间和日期
使用Formatter类的format()方法可以完成时间和日期的格式转换。本例就是使用时间格式转换符输出时间和日期的例子。
1.
新建项目demoFmtTime,并在其中创建一个demoFmtTime.java文件。在该类的主方法中首先获取当前时间,然后通过时间格式转换符以各种格式输出日期和时间。核心代码如下所示:
package domeFmtTime;
import java.util.Date;
import java.util.Formatter;
public class domeFmtTime {
public static void main(String[] args){
//以标准输出设备为目标,创建对象
Formatter fmt = new Formatter(System.out);
//获取当前时间
Date dt = new Date();
// 以各种格式输出日期和时间
fmt.format("现在的日期和时间(以默认的完整格式):%tc\n", dt);
fmt.format("今天的日期(按中国习惯):%1$tY-%1$tm-%1$td\n",dt);
fmt.format("今天是:%tA\n",dt);
fmt.format("现在的时间(24小时制):%tT\n",dt);
fmt.format("现在的时间(12小时制):%tr\n",dt);
fmt.format("现在是:%tH点%1$tM分%1$tS秒",dt);
}
}
输出日期和时间,需要以“%t”为前缀,加上下表的任意一个字符,组成完整的格式转换符,输出项必须是Date及其子类对象:
Formatter提供了一种更快捷的方式。程序员可以采用一个被称为参数索引的东西。这个索引是一个整型常量,它必须紧跟在“%”后面,并以“d`表示这个输出项是第2项,以十进制整数形式输出。
下面表示相同:
fmt.format("%Y-%m-%d",dt,dt,dt);
fmt.format("%1$tY-%1$tm-%1$td",dt);