开源库—Joda Time

为什么要使用Joda-Time?

Date还是Calendar

在Java编程中经常会遇到处理日期和时间的需求,使用哪个类来处理呢?

java.util.Date吗?在java1.0中,对日期和时间的支持只能依赖java.util.Date类,但这个类并不能很好的表示时间,只能以毫秒的精度表示时间。而且这个类的设计决策使得易用性并不那么好,比如:年份的起始日期选择是1990年,月份的起始从0开始。

在JDK1.1中,Date类中的很多方法被废弃了,取而代之的是java.util.Calendar类。从JDK1.1之后的每个 Java 版本的 Javadoc 都声明应当使用 java.util.Calendar。Calendar类也有类似的问题和设计缺陷,导致使用这些方法写出的代码非常容易出错。比如月份依旧是从0开始计算(拿掉了由1990年开始计算年份这一设计)。更糟的是,有的特性只在某一个类有提供,比如格式化和解析日期或时间的DateFormat方法就只在Date类有。

DateFormat不是线程安全的,两个线程使用同一个DateFormat实例解析日期,你可能会得到无法预期的结果。

正因为这些问题,无论Date还是Calendar,使用起来都非常费劲,所以才出现了Joda-Time这个优质的日期和时间开发库。Joda-Time成为了JDK1.8之前事实上的标准Java日期和时间库。

JDK1.8的java.time包和Joda-Time

在JDK1.8中引入的java.time包是一组新的处理日期时间的API,遵守JSR 310。值得一提的是,Joda-Time的作者Stephen Colebourne和Oracle一起共同参与了这些API的设计和实现。java.time包中的类大量吸收了Joda-Time的东西(但是java.time包中提供的API和Joda-Time并不完全相同),所以如果是JDK1.8及以后的项目,可以不需要再使用Joda-Time库了

Calendar和Joda-Time使用对比

考虑创建一个用时间表示的某个随意的时刻——比如,2000 年 1 月 1 日 0 时 0 分。
使用Calendar的方式:

1
2
Calendar calendar = Calendar.getInstance();
calendar.set(2000, Calendar.JANUARY, 1, 0, 0, 0);

使用 Joda,代码类似如下所示:

1
new DateTime(2000, 1, 1, 0, 0, 0);

这一行简单代码没有太大的区别。但是现在我将使问题稍微复杂化。
假设我希望在这个日期上加上 90 天并输出结果。使用 JDK,我需要使用如下的代码:

1
2
3
4
5
6
// 以 JDK 的方式向某一个瞬间加上 90 天并输出结果
Calendar calendar = Calendar.getInstance();
calendar.set(2000, Calendar.JANUARY, 1, 0, 0, 0);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
calendar.add(Calendar.DAY_OF_MONTH, 90);
System.out.println("jdk方式:" + sdf.format(calendar.getTime()));

使用Joda的代码:

1
2
3
// 以 Joda 的方式向某一个瞬间加上 90 天并输出结果
DateTime dateTime = new DateTime(2000, 1, 1, 0, 0, 0);
System.out.println("joda-time方式:" + dateTime.plusDays(90).toString("yyyy-MM-dd HH:mm:ss"));

两者之间的差距拉大了(Joda 用了两行代码,JDK 则是 5 行代码)。
现在假设我希望输出这样一个日期:距离 2000.1.1日 45 天之后的某天在下一个月的当前周的最后一天的日期。我甚至不想使用 Calendar 处理这个问题了,因为使用 JDK的Calendar 会太痛苦了,但是用Joda却非常轻松:

1
2
3
4
5
6
7
DateTime dateTime = new DateTime(2000, 1, 1, 0, 0, 0, 0);
System.out.println(dateTime
.plusDays(45)
.plusMonths(1)
.dayOfWeek()
.withMaximumValue()
.toString("yyyy-MM-dd HH:mm:ss"));

从上面的例子可以看出使用Joda-Time确实能够非常方便地解决开发中的对时间和日期的处理。

Joda-Time API的使用

首先要在pom文件中引入Joda-Time的依赖:

1
2
3
4
5
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.9.9</version>
</dependency>

核心类介绍

这里介绍常用的几个类:

  • DateTime - 不可变的类,用来替换JDK的Calendar类
  • LocalDate - 不可变的类,表示一个本地的日期,而不包含时间部分(没有时区信息)
  • LocalTime - 不可变的类,表示一个本地的时间,而不包含日期部分(没有时区信息)
  • LocalDateTime - 不可变的类,表示一个本地的日期-时间(没有时区信息)
  • Instant - 不可变的类,用来表示时间轴上一个瞬时的点

注意:不可变类是指类的实例无法被修改,(不可变类的一个优点就是它们是线程安全的),当通过一个API方法去操作这个类的实例,都会返回一个新的实例,而原实例保持不变。所以当调用API的方法时,都需要去获取方法的返回值,比如java.lang.String 的各种操作方法的工作方式就是如此。

DateTime的主要目的是替换JDK中的Calendar类,用来处理那些时区信息比较重要的场景。
LocalDate比较适合表示出生日期这样的类型,因为不关心这一天中的时间部分。
LocalTime适合表示一个商店的每天开门/关门时间,因为不用关心日期部分。
Instant比较适合用来表示一个事件发生的时间戳。不用去关心它使用的日历系统或者是所在的时区。

DateTime

构造方法

DateTime 是JodaTime的核心类,代表时间日期值,其构造方法多样,即可以使用各种对象构造,亦可以使用基本类型构造,核心在于能够确定在时间轴上的位置。这里介绍几个常用的构造方法:

  • DateTime():这个无参的构造方法会创建一个在当前系统所在时区的当前时间,精确到毫秒
  • DateTime(int year, int monthOfYear, int dayOfMonth, int hourOfDay, int minuteOfHour, int secondOfMinute):这个构造方法方便快速地构造一个指定的时间,这里精确到秒,类似地其它构造方法也可以传入毫秒。
  • DateTime(long instant):这个构造方法创建出来的实例,是通过一个long类型的时间戳,它表示这个时间戳距1970-01-01T00:00:00Z的毫秒数。使用默认的时区。
  • DateTime(Object instant):这个构造方法可以通过一个Object对象构造一个实例。这个Object对象可以是这些类型:ReadableInstant, String, Calendar和Date。其中String的格式需要是ISO8601格式,详见:ISODateTimeFormat.dateTimeParser()

示例:

1
2
3
4
5
6
7
8
DateTime dateTime1 = new DateTime();
System.out.println(dateTime1);
DateTime dateTime2 = new DateTime(2018,10,14,0,0,0);
System.out.println(dateTime2);
DateTime dateTime3 = new DateTime(1539514032003L);
System.out.println(dateTime3);
DateTime dateTime4 = new DateTime(new Date());
System.out.println(dateTime4);

输出:

1
2
3
4
2018-10-14T18:47:50.179+08:00
2018-10-14T00:00:00.000+08:00
2018-10-14T18:47:12.003+08:00
2018-10-14T18:47:50.249+08:00

获取当前时间的now方法

静态方法now()可获取当前时间,同时也可以传一个参数获取某个时区的时间:

1
2
3
4
5
6
DateTime now = DateTime.now();
System.out.println(now);

//获取西八区的当前时间
DateTime now1 = DateTime.now(DateTimeZone.forTimeZone(TimeZone.getTimeZone("GMT-8:00")));
System.out.println(now1);

输出:

1
2
2018-10-14T19:01:32.400+08:00
2018-10-14T03:01:32.474-08:00

字符串和DateTime的互相转换

使用parse(String str, DateTimeFormatter formatter)返回指定String值的DateTime。也即是我们经常使用的字符串(String)转时间(DateTime)

1
2
DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss");
DateTime dateTime = DateTime.parse("2016-10-10 11:12:55", formatter);

注意:所有的DateTimeFormatter实例都是线程安全的。所以,你能够以单例模式创建格式器实例,就像DateTimeFormatter所定义的那些常量,并能在多个线程间共享这些实例。

使用toString(String pattern)返回一个指定格式的String。

1
2
DateTime now = DateTime.now();
System.out.println(now.toString("yyyy-MM-dd HH:mm:ss"));

get方法

DateTime中的getXXX()方法可以获取日期和时间的各种属性。

  • getCenturyOfEra():返回世纪单位
  • getYearOfCentury():返回世纪年份
  • getYear():返回年份
  • getWeekyear():返回对应年中周数
  • getMonthOfYear():返回对应年的月数
  • getDayOfYear():返回对应年中天数
  • getDayOfMonth():返回对应月份中天数
  • getDayOfWeek():返回对应星期中天数
  • getHourOfDay():返回对应天中的小时
  • getMinuteOfDay():返回对应天中的分钟数
  • getMinuteOfHour():返回对应小时中的分钟数
  • getSecondOfDay():返回对应天中的秒数
  • getSecondOfMinute():返回对应分钟中的秒数
  • getMillis():返回毫秒数
  • getMillisOfDay():返回对应天中的毫秒
  • getMillisOfSecond():返回对应秒中的毫秒
  • getHourOfDay():返回对应天中的小时

with方法

with开头的方法(比如:withYear(int year)):用来设置DateTime实例到某个时间,因为DateTime是不可变对象,所以没有提供setter方法可供使用,with方法也没有改变原有的对象,而是返回了设置后的一个副本对象。下面这个例子,将2000-02-29的年份设置为1997。值得注意的是,因为1997年没有2月29日,所以自动转为了28日。

1
2
3
4
DateTime dateTime2000Year = new DateTime(2000,2,29,0,0,0);
System.out.println(dateTime2000Year);
DateTime dateTime1997Year = dateTime2000Year.withYear(1997);
System.out.println(dateTime1997Year);

输出:

1
2
2000-02-29T00:00:00.000+08:00
1997-02-28T00:00:00.000+08:00

相关方法总结:

  • withCenturyOfEra(int centuryOfEra):更新时间世纪单位并返回
  • withYearOfCentury(int yearOfCentury):用来设置DateTime实例所在世纪的多少年。
  • withYear(int year):设置年份
  • withWeekyear(int weekyear):用来设置DateTime实例所在年的第多少周。
  • withMonthOfYear(int monthOfYear):设置DateTime实例所在年的几月份。
  • withDayOfYear(int dayOfYear):设置DateTime实例所在年的第多少天。
  • withDayOfMonth(int dayOfMonth):用来设置DateTime实例所在月的多少号。
  • withDayOfWeek(int dayOfWeek):用来设置DateTime实例的所在周的周几。
  • withHourOfDay(int hour):用来设置DateTime实例的所在日的小时数。
  • withMinuteOfHour(int minute) :设置DateTime实例的小时的分钟数。
  • withSecondOfMinute(int second):设置DateTime实例分钟的秒数。
  • withMillisOfSecond(int millis):设置DateTime实例的秒数下的毫秒数
  • withMillisOfDay(int millis):设置DateTime实例的所在日的毫秒数。
  • withTimeAtStartOfDay():获取当天最早时间,即时分秒为:00:00:00
1
2
3
4
5
DateTime now = DateTime.now();
System.out.println(now.toString("yyyy-MM-dd HH:mm:ss"));
System.out.println(now.withDayOfMonth(16).toString("yyyy-MM-dd HH:mm:ss"));
System.out.println(now.withDayOfWeek(3).toString("yyyy-MM-dd HH:mm:ss"));
System.out.println(now.withDayOfYear(10).toString("yyyy-MM-dd HH:mm:ss"));

输出:

1
2
3
4
2018-10-14 23:56:00
2018-10-16 23:56:00
2018-10-10 23:56:00
2018-01-10 23:56:00

plus/minu方法

plus/minus开头的方法(比如:plusDay, minusMonths):用来返回在DateTime实例上增加或减少一段时间后的实例。下面的例子:在当前的时刻加1天,得到了明天这个时刻的时间;在当前的时刻减1个月,得到了上个月这个时刻的时间。

1
2
3
4
5
6
7
8
DateTime dateTime = new DateTime(2018, 5, 31, 9, 0, 0);
System.out.println(dateTime);
DateTime tomorrow = dateTime.plusDays(1);
System.out.println(tomorrow);

//5月31日加一个月返回的是6月30日
DateTime nextMonthDay = dateTime.plusMonths(1);
System.out.println(nextMonthDay);

输出:

1
2
3
2018-05-31T09:00:00.000+08:00
2018-06-01T09:00:00.000+08:00
2018-06-30T09:00:00.000+08:00

注意,在增减时间的时候,想象成自己在翻日历,所有的计算都将符合历法,由Joda-Time自动完成,不会出现非法的日期(比如:5月31日加一个月后,并不会出现6月31日,而是6月30日)。

相关方法总结(此处仅列举plus相关,与之相反的minus则省略了):

  • plus(long duration):增加指定毫秒数并返回
  • plusYears(int years):增加指定年份并返回
  • plusMonths(int months):增加指定月份并返回
  • plusWeeks(int weeks):增加指定星期并返回
  • plusDays(int days):增加指定天数并返回
  • plusHours(int hours):增加指定小时并返回
  • plusMinutes(int minutes):增加指定分钟并返回
  • plusSeconds(int seconds) :增加指定秒数并返回

Property

除了基本get方法之外,jodatime为每一个时间类创建一个内部静态类,叫Property,便于访问实例中的各个字段,使Joda Time功能更加丰富。

它相当于 Java对象的属性。属性是根据所表示的常见结构命名的,并且它被用于访问这个结构,用于完成计算目的。

jodatime 包含的property有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
centuryOfEra()
yearOfCentury()
yearOfEra()
year()
weekyear()
weekOfWeekyear()
monthOfYear()
dayOfYear()
dayOfMonth()
dayOfWeek()
hourOfDay()
minuteOfDay()
minuteOfHour()
secondOfDay()
secondOfMinute()
millisOfDay()
millisOfSecond()

可以通过不同Property中get开头的方法获取一些有用的信息:

1
2
3
4
5
6
DateTime now = DateTime.now();
System.out.println(now);
System.out.println(now.monthOfYear().getAsText());
System.out.println(now.monthOfYear().getAsText(Locale.KOREAN));
System.out.println(now.dayOfWeek().getAsShortText());
System.out.println(now.dayOfWeek().getAsShortText(Locale.UK));

输出:

1
2
3
4
十月
10월
星期日
Sun

判断DateTime对象的大小

  • compareTo(DateTime d):比较两时间大小 时间大于指定时间返回 1 时间小于指定时间返回-1 相等返回0
  • equals(DateTime d):比较两时间是否相等
  • isAfter(long instant):判断时间是否大于指定时间
  • isAfterNow():判断时间是否大于当前时间
  • isBefore(long instant):判断时间是否小于指定时间
  • isBeforeNow():判断时间是否小于当前时间
  • isEqual(long instant):判断时间是否等于指定时间
  • isEqualNow():判断时间是否等于当前时间

获取边界值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
DateTime now = DateTime.now();
System.out.println(now.minuteOfHour().withMaximumValue()); //分钟数变成59分:2018-10-15T23:59:09.134+08:00

System.out.println(now.secondOfMinute().withMinimumValue());//秒数变成00秒:2018-10-15T23:03:00.117+08:00

//获取一天的0点
System.out.println(now.withTimeAtStartOfDay());
System.out.println(now.millisOfDay().withMinimumValue());

//获取一天的23:59:59
System.out.println(now.millisOfDay().withMaximumValue());

//获取某天所在月的月底那天
System.out.println(now.dayOfMonth().withMaximumValue());

计算区间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
DateTime begin = new DateTime("2018-06-01");  
DateTime end = new DateTime("2018-08-01");

//计算区间毫秒数
Duration d = new Duration(begin, end);
long time = d.getMillis();

//计算区间天数
Period p = new Period(begin, end, PeriodType.days());
int days = p.getDays();

//计算特定日期是否在该区间内
Interval i = new Interval(begin, end);
boolean contained = i.contains(new DateTime("2012-03-01"));

与JDK Date日期类的互转

1
2
3
4
5
6
7
8
9
10
11
 
//与java.util.Date互转
DateTime dt = new DateTime(new Date()); //jdk的Date转换为DateTime
Date jdkDate = dt.toDate() //DateTime转换为JDK的Date


//转换成java.util.Calendar对象
Calendar c1 = Calendar.getInstance();
DateTime dateTime = new DateTime(c1); //Calendar转换成DateTime

Calendar calendar = new DateTime().toCalendar(Locale.getDefault()); //DateTime转换成Calendar

LocalDate和LocalTime

LocalDate只处理年月日,常用的构造器有:

  • LocalDate():无参构造器。
  • LocalDate(int year, int monthOfYear, int dayOfMonth):传入年月日的构造器。
  • LocalDate(long instant):传入一个时间戳的构造器。

LocalDate的方法跟DateTime方法类似,此处就不再详细列出,可以在使用的时候再去查询即可。同样LocalTime也有类似的使用方法。

1
2
3
4
5
LocalDate localDate = new LocalDate();
System.out.println(localDate); //2018-10-15

LocalTime localTime = new LocalTime();
System.out.println(localTime); //22:45:53.129

计算差值

Joda-Time提供了计算差值的静态方法:-

  • daysBetween(ReadableInstant start, ReadableInstant end):获取两日期相差的天数
  • hoursBetween(ReadableInstant start, ReadableInstant end):获取两日期相差的小时数
  • minutesBetween(ReadableInstant start, ReadableInstant end):获取两日期相差的分钟数
  • monthsBetween(ReadableInstant start, ReadableInstant end):获取两日期相差的月数
  • secondsBetween(ReadableInstant start, ReadableInstant end):获取两日期相差的秒数
  • weeksBetween(ReadableInstant start, ReadableInstant end):获取两日期相差的周数
  • yearsBetween(ReadableInstant start, ReadableInstant end):获取两日期相差的年数

示例:

1
2
3
4
5
6
7
DateTime now = new DateTime();
DateTime dateTime = now.minusMonths(1);
System.out.println(Hours.hoursBetween(dateTime, now).getHours()); //720

LocalDate localDate = new LocalDate();
LocalDate start = localDate.minusMonths(1);
System.out.println(Days.daysBetween(start, localDate).getDays()); //30

------ 本文完 ------