一、Joda-Time 介绍
任何企业应用程序都需要处理时间问题。应用程序需要知道当前的时间点和下一个时间点,有时它们还必须计算这两个
时间点之间的路径。使用 JDK 完成这项任务将非常痛苦和繁琐。
既然无法摆脱时间,为何不设法简化时间处理?现在来看看 Joda Time,一个面向 Java™ 平台的易于
使用的开源时间/日期库。正如您在本文中了解的那样,JodaTime轻松化解了处理日期和时间的痛苦和繁琐。
Joda-Time 令时间和日期值变得易于管理、操作和理解。事实上,易于使用是 Joda 的主要设计目标。其他目标包括可扩展性、完整的特性集以及对多种日历系统的支持。
并且 Joda 与 JDK 是百分之百可互操作的,因此您无需替换所有 Java 代码,只需要替换执行日期/时间计算的那部分代码。
Joda-Time提供了一组Java类包用于处理包括ISO8601标准在内的date和time。可以利用它把JDK Date和Calendar类完全替换掉,而且仍然能够提供很好的集成。
为什么要使用 Joda?
考虑创建一个用时间表示的某个随意的时刻 — 比如,2000 年 1 月 1 日 0 时 0 分。
我如何创建一个用时间表示这个瞬间的 JDK 对象?使用 java.util.Date?
事实上这是行不通的,因为自 JDK 1.1 之后的每个 Java 版本的 Javadoc 都声明应当使用 java.util.Calendar。
Date 中不赞成使用的构造函数的数量严重限制了您创建此类对象的途径。
那么 Calendar 又如何呢?我将使用下面的方式创建必需的实例:
Calendar calendar = Calendar.getInstance(); calendar.set(2000, Calendar.JANUARY, 1, 0, 0, 0);
使用 Joda,代码应该类似如下所示:
DateTime dateTime = new DateTime(2000, 1, 1, 0, 0, 0, 0);
这一行简单代码没有太大的区别。但是现在我将使问题稍微复杂化。
假设我希望在这个日期上加上 90 天并输出结果。使用 JDK,我需要使用清单 1 中的代码:
// 以 JDK 的方式向某一个瞬间加上 90 天并输出结果 Calendar calendar = Calendar.getInstance(); calendar.set(2000, Calendar.JANUARY, 1, 0, 0, 0); SimpleDateFormat sdf = new SimpleDateFormat("E MM/dd/yyyy HH:mm:ss.SSS"); calendar.add(Calendar.DAY_OF_MONTH, 90); System.out.println(sdf.format(calendar.getTime()));
// 以 Joda 的方式向某一个瞬间加上 90 天并输出结果 DateTime dateTime = new DateTime(2000, 1, 1, 0, 0, 0, 0); System.out.println(dateTime.plusDays(90).toString("E MM/dd/yyyy HH:mm:ss.SSS");
两者之间的差距拉大了(Joda 用了两行代码,JDK 则是 5 行代码)。
现在假设我希望输出这样一个日期:距离 2000.1.1日 45 天之后的某天在下一个月的当前周的最后一天的日期。
坦白地说,我甚至不想使用 Calendar 处理这个问题。
使用 JDK 实在太痛苦了,即使是简单的日期计算,比如上面这个计算。
正是多年前的这样一个时刻,我第一次领略到 JodaTime的强大。使用 Joda,用于计算的代码所示:
DateTime dateTime = new DateTime(2000, 1, 1, 0, 0, 0, 0); System.out.println(dateTime.plusDays(45).plusMonths(1).dayOfWeek() .withMaximumValue().toString("E MM/dd/yyyy HH:mm:ss.SSS"); 输出为: Sun 03/19/2000 00:00:00.000
如果您正在寻找一种易于使用的方式替代 JDK 日期处理,那么您真的应该考虑 Joda。
二、创建Joda-Time对象
现在,我将展示在采用该库时会经常遇到的一些 Joda 类,并展示如何创建这些类的实例。
ReadableInstant
Joda 通过 ReadableInstant 类实现了瞬间性这一概念。表示时间上的不可变瞬间的 Joda 类都属于这个类的子类。
(将这个类命名为ReadOnlyInstant 可能更好,我认为这才是设计者需要传达的意思)。
换句话说,ReadableInstant 表示时间上的某一个不可修改的瞬间。
其中的两个子类分别为 DateTime 和 DateMidnight:
DateTime:这是最常用的一个类。它以毫秒级的精度封装时间上的某个瞬间时刻。
DateTime 始终与 DateTimeZone 相关,如果您不指定它的话,它将被默认设置为运行代码的机器所在的时区。
可以使用多种方式构建 DateTime 对象。这个构造函数使用系统时间:
DateTime dateTime = new DateTime();
如果您创建了一个 DateTime 的实例,并且没有提供 Chronology 或 DateTimeZone,Joda将使用 ISOChronology(默认)和DateTimeZone(来自系统设置)。
Joda 可以使您精确地控制创建 DateTime 对象的方式,该对象表示时间上的某个特定的瞬间。
DateTime dateTime = new DateTime( 2000, //year 1, // month 1, // day 0, // hour (midnight is zero) 0, // minute 0, // second 0 // milliseconds );
下一个构造函数将指定从 epoch(1970年1月1日 子时 格林威治标准时间) 到某个时刻所经过的毫秒数。
它根据 JDK Date 对象的毫秒值创建一个DateTime 对象,其时间精度用毫秒表示,因为 epoch 与 Joda 是相同的:
java.util.Date jdkDate = new Date(); long timeInMillis = jdkDate.getTime(); DateTime dateTime = new DateTime(timeInMillis);
或者 Date 对象直接传递给构造函数:
dateTime = new DateTime(new Date());
Joda 支持使用许多其他对象作为构造函数的参数,用于创建 DateTime:
// Use a Calendar dateTime = new DateTime(calendar); // Use another Joda DateTime dateTime = new DateTime(anotherDateTime); // Use a String (must be formatted properly) String timeString = "2006-01-26T13:30:00-06:00"; dateTime = new DateTime(timeString); timeString = "2006-01-26"; dateTime = new DateTime(timeString);
注意,如果您准备使用 String(必须经过解析),您必须对其进行精确地格式化。
DateMidnight:这个类封装某个时区(通常为默认时区)在特定年/月/日的午夜时分的时刻。
它基本上类似于 DateTime,不同之处在于时间部分总是为与该对象关联的特定 DateTimeZone 时区的午夜时分。
ReadablePartial
应用程序所需处理的日期问题并不全部都与时间上的某个完整时刻有关,因此您可以处理一个局部时刻。
例如,有时您比较关心年/月/日,或者一天中的时间,甚至是一周中的某天。Joda 设计者使用ReadablePartial 接口捕捉这种表示局部时间的概念,
这是一个不可变的局部时间片段。用于处理这种时间片段的两个有用类分别为 LocalDate 和LocalTime:
LocalDate:该类封装了一个年/月/日的组合。当地理位置(即时区)变得不重要时,使用它存储日期将非常方便。
例如,某个特定对象的出生日期 可能为 1999 年 4 月 16 日,但是从技术角度来看,
在保存所有业务值的同时不会了解有关此日期的任何其他信息(比如这是一周中的星期几,或者这个人出生地所在的时区)。
在这种情况下,应当使用 LocalDate。
LocalTime:这个类封装一天中的某个时间,当地理位置不重要的情况下,可以使用这个类来只存储一天当中的某个时间。
例如,晚上 11:52 可能是一天当中的一个重要时刻(比如,一个 cron 任务将启动,它将备份文件系统的某个部分),
但是这个时间并没有特定于某一天,因此我不需要了解有关这一时刻的其他信息。
创建对象代码:
package com.jt.joda; import java.util.Date; import org.joda.time.DateTime; import org.joda.time.LocalDate; import org.joda.time.LocalTime; import org.junit.Test; public class Demo { @Test public void test1(){ //方法一:取系统点间 DateTime dt1 = new DateTime(); System.out.println(dt1); //方法二:通过java.util.Date对象生成 DateTime dt2 = new DateTime(new Date()); System.out.println(dt2); //方法三:指定年月日点分秒生成(参数依次是:年,月,日,时,分,秒,毫秒) DateTime dt3 = new DateTime(2012, 5, 20, 13, 14, 0, 0); System.out.println(dt3); //方法四:ISO8601形式生成 DateTime dt4 = new DateTime("2012-05-20"); System.out.println(dt4); DateTime dt5 = new DateTime("2012-05-20T13:14:00"); System.out.println(dt5); //只需要年月日的时候 LocalDate localDate = new LocalDate(2009, 9, 6);// September 6, 2009 System.out.println(localDate); //只需要时分秒毫秒的时候 LocalTime localTime = new LocalTime(13, 30, 26, 0);// 1:30:26PM System.out.println(localTime); } /* 2015-09-25T17:51:12.900+08:00 2015-09-25T17:51:12.977+08:00 2012-05-20T13:14:00.000+08:00 2012-05-20T00:00:00.000+08:00 2012-05-20T13:14:00.000+08:00 2009-09-06 13:30:26.000 */ }
三、与JDK日期对象转换
许多代码都使用了 JDK Date 和 Calendar 类。但是幸亏有 Joda,可以执行任何必要的日期算法,然后再转换回 JDK 类。
这将两者的优点集中到一起。您在本文中看到的所有 Joda 类都可以从 JDK Calendar 或 Date 创建,正如您在 创建 JodaTime对象 中看到的那样。
出于同样的原因,可以从您所见过的任何 Joda 类创建 JDK Calendar 或 Date。
DateTime dt = new DateTime(); //转换成java.util.Date对象 Date d1 = new Date(dt.getMillis()); Date d2 = dt.toDate(); //转换成java.util.Calendar对象 Calendar c1 = Calendar.getInstance(); c1.setTimeInMillis(dt.getMillis()); Calendar c2 = dt.toCalendar(Locale.getDefault());
对于 ReadablePartial 子类,您还需要经过额外一步,如所示:
Date date = localDate.toDateMidnight().toDate();
要创建 Date 对象,您必须首先将它转换为一个 DateMidnight 对象,然后只需要将 DateMidnight 对象作为 Date。
(当然,产生的 Date 对象将把它自己的时间部分设置为午夜时刻)。
JDK 互操作性被内置到 Joda API 中,因此您无需全部替换自己的接口,如果它们被绑定到 JDK 的话。比
如,您可以使用 Joda 完成复杂的部分,然后使用 JDK 处理接口。
四、日期计算
现在,您已经了解了如何创建一些非常有用的 Joda 类,我将向您展示如何使用它们执行日期计算。
假设在当前的系统日期下,我希望计算上一个月的最后一天。对于这个例子,我并不关心一天中的时间,因为我只需要获得年/月/日,如所示:
LocalDate now = SystemFactory.getClock().getLocalDate(); LocalDate lastDayOfPreviousMonth = now.minusMonths(1).dayOfMonth().withMaximumValue();
首先,我从当前月份减去一个月,得到 “上一个月”。
接着,我要求获得 dayOfMonth 的最大值,它使我得到这个月的最后一天。
注意,这些调用被连接到一起(注意 Joda ReadableInstant 子类是不可变的),这样您只需要捕捉调用链中最后一个方法的结果,从而获得整个计算的结果。
您可能对dayOfMonth() 调用感兴趣。这在 Joda 中被称为属性(property)。它相当于 Java对象的属性。
属性是根据所表示的常见结构命名的,并且它被用于访问这个结构,用于完成计算目的。
属性是实现 Joda 计算威力的关键。您目前所见到的所有 4 个 Joda 类都具有这样的属性。一些例子包括:
- yearOfCentury
- dayOfYear
- monthOfYear
- dayOfMonth
- dayOfWeek
假设您希望获得任何一年中的第 11 月的第一个星期二的日期,而这天必须是在这个月的第一个星期一之后。
LocalDate now = SystemFactory.getClock().getLocalDate(); LocalDate electionDate = now.monthOfYear() .setCopy(11) // November .dayOfMonth() // Access Day Of Month Property .withMinimumValue() // Get its minimum value .plusDays(6) // Add 6 days .dayOfWeek() // Access Day Of Week Property .setCopy("Monday") // Set to Monday .plusDays(1); // Gives us Tuesday
.setCopy("Monday") 是整个计算的关键。不管中间 LocalDate 值是多少,将其 dayOfWeek 属性设置为 Monday 总是能够取得当前周的周一,这样的话,在每月的开始再加上 6 天就能够让您得到第一个星期一。再加上一天就得到第一个星期二。Joda 使得执行此类计算变得非常容易。
下面是其他一些因为使用 Joda 而变得超级简单的计算:
DateTime dt = new DateTime(); //昨天 DateTime yesterday = dt.minusDays(1); //明天 DateTime tomorrow = dt.plusDays(1); //1个月前 DateTime before1month = dt.minusMonths(1); //3个月后 DateTime after3month = dt.plusMonths(3); //2年前 DateTime before2year = dt.minusYears(2); //5年后 DateTime after5year = dt.plusYears(5);