疯狂Java讲义(七)----第三部分

1.变量处理和方法处理

        Java 9引入了一个新的VarHandle类并增强了原有的MethodHandle类通过这两个类,允许Java像动态语言一样引用变量、引用方法,并调用它们

  (1) Java 9增强的MethodHandle

        MethodHandle为Java增加了方法引用的功能,方法引用的概念有点类似于C的“函数指针”。这种方法引用是一种轻量级的引用方式,它不会检查方法的访问权限,也不管方法所属的类、实例方法或静态方法MethodHandle就是简单代表特定的方法,并可通过MethodHandle来调用方法
        为了使用MethodHandle,还涉及如下几个类。

  • MethodHandles: MethodHandle的工厂类,它提供了一系列静态方法用于获取 MethodHandle
  • MethodHandles.Lookup: Lookup静态内部类也是 MethodHandle、VarHandle 的工厂类,专门用于获取MethodHandle和 VarHandle
  • MethodType:代表一个方法类型。MethodType根据方法的形参、返回值类型来确定方法类型。下面程序示范了MethodHandle 的用法。

        从上面三行粗体字代码可以看出,程序使用MethodHandles.Lookup对象根据类、方法名、方法类型来获取 MethodHandle对象。由于此处的方法名只是一个字符串,而该字符串可以来自于变量、配置文件等,这意味着通过MethodHandle可以让Java动态调用某个方法

  (2) Java 9增加的VarHandle

        VarHandle主要用于动态操作数组的元素对象的成员变量。VarHandle与 MethodHandle非常相似,它也需要通过MethodHandles 来获取实例,接下来调用VarHandle的方法即可动态操作指定数组的元素或指定对象的成员变量。
        下面程序示范了VarHandle的用法。

        从上面前两行粗体字代码可以看出,程序调用MethodHandles类的静态方法可获取操作数组的VarHandle对象,接下来程序可通过VarHandle对象来操作数组的方法,包括比较并设置数组元素、获取并设置数组元素等,VarHandle具体支持哪些方法则可参考API文档。
        上面程序中后面三行粗体字代码则示范了使用VarHandle操作实例变量的情形,由于实例变量需要使用对象来访问,因此使用VarHandle操作实例变量时需要传入一个User对象。VarHandle既可设置实例变量的值,也可获取实例变量的值。当然VarHandle也提供了更多的方法来操作实例变量,具体可参考API文档。
        使用VarHandle 操作类变量与操作实例变量差别不大,区别只是类变量不需要对象,因此使用VarHandle操作类变量时无须传入对象作为参数
        VarHandle 与 MethodHandle一样,它也是一种动态调用机制,当程序通过 MethodHandles.Lookup来获取成员变量时,可根据字符串名称来获取成员变量,这个字符串名称同样可以是动态改变的,因此非常灵活

扫描二维码关注公众号,回复: 13295336 查看本文章

2. Java 国际化

        全球化的Internet需要全球化的软件。全球化软件,意味着同一种版本的产品能够容易地适用于不同地区的市场,软件的全球化意味着国际化和本地化。当一个应用需要在全球范围使用时,就必须考虑在不同的地域和语言环境下的使用情况,最简单的要求就是用户界面上的信息可以用本地化语言来显示
        国际化是指应用程序运行时,可根据客户端请求来自的国家/地区、语言的不同而显示不同的界面。例如,如果请求来自于中文操作系统的客户端,则应用程序中的各种提示信息错误和帮助等都使用中文文字;如果客户端使用英文操作系统,则应用程序能自动识别,并做出英文的响应。
        引入国际化的目的是为了提供自适应、更友好的用户界面,并不需要改变程序的逻辑功能。国际化的英文单词是Internationalization,因为这个单词太长了,有时也简称I18N,其中I是这个单词的第一个字母,18表示中间省略的字母个数,而N代表这个单词的最后一个字母。
        一个国际化支持很好的应用,在不同的区域使用时,会呈现出本地语言的提示。这个过程也被称为Localization,即本地化。类似于国际化可以称为I18N,本地化也可以称为L10N.
        Java 9国际化支持升级到了Unicode 8.0字符集因此提供了对不同国家、不同语言的支持,它已经具有了国际化和本地化的特征及API,因此Java程序的国际化相对比较简单。尽管Java开发工具为国际化和本地化的工作提供了一些基本的类,但还是有一些对于Java应用程序的本地化和国际化来说较困难的工作,例如:消息获取,编码转换,显示布局和数字、日期、货币的格式等。

当然,一个优秀的全球化软件产品,对国际化和本地化的要求远远不止于此,甚至还包括用户提交数据的国际化和本地化。

3. Java 国际化的思路

        Java程序的国际化思路是将程序中的标签、提示等信息放在资源文件中,程序需要支持哪些国家、语言环境,就对应提供相应的资源文件。资源文件是key-value对每个资源文件中的 key是不变的,但 value 则随不同的国家、语言而改变。图7.7显示了Java程序国际化的思路。


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所支持的语言和国家。

4. Java 支持的国家和语言

        事实上,Java不可能支持所有的国家和语言,如果需要获取Java 所支持的国家和语言,则可调用Locale类的getAvailableLocales()方法,该方法返回一个Locale数组,该数组里包含了Java所支持的国家和语言。
        下面的程序简单地示范了如何获取Java所支持的国家和语言。

 通过该程序就可获得Java所支持的国家/语言环境。

 5.完成程序国际化:

 Java 9支持使用UTF-8字符集来保存属性文件,这样在属性文件中就可以直接包含非西欧字符,因此属性文件也不再需要使用native2ascii 工具进行处理。唯一要注意的是,属性文件必须显式保存为UTF-8字符集

 看到这两份文件文件名的 baseName是相同的: mess。前面已经介绍了资源文件的三种命名方式,其中 baseName后面的国家、语言必须是Java所支持的国家、语言组合。将上面的Java程序修改成如下形式。

        上面程序中的打印语句不再是直接打印“hello”字符串,而是打印从资源包中读取的信息。如果在中文环境下运行该程序,将打印“你好!";如果在“控制面板”中将机器的语言环境设置成美国,然后再次运行该程序,将打印“welcome!”字符串。
        从上面程序可以看出,如果希望程序完成国际化,只需要将不同的国家/语言(Locale)的提示信息分别以不同的文件存放即可。例如,简体中文的语言资源文件就是Xxx_zh_CN.properties文件,而美国英语的语言资源文件就是Xxx_en_US.properties 文件。
        Java程序国际化的关键类是ResourceBundle,它有一个静态方法: getBundle(String baseName,Localelocale),该方法将根据Locale加载资源文件,而Locale封装了一个国家、语言,例如,简体中文环境可以用简体中文的Locale代表,美国英语环境可以用美国英语的Locale代表。
        从上面资源文件的命名中可以看出,不同国家、语言环境的资源文件的 baseName是相同的,即baseName为 mess 的资源文件有很多个,不同的国家、语言环境对应不同的资源文件。
        例如,通过如下代码来加载资源文件。

        上面代码将会加载baseName为 mess 的系列资源文件之一,到底加载其中的哪个资源文件,则取决于myLocale;对于简体中文的Locale,则加载mess_zh_CN.properties文件。
        一旦加载了该文件后,该资源文件的内容就是多个key-value对,程序就根据key 来获取指定的信息,例如获取key为hello 的消息,该消息是“你好!”——这就是Java程序国际化的过程。
如果对于美国英语的Locale,则加载mess_en_US.properties文件,该文件中 key为hello的消息是"Welcome!”。
        Java程序国际化的关键类是ResourceBundle和 LocaleResourceBundle根据不同的Locale加载语言资源文件,再根据指定的key取得已加载语言资源文件中的字符串

6.使用MessageFormat处理包含占位符的字符串

        上面程序中输出的消息是一个简单消息,如果需要输出的消息中必须包含动态的内容,例如,这些内容必须是从程序中取得的。比如如下字符串:

在上面的输出字符串中,yeeku是浏览者的名字,必须动态改变,后面的时间也必须动态改变。在这种情况下,可以使用带占位符的消息。例如,提供一个myMess_en_US.properties 文件,该文件的内容如下:

 提供一个myMess_zh_CN.properties文件,该文件的内容如下

        当程序直接使用ResourceBundle的getString()方法来取出 msg对应的字符串时,在简体中文环境下得到“你好,{0}!今天是{1}。”字符串,这显然不是需要的结果,程序还需要为{0}和{1}两个占位符赋值。此时需要使用MessageFormat类,该类包含一个有用的静态方法

  • format(String pattern , Object.. values):返回后面的多个参数值填充前面的 patterm字符串,其中pattern字符串不是正则表达式,而是一个带占位符的字符串。

借助于上面的MessageFormat类的帮助,将国际化程序修改成如下形式。

        从上面的程序中可以看出,对于带占位符的消息字符串,只需要使用MessageFormat类的format()方法为消息中的占位符指定参数即可。

7. 使用类文件代替资源文件

        除使用属性文件作为资源文件外,Java也允许使用类文件代替资源文件,即将所有的key-value对存入class文件,而不是属性文件
        使用类文件来代替资源文件必须满足如下条件。

  • 该类的类名必须是baseName_language_country,这与属性文件的命名相似。
  • 该类必须继承ListResourceBundle,并重写getContents()方法,该方法返回Object数组,该数组的每一项都是key-value 对。

下面的类文件可以代替上面的属性文件。

        上面文件是一个简体中文语言环境的资源文件,该文件可以代替myMess _zh_CN.properties文件;如果需要代替美国英语语言环境的资源文件,则还应该提供一个myMess_en_US类
        如果系统同时存在资源文件、类文件,系统将以类文件为主,而不会调用资源文件。对于简体中文的Locale,ResourceBundle搜索资源文件的顺序是:

  1. (1) baseName_zh_CN.class
  2. (2) baseName_zh_CN.properties
  3. (3) baseName_zh.class
  4. (4) baseName_zh.properties
  5. (5) baseName.class
  6. (6) baseName.properties

系统按上面的顺序搜索资源文件,如果前面的文件不存在,才会使用下一个文件。如果一直找不到对应的文件,系统将抛出异常。

8. Java 9新增的日志API

        Java 9强化了原有的日志API,这套日志API 只是定义了记录消息的最小API,开发者可将这些日志消息路由到各种主流的日志框架(如SLF4J、Log4J等),否则默认使用Java传统的java.util.logging日志API。
        这套日志API的用法非常简单,只要两步即可。

  1. 调用System类的getLogger(String name)方法获取System.Logger对象
  2. 调用System.Logger对象的 log()方法输出日志。该方法的第一个参数用于指定日志级别

为了与传统java.util.logging日志级别、主流日志框架的级别兼容,Java9定义了如表7.7所示的日志级别。

        该日志级别是一个非常有用的东西:在开发阶段调试程序时,可能需要大量输出调试信息;在发布软件时,又希望关掉这些调试信息。此时就可通过日志来实现,只要将系统日志级别调高,所有低于该级别的日志信息就都会被自动关闭,如果将日志级别设为OFF,那么所有日志信息都会被关闭
        例如,如下程序示范了Java 9新增的日志API。

        上面程序中第一行粗体字代码获取Java 9提供的日志API,由于此处并未使用第三方日志框架,因此系统默认使用java.util.logging日志作为实现,因此第二行代码使用java.util.logging.Logger 来设置日志级别。程序将系统日志级别设为FINE(等同于DEBUG),这意味着高于或等于DEBUG级别的日志信息都会被输出到a.xml文件。运行上面程序,将可以看到在该文件所在目录下生成了一个a.xml文件,该文件中包含三条日志记录,分别对应于上面三行代码调用log()方法输出的日志记录。
        如果将上面第二行粗体字代码的日志级别改为SEVERE(等同于ERROR),这意味着高于或等于ERROR级别的日志信息都会被输出到a.xml 文件。再次运行该程序,将会看到该程序生成的 a.xml 文件仅包含一条日志记录,这意味着DEBUG、INFO级别的日志信息都被自动关闭了
        除简单使用之外,Java 9的日志API也支持国际化——System类除使用简单的getLogger(String name)方法获取System.Logger对象之外,还可使用getLogger(String name, ResourceBundle bundle)方法来获取该对象,该方法需要传入一个国际化语言资源包,这样该Logger对象即可根据key来输出国际化的日志信息。

 接下来程序可使用ResourceBundle先加载该国际化语言资源包,然后就可通过Java 9的日志API来输出国际化的日志信息了。

        该程序与前一个程序的区别就是粗体字代码,这行粗体字代码获取System.Logger时加载了ResourceBundle资源包。接下来调用System.Logger的 log()方法输出日志信息时,第二个参数应该使用国际化消息key,这样即可输出国际化的日志信息。
        在简体中文环境下运行该程序,将会看到a.xml文件中的日志信息是中文信息;在美式英文环境下运行该程序,将会看到a.xml文件中的日志信息是英文信息。

9. 使用NumberFormat格式化数字

        MessageFormat是抽象类Format的子类,Format抽象类还有两个子类:NumberFormat和DateFormat,它们分别用以实现数值、日期的格式化。NumberFormat 、DateFormat可以将数值、日期转换成字符串,也可以将字符串转换成数值、日期。图7.9显示了NumberFormat和DateFormat的主要功能。


NumberFormat和 DateFormat都包含了format(和parse()方法,其中format()用于将数值、日期格式化成字符串,parse()用于将字符串解析成数值、日期
NumberFormat也是一个抽象基类,所以无法通过它的构造器来创建NumberFormat对象,它提供了如下几个类方法来得到 NumberFormat对象。

  • getCurrencyInstance():返回默认Locale的货币格式器。也可以在调用该方法时传入指定的 Locale,则获取指定Locale的货币格式器。
  • getIntegerInstance():返回默认Locale的整数格式器。也可以在调用该方法时传入指定的Locale,则获取指定Locale的整数格式器。
  • getNumberInstance():返回默认 Locale 的通用数值格式器。也可以在调用该方法时传入指定的Locale,则获取指定Locale的通用数值格式器。
  • getPercentInstance():返回默认Locale的百分数格式器。也可以在调用该方法时传入指定的Locale,则获取指定Locale的百分数格式器。

一旦取得了NumberFormat对象后,就可以调用它的 format()方法来格式化数值,包括整数和浮点数。如下例子程序示范了NumberFormat的三种数字格式化器的用法。

        运行上面程序,将看到如图7.10所示的结果。

        从图7.10中可以看出,德国的小数点比较特殊,它们采用逗号(,)作为小数点;中国、日本使用¥作为货币符号,而美国则采用$作为货币符号。细心的读者可能会发现,NumberFormat其实也有国际化的作用!没错,同样的数值在不同国家的写法是不同的,而NumberFormat 的作用就是把数值转换成不同国家的本地写法。
        至于使用NumberFormat类将字符串解析成数值的意义不大(因为可以使用Integer、Double等包装类完成这种解析),故此处不再赘述。

10. 使用DateFormat格式化日期、时间

        与NumberFormat 相似的是,DateFormat也是一个抽象类,它也提供了如下几个类方法用于获取DateFormat对象。

  • getDateInstance():返回一个日期格式器,它格式化后的字符串只有日期,没有时间。该方法可以传入多个参数,用于指定日期样式和Locale等参数;如果不指定这些参数,则使用默认参数。
  • getTimeInstance():返回一个时间格式器,它格式化后的字符串只有时间,没有日期。该方法可以传入多个参数,用于指定时间样式和Locale等参数;如果不指定这些参数,则使用默认参数。
  • getDateTimeInstance():返回一个日期、时间格式器,它格式化后的字符串既有日期,也有时间。该方法可以传入多个参数,用于指定日期样式、时间样式和Locale等参数;如果不指定这些参数,则使用默认参数。

上面三个方法可以指定日期样式、时间样式参数,它们是DateFormat的4个静态常量:FULL、LONG、MEDIUM和SHORT,通过这4个样式参数可以控制生成的格式化字符串。看如下例子程序。

        上面程序共创建了16个 DateFormat对象,分别为中国、美国两个Locale各创建8个DateFormat对象,分别是SHORT、MEDIUM、LONG、FULL 四种样式的日期格式器、时间格式器。运行上面程序,会看到如图7.11所示的效果。


        从图7.11中可以看出,正如NumberFormat 提供了国际化的能力一样,DateFormat也具有国际化的能力,同一个日期使用不同的Locale格式器格式化的效果完全不同,格式化后的字符串正好符合Locale对应的本地习惯。
        获得了DateFormat之后,还可以调用它的setLenient(boolean lenient)方法来设置该格式器是否采用严格语法。举例来说,如果采用不严格的日期语法(该方法的参数为true),对于字符串"2004-2-31"将会转换成2004年3月2日;如果采用严格的日期语法,解析该字符串时将抛出异常。
        DateFormat的parse()方法可以把一个字符串解析成Date对象,但它要求被解析的字符串必须符合日期字符串的要求,否则可能抛出 ParseException异常。例如,如下代码片段:

        上面代码中最后一行代码解析日期字符串时引发ParseException 异常,因为"2017/10/07"是一个SHORT样式的日期字符串,必须用SHORT样式的DateFormat实例解析,否则将抛出异常

11. 使用SimpleDateFormat格式化日期

        前面介绍的DateFormat的parse()方法可以把字符串解析成Date对象,但实际上DateFormat的parse()方法不够灵活——它要求被解析的字符串必须满足特定的格式!为了更好地格式化日期、解析日期字符串,Java提供了SimpleDateFormat类
        SimpleDateFormat是DateFormat 的子类,正如它的名字所暗示的,它是“简单”的日期格式器。很多读者对“简单”的日期格式器不屑一顾,实际上SimpleDateFormat 比 DateFormat更简单,功能更强大。

        SimpleDateFormat可以非常灵活地格式化 Date,也可以用于解析各种格式的日期字符串。创建SimpleDateFormat对象时需要传入一个pattern字符串,这个pattern不是正则表达式,而是一个日期模板字符串

        从上面程序中可以看出,使用SimpleDateFormat可以将日期格式化成形如“公元2014年中第101天”这样的字符串,也可以把形如“14###三月##21”这样的字符串解析成日期,功能非常强大。SimpleDateFormat 把日期格式化成怎样的字符串,以及能把怎样的字符串解析成Date完全取决于创建该对象时指定的pattern参数,pattern是一个使用日期字段占位符的日期模板
        如果读者想知道SimpleDateFormat支持哪些日期、时间占位符,可以查阅API文档中SimpleDateFormat类的说明,此处不再赘述。

12. Java8新增的日期、时间格式器

        Java8新增的日期、时间API里不仅包括了Instant、LocalDate、LocalDateTime、LocalTime等代表日期、时间的类,而且在java.time.format包下提供了一个DateTimeFormatter格式器类,该类相当于前面介绍的 DateFormat 和l SimpleDateFormat的合体,功能非常强大。
        与DateFormat、SimpleDateFormat类似,DatcTimeFormatter 不仅可以将日期、时间对象格式化成字符串,也可以将特定格式的字符串解析成日期、时间对象
        为了使用DateTimeFormatter进行格式化或解析,必须先获取 DateTimeFormatter对象,获取DateTimeFormatter对象有如下三种常见的方式。

  • 直接使用静态常量创建DateTimeFormatter 格式器。DateTimeFormatter类中包含了大量形如ISO_LOCAL_DATE、ISO_LOCAL_TIME、ISO_LOCAL_DATE_TIME等静态常量,这些静态常量本身就是DateTimeFormatter 实例。
  • 使用代表不同风格的枚举值来创建DateTimeFormatter格式器。在 FormatStyle枚举类中定义了FULL、LONG、MEDIUM、SHORT四个枚举值,它们代表日期、时间的不同风格。
  • 根据模式字符串来创建DateTimeFormatter格式器。类似于SimpleDateFormat,可以采用模式字符串来创建DateTimeFormatter,如果需要了解DateTimeFormatter支持哪些模式字符串,则需要参考该类的API文档。

13. 使用DateTimeFormatter完成格式化

        使用DateTimeFormatter 将日期、时间(LocalDate、LocalDateTime、LocalTime等实例)格式化为字符串,可通过如下两种方式。

  • 调用DateTimeFormatter 的 format(TemporalAccessor temporal)方法执行格式化,其中 LocalDate,LocalDateTime、LocalTime等类都是TemporalAccessor接口的实现类。
  • 调用LocalDate、LocalDateTime、LocalTime 等日期、时间对象的 format(DateTimeFormatterformatter)方法执行格式化。

        上面两种方式的功能相同,用法也基本相似,如下程序示范了使用DateTimeFormatter 来格式化日期、时间。

        上面程序使用三种方式创建了6个 DateTimeFormatter对象,然后程序中两行粗体字代码分别使用不同方式来格式化日期。运行上面程序,会看到如图7.12所示的效果。

        从图7.12可以看出,使用DateTimeFormatter进行格式化时不仅可按系统预置的格式对日期、时间进行格式化,也可使用模式字符串对日期、时间进行自定义格式化,由此可见,DateTimeFormatter 的功能完全覆盖了传统的 DateFormat、SimpleDateFormate的功能。

14. 使用DateTimeFormatter解析字符串

        为了使用DateTimeFormatter将指定格式的字符串解析成日期、时间对象(LocalDate、LocalDateTime.LocalTime等实例),可通过日期、时间对象提供的 parse(CharSequence text, DateTimeFormatter formatter)方法进行解析。
        如下程序示范了使用DateTimeFormatter解析日期、时间字符串。

 上面程序中定义了两个不同格式的日期、时间字符串,为了解析它们,程序分别使用对应的格式字符串创建了DateTimeFormatter对象,这样DateTimeFormatter即可按该格式字符串将日期、时间字符串解析成LocalDateTime对象。编译、运行该程序,即可看到两个日期、时间字符串都被成功地解析成LocalDateTime。

猜你喜欢

转载自blog.csdn.net/indeedes/article/details/121004526