1. 简介
Joda-Time是Java 8发布之前使用最广泛的日期和时间处理库,它的目的是提供一个直观的日期和时间处理API,并解决Java Date/Time API中存在的设计问题。
该库中实现的核心概念是在Java 8版本发布时引入到JDK核心中的,新的日期和时间API位于java.time包(JSR-310本文)中,你可以在中找到这些功能的概述。
Java 8发布后,作者认为该项目基本完成,并建议尽可能使用Java 8 API。
2. 为什么要使用Joda-Time?
Java 8之前的日期/时间API存在多个设计问题。
其中一个问题是Date和SimpleDateFormatter类不是线程安全的,为了解决这个问题,Joda-Time使用不可变类来处理日期和时间。
Date类并不表示实际的日期,而是指定一个时间点,精度为毫秒。Date中的年份从1900年开始,而大多数日期操作通常使用纪元时间,该时间从1970年1月1日开始。
此外,日期的日、月、年偏移量是违反直觉的。日期从0开始,而月份从1开始,要访问它们中的任何一个,我们必须使用Calendar类,Joda-Time提供了一个简洁流式的API来处理日期和时间。
Joda-Time还提供对八种日历系统的支持,而Java仅提供两种:公历-java.util.GregorianCalendar和日语-java.util.JapaneseImperialCalendar。
3. 设置
为了包含Joda-Time库的功能,我们需要从Maven Central添加以下依赖:
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.12.5</version>
</dependency>
4. 库概述
Joda-Time使用org.joda.time包中的类 来模拟日期和时间的概念。
其中最常用的类别是:
- LocalDate:表示没有时间的日期
- LocalTime:表示不带时区的时间
- LocalDateTime:表示不带时区的日期和时间
- Instant:表示距离Java纪元1970-01-01T00:00:00Z的精确时间点(以毫秒为单位)
- Duration:表示两个时间点之间的持续时间(以毫秒为单位)
- Period:与Duration类似,但允许访问日期和时间对象的各个组成部分,如年、月、日等。
- Interval:表示两个时刻之间的时间间隔
其他重要功能包括日期解析器和格式化程序,这些可以在org.joda.time.format包中找到。
日历系统和时区特定类可以在org.joda.time.chrono和org.joda.time.tz包中找到。
让我们看一些使用Joda-Time的主要功能来处理日期和时间的示例。
5. 表示日期和时间
5.1 当前日期和时间
可以使用LocalDate类中的now()方法获取 不带时间信息的当前日期:
LocalDate currentDate = LocalDate.now();
当我们只需要当前时间,而不需要日期信息时,我们可以使用LocalTime类:
LocalTime currentTime = LocalTime.now();
为了获取当前日期和时间的表示而不考虑时区,我们可以使用LocalDateTime:
LocalDateTime currentDateAndTime = LocalDateTime.now();
现在,使用currentDateAndTime,我们可以将其转换为其他类型的日期和时间建模对象。
我们可以使用toDateTime()方法获取一个DateTime对象(该对象会考虑时区),当不需要时间时,可以使用toLocalDate()方法将其转换为LocalDate;而当只需要时间时,可以使用toLocalTime()方法获取LocalTime对象:
DateTime dateTime = currentDateAndTime.toDateTime();
LocalDate localDate = currentDateAndTime.toLocalDate();
LocalTime localTime = currentDateAndTime.toLocalTime();
所有上述方法都有一个重载方法,它接收一个DateTimeZone对象来帮助我们表示指定时区的日期或时间:
LocalDate currentDate = LocalDate.now(DateTimeZone.forID("America/Chicago"));
此外,Joda-Time还与Java日期和时间API完美集成。它的构造函数接收一个java.util.Date对象,我们也可以使用toDate()方法返回一个java.util.Date对象:
LocalDateTime currentDateTimeFromJavaDate = new LocalDateTime(new Date());
Date currentJavaDate = currentDateTimeFromJavaDate.toDate();
5.2 自定义日期和时间
为了表示自定义日期和时间,Joda-Time提供了几个构造函数,我们可以指定以下对象:
- Instant
- Java Date对象
- 使用ISO格式的日期和时间的字符串表示
- 日期和时间的部分:年、月、日、时、分、秒、毫秒
Date oneMinuteAgoDate = new Date(System.currentTimeMillis() - (60 * 1000));
Instant oneMinutesAgoInstant = new Instant(oneMinuteAgoDate);
DateTime customDateTimeFromInstant = new DateTime(oneMinutesAgoInstant);
DateTime customDateTimeFromJavaDate = new DateTime(oneMinuteAgoDate);
DateTime customDateTimeFromString = new DateTime("2018-05-05T10:11:12.123");
DateTime customDateTimeFromParts = new DateTime(2018, 5, 5, 10, 11, 12, 123);
定义自定义日期和时间的另一种方法是解析ISO格式的日期和时间的给定字符串表示形式:
DateTime parsedDateTime = DateTime.parse("2018-05-05T10:11:12.123");
我们还可以通过定义自定义DateTimeFormatter来解析日期和时间的自定义表示:
DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("MM/dd/yyyy HH:mm:ss");
DateTime parsedDateTimeUsingFormatter = DateTime.parse("05/05/2018 10:11:12", dateTimeFormatter);
6. 使用日期和时间
6.1 使用Instant
Instant表示从1970-01-01T00:00:00Z到指定时刻的毫秒数,例如,可以使用默认构造函数或方法now()获取当前时刻:
Instant instant = new Instant();
Instant.now();
要为自定义时刻创建Instant,我们可以使用其中一个构造函数或使用方法ofEpochMilli()和ofEpochSecond():
Instant instantFromEpochMilli = Instant.ofEpochMilli(milliesFromEpochTime);
Instant instantFromEpocSeconds = Instant.ofEpochSecond(secondsFromEpochTime);
构造函数接收一个表示ISO格式的日期和时间的字符串、一个Java日期或一个表示从1970-01-01T00:00:00Z开始的毫秒数的长值:
Instant instantFromString = new Instant("2018-05-05T10:11:12");
Instant instantFromDate = new Instant(oneMinuteAgoDate);
Instant instantFromTimestamp = new Instant(System.currentTimeMillis() - (60 * 1000));
当日期和时间表示为字符串时,我们可以选择使用所需的格式来解析字符串:
Instant parsedInstant = Instant.parse("05/05/2018 10:11:12", dateTimeFormatter);
现在我们知道了Instant代表什么以及如何创建一个,让我们看看如何使用它。
要与Instant对象进行比较,我们可以使用compareTo(),因为它实现了Comparable接口,但我们也可以使用Instant也实现的ReadableInstant接口中提供的Joda-Time API方法:
assertTrue(instantNow.compareTo(oneMinuteAgoInstant) > 0);
assertTrue(instantNow.isAfter(oneMinuteAgoInstant));
assertTrue(oneMinuteAgoInstant.isBefore(instantNow));
assertTrue(oneMinuteAgoInstant.isBeforeNow());
assertFalse(oneMinuteAgoInstant.isEqual(instantNow));
另一个有用的特性是Instant可以转换为DateTime对象或事件Java Date:
DateTime dateTimeFromInstant = instant.toDateTime();
Date javaDateFromInstant = instant.toDate();
当我们需要访问日期和时间的部分内容(例如年份、小时等)时,我们可以使用get()方法并指定一个DateTimeField:
int year = instant.get(DateTimeFieldType.year());
int month = instant.get(DateTimeFieldType.monthOfYear());
int day = instant.get(DateTimeFieldType.dayOfMonth());
int hour = instant.get(DateTimeFieldType.hourOfDay());
现在我们已经介绍了Instant类,让我们看一些如何使用Duration、Period和Interval的例子。
6.2 使用Duration、Period和Interval
Duration表示两个时间点(在本例中是两个Instant)之间的时间间隔(以毫秒为单位),当我们需要在另一个Instant上加减特定时间量而不考虑时间顺序和时区时,就会用到它:
long currentTimestamp = System.currentTimeMillis();
long oneHourAgo = currentTimestamp - 24*60*1000;
Duration duration = new Duration(oneHourAgo, currentTimestamp);
Instant.now().plus(duration);
另外,我们可以确定持续时间代表多少天、多少小时、多少分钟、多少秒或多少毫秒:
long durationInDays = duration.getStandardDays();
long durationInHours = duration.getStandardHours();
long durationInMinutes = duration.getStandardMinutes();
long durationInSeconds = duration.getStandardSeconds();
long durationInMilli = duration.getMillis();
Period和 Duration的主要区别在于,Period是根据日期和时间组件(年、月、小时等)定义的,并不表示精确的毫秒数,使用Period进行日期和时间计算时,会考虑时区和夏令时。
例如,在2月1日加上1个月的Period,得到的日期表示形式将是3月1日,使用Period后,库中会考虑闰年。
如果我们使用Duration,结果将不正确,因为Duration代表不考虑时间顺序或时区的固定时间量:
Period period = new Period().withMonths(1);
LocalDateTime datePlusPeriod = localDateTime.plus(period);
顾名思义,Interval表示两个Instant对象所表示的两个固定时间点之间的日期和时间间隔:
Interval interval = new Interval(oneMinuteAgoInstant, instantNow);
当我们需要检查两个区间是否重叠或计算它们之间的间隙时,该类非常有用,overlap()方法将返回重叠的区间,不重叠时则返回null:
Instant startInterval1 = new Instant("2018-05-05T09:00:00.000");
Instant endInterval1 = new Instant("2018-05-05T11:00:00.000");
Interval interval1 = new Interval(startInterval1, endInterval1);
Instant startInterval2 = new Instant("2018-05-05T10:00:00.000");
Instant endInterval2 = new Instant("2018-05-05T11:00:00.000");
Interval interval2 = new Interval(startInterval2, endInterval2);
Interval overlappingInterval = interval1.overlap(interval2);
可以使用gap()方法计算间隔之间的差异,当我们想知道一个间隔的结束是否等于另一个间隔的开始时,我们可以使用abuts()方法:
assertTrue(interval1.abuts(new Interval(
new Instant("2018-05-05T11:00:00.000"),
new Instant("2018-05-05T13:00:00.000"))));
6.3 日期和时间操作
一些最常见的操作是加、减和转换日期和时间,该库为每个类LocalDate、LocalTime、LocalDateTime和DateTime提供了特定的方法。需要注意的是,这些类是不可变的,因此每次方法调用都会创建一个该类型的新对象。
让我们以当前时刻的LocalDateTime为例,尝试改变它的值:
LocalDateTime currentLocalDateTime = LocalDateTime.now();
要为currentLocalDateTime添加额外的一天,我们使用plusDays()方法:
LocalDateTime nextDayDateTime = currentLocalDateTime.plusDays(1);
我们还可以使用plus()方法将Period或Duration添加到我们的currentLocalDateTime:
Period oneMonth = new Period().withMonths(1);
LocalDateTime nextMonthDateTime = currentLocalDateTime.plus(oneMonth);
其他日期和时间组件的方法类似,例如plusYears()用于添加额外的年份,plusSeconds()用于添加更多秒数等等。
要从currentLocalDateTime中减去一天, 我们可以使用minusDays()方法:
LocalDateTime previousDayLocalDateTime = currentLocalDateTime.minusDays(1);
除了使用日期和时间进行计算之外,我们还可以设置日期或时间的各个部分。例如,可以使用withHourOfDay()方法将小时设置为10,其他以“with”前缀开头的方法也可用于设置该日期或时间的各个组成部分:
LocalDateTime currentDateAtHour10 = currentLocalDateTime
.withHourOfDay(0)
.withMinuteOfHour(0)
.withSecondOfMinute(0)
.withMillisOfSecond(0);
另一个重要方面是,我们可以将日期和时间类类型转换为另一种类型。为此,我们可以使用库提供的特定方法:
- toDateTime():将LocalDateTime转换为DateTime对象
- toLocalDate():将LocalDateTime转换为LocalDate对象
- toLocalTime():将LocalDateTime转换为LocalTime对象
- toDate():将LocalDateTime转换为Java Date对象
7. 使用时区
Joda-Time让我们能够轻松地处理不同的时区并在它们之间切换,我们有一个DateTimeZone抽象类,它用于表示与时区相关的所有方面。
Joda-Time使用的默认时区是从user.timezone Java系统属性中选择的,库API允许我们为每个类或计算单独指定应使用的时区。例如,我们可以创建一个LocalDateTime对象
当我们知道将在整个应用程序中使用特定时区时,我们可以设置默认时区:
DateTimeZone.setDefault(DateTimeZone.UTC);
从现在开始,所有日期和时间操作(如果没有另行指定)都将以UTC时区表示。
要查看所有可用的时区,我们可以使用方法getAvailableIDs():
DateTimeZone.getAvailableIDs()
当我们需要表示特定时区的日期或时间时,我们可以使用LocalTime、LocalDate、LocalDateTime、DateTime类中的任何一个,并在构造函数中指定DateTimeZone对象:
DateTime dateTimeInChicago = new DateTime(DateTimeZone.forID("America/Chicago"));
DateTime dateTimeInBucharest = new DateTime(DateTimeZone.forID("Europe/Bucharest"));
LocalDateTime localDateTimeInChicago = new LocalDateTime(DateTimeZone.forID("America/Chicago"));
另外,在这些类之间转换时,我们可以指定所需的时区,方法toDateTime()接受DateTimeZone对象,而toDate()接收java.util.TimeZone对象:
DateTime convertedDateTime = localDateTimeInChicago.toDateTime(DateTimeZone.forID("Europe/Bucharest"));
Date convertedDate = localDateTimeInChicago.toDate(TimeZone.getTimeZone("Europe/Bucharest"));
8. 总结
Joda-Time是一个非常棒的库,它最初的主要目标是修复JDK中关于日期和时间操作的问题,并成为了事实上的日期和时间处理库,最近,它的主要概念被引入到了Java 8中。
值得注意的是,作者认为它“是一个基本完成的项目”,并建议迁移现有代码以使用Java 8实现。
Post Directory
