国际化与格式化

全球化的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搜索资源文件的顺序是:

  1. baseName_zh_CN.class
  2. baseName_zh_CN.properties
  3. baseName_zh.class
  4. baseName_zh.properties
  5. baseName.class
  6. 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及其子类对象:

java做国际化 java 国际化_java做国际化


Formatter提供了一种更快捷的方式。程序员可以采用一个被称为参数索引的东西。这个索引是一个整型常量,它必须紧跟在“%”后面,并以“java做国际化 java 国际化_字符串_02d`表示这个输出项是第2项,以十进制整数形式输出。

下面表示相同:

fmt.format("%Y-%m-%d",dt,dt,dt);
fmt.format("%1$tY-%1$tm-%1$td",dt);