Joda-Time用户指导

来源:互联网 发布:mac os x 10.11黑苹果 编辑:程序博客网 时间:2024/06/05 15:50

1 引言

       Joda-Time就像一座冰山,90%的代码对于用户代码都是不可见的。许多,可能是大多数的应用程序从来不需要知道它底层是如何实现的。这个文档是为普通用户提供Joda-Time API的入门指导,而不是为API开发者。

    文档大部分只提供了代码片段,以说明大多数的普通使用场景。我们重点是介绍DateTimeIntervalDurationPeriod类。

    我们还会看看格式化和解析以及更高级的主题。

2.1 时刻

    在Joda-Time最频繁使用的概念就是时刻。时刻定义为连续时间中的一个片刻,用距离1970-01-01T00:00Z的毫秒数来表示。这里毫秒的定义与JDK中的Date或者Calendar中是一致的。两个API间的互操作性是相当简单的。

    在Joda-Time中,时刻用ReadableInstant接口表示。 这个接口的主要实现,普通API用户需要十分熟悉的类就是DateTimeDateTime是不可变的,一旦创建,值就不能改变。这样,这个类可以被安全地传递,在多线程中使用时不需要同步。

    使用Chronology可以将毫秒时刻转换成任意的日期时间字段。DateTime 为大多数的日期和时间字段提供了get方法。

    我们在概述里面讨论年代都点太深了。

    DateTime还有一个随同不可变类MutableDateTime。这个类的对象可以被修改,不是线程安全的。

    ReadableInstant的其它实现包括Instant和被弃用的DateMidnight

2.1.1 字段

    DateTime的主要API已经很小了,仅限于每一个日历字段的get方法。所以,日历字段'day-of-year'可通过调用getDayOfYear()方法获取。完整的字段列表和它们的描述,请参阅字段参考

2.1.2 属性

    通过使用属性,发现它有很强大的功能。每一个日历字段都关联了这样一个属性。这样,通过getDayOfYear()方法直接返回的'day-of-year'的值也和dayOfYear()方法返回的属性有关联。和DateTime关联的属性是DateTime.Property

    了解属性中的方法对于大多数API来说都是机密的。本文档稍后会介绍属性的更多用法。

2.2 时间间隔

       Joda-Time中的时间间隔表示从一个时刻到另一个时刻之间的间隔。两个时刻均是连续日期时间上的特定时刻。

    时间间隔实现为半开放的,也就是说,包含起始时刻,但是不包含终止时刻。终止时刻永远大于或等于起始时刻。两个端点必须具有相同的年代和相同的时区。

    时间间隔总共有两个实现类,IntervalMutableInterval。这两个类都是ReadableInterval的特殊实现。

2.3 持续时间

    Joda-Time中的持续时间表示为时间的持续,单位为毫秒。持续时间通常从时间间隔获得。

    持续时间是非常简单的概念,实现也很简单。它们没有年代和时区,只由持续的毫秒组成。 

    持续时间可以加到时刻上,或者时间间隔的一端以便修改那些对象。在日期时间数学中,你可以这样说:

      instant  +  duration  =  instant

    目前,ReadableDuration接口只有一个实现:Duration

2.4 周期

    Joda-Time中的周期表示用字段定义的一段时间,例如3527小时。与持续时间不同的是它不是用毫秒数精确表示的。只有制定了它相对的时刻(包括年代和时区),周期才能解析为精确的毫秒数。

    例如,考虑周期为一个月。如果你把这个周期加到21日(ISO),那么你将得到31日。如果你将相同的周期加到31日,那么你将得到41日。但是这两种情况下,添加的持续时间(毫秒数)是完全不同的。

    作为第二个例子,我们考虑日光节约时间的边界加1天。如果你使用周期来完成这个加法,那么23个小时或25个小时会被按需相加。如果你创建的持续时间等于24小时,那么你会得到错误的结果。

    周期被实现为一组int字段。周期中的标准字段是年、月、周、天、时、分、秒、毫秒。PeriodType类允许对这些字段进行限制,例如排除周。这是很重要的,在将持续时间或时间间隔转换为周期时,计算需要知道它应该填写那个周期字段。周期中存在获取每一个字段值的方法。周期不会关联到年代或者时区。

      instant  +  period  =  instant

    ReadablePeriod接口有两个实现:PeriodMutablePeriod

2.5 年代

        Joda-Time设计是基于年表的。年表是一个计算引擎,它支持复杂的日历系统。它封装了字段对象,这些对象可以被按需分割绝对时间,将绝对时间变为可识别的日历字段像'day-of-week'。它有一个高效的可插拔的日历系统。

    年代计算实际是Chronology类和字段类DateTimeFieldDurationField分开的。与这三个类的子类一起扩大了库的代码量。大多数用户从来不必直接使用或引用这些子类。相反,它们只是简单地获取chronology单例,如下所示:

            Chronology coptic = CopticChronology.getInstance();

    在内部,所有的年代类和字段类等等都是单例。所以使用Joda-Time,在初始化时有些花销,但是之后,只有主要的API实例类会创建和进行垃圾回收。

    虽然Chronology是设计的重点,但它不是使用API的重点。

    对于大多数应用来说,Chronology是被忽略的,因为默认使用ISOChronology。这适用于大多数情况。如果你需要在15821015日之前的精确日期,或者当你喜欢的地域终止使用公历,你也需要改变它,如果你需要特定的日历,例如科普特日历,那么提前说明吧。

2.6 时区

年代类还支持时区功能。这是对底层的年代类使用了装饰器设计模式。DateTimeZone类提供了工厂方法来访问时区,如下所示:

DateTimeZone zone = DateTimeZone.forID("Europe/London");

除了指定名称的时区,Joda-Time还支持固定时区。最简单的一个就是UTC,它被定义为常量:

DateTimeZone zoneUTC = DateTimeZone.UTC;

其他的固定偏移时间都可以通过特殊的工厂方法获得:

DateTimeZone zoneUTC = DateTimeZone.forOffsetHours(hours);

时区的实现是基于公共的IANA时区数据库提供的数据。完整的时区的列表可以在这里找到。

Joda-Time提供了一个默认的时区,在没有指定时区时使用。这类似于java.util.TimeZone的默认时区。这个值可以通过静态方法访问和更新:

DateTimeZone defaultZone = DateTimeZone.getDefault();

DateTimeZone.setDefault(myZone);

2.7 接口用法

正如你所看到的,Joda-Time定义了许多新的接口,在整个javadoc中都是可见的。最重要的接口是ReadableInstant,目前它总共有4个实现。其它重要的接口是ReadableIntervalReadablePeriod。这些接口目前分别被用作值唯一类和可变类的泛型参数。

这里提到的重要一点是Joda接口用法是不同与JDK集合框架接口的。当使用集合接口时,例如ListMap,你通常只持有ListMap类型的变量,只有在创建对象时才使用具体类。

    List list = new ArrayList();

    Map map = new HashMap();

Joda-Time中,接口的存在只是允许类似的日期实现之间进行互操作,例如可变类和不可变类。这样,它们只提供具体类的方法的一个子集。对于大多情况下,我们将使用具体类,,而不是接口。这使得我们可以访问库的强大功能。

    DateTime dt = new DateTime();

然而,为了尽量的灵活,你可能选择使用Joda-Time接口区声明方法参数。接口的方法可以获取这个方法内使用的具体类。

    public void process(ReadableDateTime dateTime) {

        DateTime dt = dateTime.toDateTime();

    }

2.8 包结构

包结构被设计成分离公共API和私有API。公共包就是根package(org.joda.time)和格式包。私有包是basechronoconvertfieldtz包。大多数应用都不需要导入私有包中的类。

3 使用DateTime

3.1 构造函数

使用DateTime构造函数创建日期时间对象。默认的构造函数如下所示:

    DateTime dt = new DateTime();

创建的DateTime对象表示当前的日期和时间(用系统时钟确定的毫秒树表示),使用默认时区的ISO日历构造。

你可以用一个初始字符串创建datetime对象来表示特定的日期和时间:

    DateTime dt = new DateTime("2004-12-13T21:39:45.618-08:00");

这个初始字符串必须符合ISO8601标准格式。

DateTime也提供了其他的使用各种标准字段构造特定的日期和时间。这也允许使用任意的日历和时区。

3.2 JDK互操作

DateTime有一个参数为Object的构造函数。特别地是,这个构造函数可以传入一个JDKDate, JDKCalendar或者JDK GregorianCalendar (接受ISO8601格式字符串或用Long表示毫秒数)。这是与JDK互操作的一方面。另一种与JDK互操作的情况是DateTime提供了返回JDK对象的方法。

这样,Joda DateTimeJDK Date之间的内部转换可以按照如下执行:

    //  Joda  JDK

    DateTime dt = new DateTime();

    Date jdkDate = dt.toDate();

 

    //  JDK  Joda

    dt = new DateTime(jdkDate);

类似地,与JDK Calendar的互转:

    // from Joda to JDK

    DateTime dt = new DateTime();

    Calendar jdkCal = dt.toCalendar(Locale.CHINESE);

 

    // from JDK to Joda

    dt = new DateTime(jdkCal);

JDK GregorianCalendar的互转:

    // from Joda to JDK

    DateTime dt = new DateTime();

    GregorianCalendar jdkGCal = dt.toGregorianCalendar();

 

    // from JDK to Joda

    dt = new DateTime(jdkGCal);

4 查询DateTime

日历字段(DateTimeField)的计算与日历时刻(DateTime)表示的分离创造了功能强大的、灵活的API。两者之间的连接是通过属性(DateTime.Property)维护的,日期属性提供了访问字段的方法。

例如,对于特定的DateTime直接获取一周的某一天的方式就是调用

    int iDoW = dt.getDayOfWeek();

这里的iDoW可以是下面的几个值(在类DateTimeConstants定义)

    public static final int MONDAY = 1;

    public static final int TUESDAY = 2;

    public static final int WEDNESDAY = 3;

    public static final int THURSDAY = 4;

    public static final int FRIDAY = 5;

    public static final int SATURDAY = 6;

    public static final int SUNDAY = 7;

4.1 访问字段

直接的方法简单使用还好,但是通过属性/字段机制可以获得等级多的灵活性。Week属性的第几天可以这样获得:

    DateTime.Property pDoW = dt.dayOfWeek();

这样可获取关于字段的更丰富的信息,例如

    String strST = pDoW.getAsShortText(); // 返回 "Mon", "Tue", etc.

    String strT = pDoW.getAsText(); // 返回 "Monday", "Tuesday", etc.

这返回了周天的短字段串和长字符串(基于当前的本地环境)。这个方法的本地化版本也是可用的,这样

    String strTF = pDoW.getAsText(Locale.FRENCH); // returns "Lundi", etc.

可以被返回法语的周天名。

当前,字段的原始整数值仍然可以访问:

    iDoW = pDoW.get();

这个熟悉也提供了其他值的访问,例如最大与最小文本的大小,闰年,相关的持续时间等等。完整的介绍,请参阅documentation了解基类AbstractReadableInstantFieldProperty

实践中,不会创建中间变量pDoW。如果调用匿名中间对象上的方法代码更易于阅读。例如:

    strT = dt.dayOfWeek().getAsText();

    iDoW = dt.dayOfWeek().get();

 

注意:对于获取字段的数值的单一场景,我们土建使用DateTimeget方法,因为它更高效:

    iDoW = dt.getDayOfWeek();

4.2 日期字段

DateTime实现提哦给你了完整的标准日历字段:

    dt.getEra();

    dt.getYear();

    dt.getWeekyear();

    dt.getCenturyOfEra();

    dt.getYearOfEra();

    dt.getYearOfCentury();

    dt.getMonthOfYear();

    dt.getWeekOfWeekyear();

    dt.getDayOfYear();

    dt.getDayOfMonth();

    dt.getDayOfWeek();

每一个字段都有对应的属性方法,属性方法返回与特定字段绑定DateTime.Property对象,例如year()monthOfYear()。这些属性表示的字段与它们的名字表示的完全一致。这些精确的定义可以参考字段介绍

正如你所希望的,在上面的day-of-week示例中演示的所有方法都适用于任意属性。例如,从DataTime中获取标准的月、日、年字段,我们这样写:

    String month = dt.monthOfYear().getAsText();

    int maxDay = dt.dayOfMonth().getMaximumValue();

    boolean leapYear = dt.yearOfEra().isLeap();

4.3 时间字段

另一组属性访问字段表示当天的时间计算。计算DateTime表示的时刻的时、分、秒,我们可以这样写:

    int hour = dt.getHourOfDay();

    int min = dt.getMinuteOfHour();

    int sec = dt.getSecondOfMinute();

再次强调,每一个字段都有对应的属性方法用于更复杂的操作。完整的事件字段列表可以在字段介绍中找到。

5 巧妙的使用DateTime

    DateTime对象有值语义,一旦构建就不能被修改(它们是不可变的)。因此,大多简单的操作DateTime对象涉及到新DateTime的构建,作为原始DateTime的修改拷贝。

警告:使用不可变类经常犯的一个错误就是忘记将结果赋值给一个变量。记住,调用不可变对象上的addset方法对这个对象本身没有任何影响。只是结果会被更新。

5.1 修改字段

    一种修改字段的方式是使用属性上的方法。返回到我们之前的例子,如果我们希望修改dt对象的day-of-week字段为Monday,我们可以使用属性的setCopy方法:

    DateTime result = dt.dayOfWeek().setCopy(DateTimeConstants.MONDAY);

注意:如果DateTime对象已经设置为Monday了,那么会返回相同的对象。

    要增加一个日期,可以使用addToCopy方法:

    DateTime result = dt.dayOfWeek().addToCopy(3);

5.2 DateTime的方法

    另一种完成类似地计算的方式是使用DateTime对象自身的方法。因此,我们可以如下所示直接在dt上增加3天:

    DateTime result = dt.plusDays(3);

5.3 使用MutableDateTime

    上面概述的方法都适用于简单的计算场景,只涉及到一个或两个字段。在需要修改多个字段的场景中,更加高效的做法是创建一个DateTime的可变拷贝,修改拷贝对象,最后再创建一个新的DateTime

    MutableDateTime mdt = dt.toMutableDateTime();

    // perform various calculations on mdt

    ...

    DateTime result = mdt.toDateTime();

    MutableDateTime有许多方法,包括直接修改DateTime的标准的set方法。

6 修改时区

    DateTime支持三两个普通时区的运算。例如,你想要获取此时伦敦的本地时间,你可以这样做:

    // get current moment in default time zone

    DateTime dt = new DateTime();

    // translate to London local time

    DateTime dtLondon = dt.withZone(DateTimeZone.forID("Europe/London"));

    这里,DateTimeZone.forID("Europe/London")方法返回的是伦敦的时区值。返回值dtLondon有相同的绝对时间,但是具有不同的字段值。

    当前,也支持相反的操作,即获取与伦敦本地时间相同的默认时区的绝对时间。

    // get current moment in default time zone

    DateTime dt = new DateTime();

    // find the moment when London will have / had the same time

    dtLondonSameTime = dt.withZoneRetainFields(DateTimeZone.forID("Europe/London"));

    所有时区的ID字符串(例如"Europe/London")可以通过调用DateTimeZone.getAvailableIDs()方法获得。完整的可用时区列表可以在这里找到。

7 修改年代

    DateTime类也有一个方法修改日历。这允许你修改指定时刻的日历。因此,如果你想要获得当前时间的佛历DateTime,你可以这样做

    // get current moment in default time zone

    DateTime dt = new DateTime();

    dt.getYear();  // returns 2004

    // change to Buddhist chronology

    DateTime dtBuddhist = dt.withChronology(BuddhistChronology.getInstance());

    dtBuddhist.getYear();  // returns 2547

    这里,BuddhistChronology.getInstance方法是一个获取佛历年代的工厂方法。


8 输入与输出

    从外部自定义的格式的源读取日期时间信息对于有日期时间计算需求的应用是相当频繁的。写入到自定义格式也是很常见的需求。

    许多自定义格式时用日期格式字符串表示的,在这个字符串中指定了一系列日历字段(数字和字符串等等)和字段长度。例如,模式"yyyy"表示4个数字的年。其他的格式不没有这么简单的表示。例如,表示2个数字的模式"yy"没法唯一确定它所属的世纪。在输出时,这个不会引起任何问题,但是在解释输入时会有问题。

    另外,今天,有几种常用的日期和时间的序列化标准,尤其是ISO8601。这些都是被大多数的日期时间应用支持的。

       Joda-Time支持通过灵活的架构支持不同的需求。我们现在将介绍这个架构的不同元素。

8.1 格式化器

    所有的打印和解析都是使用DateTimeFormatter对象执行的。考虑有一个这样的对象fmt,解析过程如下所示:

    String strInputDateTime;

    // string is populated with a date time string in some fashion

    ...

    DateTime dt = fmt.parseDateTime(strInputDateTime);

    这样,从格式化器就返回了DateTime对象。类似地,执行输出如下所示:

    String strOutputDateTime = fmt.print(dt);

8.2 标准格式化器

    ISODateTimeFormat支持基于ISO8601的标准格式化。它提供了很多工厂方法。例如,如果使用日期时间的ISO标准格式yyyy-MM-dd'T'HH:mm:ss.SSSZZ,你初始化fmt为:

    DateTimeFormatter fmt = ISODateTimeFormat.dateTime();

    然后你使用这个对象fmt,读写日期时间对象。

8.3 自定义格式化器

    如果需要自定义的格式化器,你可以使用DateTimeFormat类的提供的工厂方法。这样,要获得一个表示422天的日器,即格式为yyyyMMdd,你可以这样做:

    DateTimeFormatter fmt = DateTimeFormat.forPattern("yyyyMMdd");

    模式字符串与JDK日期模式是兼容的。

    你可能需要打印或解析特定Locale下的日期。这可以通过调用格式化器的withLocale方法实现,这个方法返回另一个格式化器。

    DateTimeFormatter fmt = DateTimeFormat.forPattern("yyyyMMdd");

    DateTimeFormatter frenchFmt = fmt.withLocale(Locale.FRENCH);

    DateTimeFormatter germanFmt = fmt.withLocale(Locale.GERMAN);

格式化器是不可变的,所以withLocale方法不会修改原来的格式化器。

8.4 奇怪的格式化器

    最后,你有一个格式,很难用模式字符串表示,Joda-Time架构暴露了一个构建器类,这个类用于构建可编程自定义的格式化器。这样,如果你想要一个打印和解析形如"22-Jan-65"的日期,那么你可以做:

    DateTimeFormatter fmt = new DateTimeFormatterBuilder()

            .appendDayOfMonth(2)

            .appendLiteral('-')

            .appendMonthOfYearShortText()

            .appendLiteral('-')

            .appendTwoDigitYear(1956)  // pivot = 1956

            .toFormatter();

    每一个append方法都会向构建器中增加一个新的字段,并返回新的构建器。最后的toFormatter方法创建用于打印和解析的实际格式化器。

    关于这个格式化有意思的是两数字年。由于两数字年的解析式模棱两可的,appendTwoDigitYear方法需要一个额外的参数来定义这两个数字是哪一个100年的范围,通过制定一个范围中点。例如,范围是(1956 - 50) = 1906(1956 + 49) = 2005. 这样04将是2004,而07却是1907。这种转换对于普通的格式字符串是不可能的,突出了Joda-Time格式化架构的强大功能。

8.5 直接访问

    为了简化格式化架构的访问,日期时间类,如DateTime,提供了这些方法。

    DateTime dt = new DateTime();

    String a = dt.toString();

    String b = dt.toString("dd:MM:yy");

    String c = dt.toString("EEE", Locale.FRENCH);

    DateTimeFormatter fmt = ...;

    String d = dt.toString(fmt);

    每一种结果都说明了使用格式化器的不同方式。结果a是标准的ISO8601字符串。结果b将使用模式'dd:MM:yy'输出(注意模式内部户缓存)。结果c使用法语的模式'EEE'。结果d使用指定的格式输出,等同于fmt.print(dt)

9 高级特性

9.1 修改当前时间

      Joda-Time允许修改当前时间。获取当前时间的所有方法都可以通过DateTimeUtils间接访问。这将允许修改当前时间,对于测试是非常有用的。

    // always return the same time when querying current time

    DateTimeUtils.setCurrentMillisFixed(millis);

    // offset the real time

    DateTimeUtils.setCurrentMillisOffset(millis);

    注意用这种方式修改当前时间不会影响系统时钟。

9.2 转换器

       API中每一个重要的具体类的构造器都有一个参数Object。这个Object参数被传递给转换系统,转换系统负责将这个对象转换为可接受的Joda-Time。例如,转换器可以讲JDKDate对象转换为DateTime。如果有需要,你还可以添加自己的转换器。

9.3 安全性

       Joda-Time包含标准JDK安全方案以检测敏感修改,包括修改时区处理器、修改当前时间、修改转换器。参见JodaTimePermission了解详细信息。