JDK8 Date
Java SE 8 Date and Time
为什么需要?
Java开发人员的一个长期困扰是对普通开发人员的日期和时间用例的支持不足。
例如,现有的类(例如java.util.Date 和 SimpleDateFormatter)并不是线程安全的,这给用户带来了潜在的并发问题——这不是一般开发人员在编写日期处理代码时所期望处理的问题。
一些日期和时间类也显示出相当糟糕的API设计。
例如,java.util中的年份。日期从1900年开始,月份从1月开始,天数从0开始——这不是很直观。
这些问题以及其他一些问题导致了第三方日期和时间库(如Joda-Time)的流行。
为了解决这些问题并在JDK核心中提供更好的支持,为Java SE 8设计了一个新的日期和时间API,它没有这些问题。
该项目由JSR 310下的Joda-Time (Stephen Colebourne)和Oracle共同领导,并将出现在新的Java SE 8包 java.time 中。
核心思想
- Immutable-value classes.
Java中现有格式化程序的一个严重缺陷是它们不是线程安全的。
这使得开发人员不得不以线程安全的方式使用它们,并在日常的数据处理代码开发中考虑并发问题。新的API通过确保其所有核心类都是不可变的并表示定义良好的值来避免这个问题。
- Domain-driven design.
新的API非常精确地使用类对其域进行建模,类代表了日期和时间的不同用例。这与以前在这方面很差的Java库不同。
例如, java.util.Date
表示 timeline 上的一个瞬间——这是一个自UNIX时代以来的毫秒数的包装器——但是如果您调用toString(),结果表明它有一个时区,在开发人员中引起混乱。
这种对域驱动设计的强调提供了关于清晰性和可理解性的长期好处,但是在将以前的api移植到Java SE 8时,您可能需要考虑应用程序的日期域模型。
- Separation of chronologies
新的API允许人们使用不同的日历系统,以支持世界上某些地区(如日本或泰国)的用户需求,这些地区不一定遵循ISO-8601。
这样做并没有给大多数开发人员带来额外的负担,他们只需要使用标准的年表。
LocalDate and LocalTime
在使用新API时,您可能会遇到的第一个类是LocalDate和LocalTime。它们是本地的,因为它们从观察者的上下文中表示日期和时间,比如桌子上的日历或墙上的时钟。还有一个名为LocalDateTime的复合类,它是LocalDate和LocalTime的配对。
时区,消除了不同观察者上下文的歧义,被放在一边;当不需要上下文时,应该使用这些本地类。桌面JavaFX应用程序可能就是这种情况之一。这些类甚至可以用来表示具有一致时区的分布式系统上的时间。
代码示例
创建实例
LocalDateTime timePoint = LocalDateTime.now(); // The current date and time
LocalDate.of(2012, Month.DECEMBER, 12); // from values
LocalDate.ofEpochDay(150); // middle of 1970
LocalTime.of(17, 18); // the train I took home today
LocalTime.parse("10:15:30"); // From a String
- 转 LocalDate
LocalDate theDate = timePoint.toLocalDate();
Month month = timePoint.getMonth();
int day = timePoint.getDayOfMonth();
timePoint.getSecond();
- 更改对象值以执行计算
因为在新的API中所有的核心类都是不可变的,所以这些方法是用新的对象调用并返回的,而不是使用setter(参见清单3)。
// Set the value, returning a new object
LocalDateTime thePast = timePoint.withDayOfMonth(
10).withYear(2010);
/* You can use direct manipulation methods,
or pass a value and field pair */
LocalDateTime yetAnother = thePast.plusWeeks(
3).plus(3, ChronoUnit.WEEKS);
- 调整器
新的API还有一个调整器的概念——一段代码,可以用来包装通用的处理逻辑。
您可以编写WithAdjuster(用于设置一个或多个字段),或者编写PlusAdjuster(用于添加或减去某些字段)。值类还可以充当调节器,在这种情况下,它们会更新它们表示的字段的值。内置的调整器是由新的API定义的,但是如果您希望重用特定的业务逻辑,您可以编写自己的调整器。
import static java.time.temporal.TemporalAdjusters.*;
LocalDateTime timePoint = ...
foo = timePoint.with(lastDayOfMonth());
bar = timePoint.with(previousOrSame(ChronoUnit.WEDNESDAY));
// Using value classes as adjusters
timePoint.with(LocalTime.now());
Truncation
新的API通过提供表示日期、时间和带时间的日期的类型来支持不同的精确时间点,但是显然有比这更细粒度的精确概念。
truncatedTo方法支持这样的用例,它允许将值截断为字段。
LocalTime truncatedTime = time.truncatedTo(ChronoUnit.SECONDS);
Time Zones
我们以前看到的本地类抽象了时区引入的复杂性。
时区是一组规则,对应于标准时间相同的区域。大约有40个。时区是由协调世界时(UTC)的偏移量定义的。它们大致同步移动,但有一个特定的差异。
时区可以用两个标识符来表示:缩写为“PLT”,较长为“Asia/Karachi”。在设计应用程序时,应该考虑哪些场景适合使用时区,以及什么时候偏移量合适。
ZoneId是一个区域的标识符(参见清单6),每个ZoneId对应于一些规则,这些规则定义了该位置的时区。
在设计软件时,如果您考虑使用“PLT”或“Asia/Karachi”等字符串,那么您应该使用这个域类。一个示例用例将存储用户对其时区的首选项。
- Listing 6
// You can specify the zone id when creating a zoned date time
ZoneId id = ZoneId.of("Europe/Paris");
ZonedDateTime zoned = ZonedDateTime.of(dateTime, id);
assertEquals(id, ZoneId.from(zoned));
区域偏移
区域偏移是表示格林尼治/UTC和时区之间的差异的时间周期。这可以在特定的时间点为特定的ZoneId解决,如清单7所示。
- 清单7
ZoneOffset offset = ZoneOffset.of("+2:00");
Time Zone Classes
ZonedDateTime
ZonedDateTime是一个具有完全限定时区的日期和时间(参见清单8),它可以在任何时间点解决偏移问题。
经验法则是,如果您想在不依赖于特定服务器上下文的情况下表示日期和时间,那么应该使用ZonedDateTime。
- 清单8
ZonedDateTime.parse("2007-12-03T10:15:30+01:00[Europe/Paris]");
OffsetDateTime
OffsetDateTime是一个日期和时间,有一个已解决的偏移量。
这对于将数据序列化到数据库中非常有用,如果您的服务器位于不同的时区,那么还应该将其用作记录时间戳的序列化格式。
OffsetTime
OffsetTime是一个具有解析偏移量的时间,如清单9所示。
OffsetTime time = OffsetTime.now();
// changes offset, while keeping the same point on the timeline
OffsetTime sameTimeDifferentOffset = time.withOffsetSameInstant(
offset);
// changes the offset, and updates the point on the timeline
OffsetTime changeTimeWithNewOffset = time.withOffsetSameLocal(
offset);
// Can also create new object with altered fields as before
changeTimeWithNewOffset
.withHour(3)
.plusSeconds(2);
Periods
Periods 表示“3个月1天”,这是时间轴上的距离。这与我们到目前为止看到的其他类不同,它们是时间轴上的点。参见清单10所示。
- 清单10
// 3 years, 2 months, 1 day
Period period = Period.of(3, 2, 1);
// You can modify the values of dates using periods
LocalDate newDate = oldDate.plus(period);
ZonedDateTime newDateTime = oldDateTime.minus(period);
// Components of a Period are represented by ChronoUnit values
assertEquals(1, period.get(ChronoUnit.DAYS));
Durations
持续时间是用时间度量的时间轴上的距离,它实现了与周期类似的目的,但是精度不同,如清单11所示。
- 清单11
// A duration of 3 seconds and 5 nanoseconds
Duration duration = Duration.ofSeconds(3, 5);
Duration oneDay = Duration.between(today, yesterday);
可以对Duration实例执行普通的加减和“with”操作,也可以使用Duration修改日期或时间的值。
Chronologies
为了支持使用非iso日历系统的开发人员的需求,Java SE 8引入了年表的概念,它表示日历系统,并充当日历系统中时间点的工厂。
还有一些接口对应于核心时间点类,但是由参数化
Chronology
ChronoLocalDate
ChronoLocalDateTime
ChronoZonedDateTime
这些类纯粹是为那些需要考虑本地日历系统的高度国际化应用程序的开发人员提供的,如果没有这些要求,开发人员就不应该使用它们。
有些日历系统甚至没有一个月或一周的概念,需要通过非常通用的字段API进行计算。
常用代码片段
判断连个日期相差几天在指定范围内
/**
* 断言时间在指定的区间内.
* 1. 参数符合规范
* 2. 0 maxDiffDays) {
final String error = String.format("开始日期:%s 与结束日期:%s 超过范围:%d 天", startDateStr, endDateStr, maxDiffDays);
throw new RuntimeException(error);
}
}
Period.between 的坑
近期使用才发现,Period.between() 计算的实际上当月的差。
如果隔月,就返回0 ,真是大坑
想获取真正的日、月间隔:
long days = ChronoUnit.DAYS.between(localDateStart, localDateEnd);
long monthDiff = ChronoUnit.MONTHS.between(localDateStart, localDateEnd);
工具方法
添加指定月份
/**
* 添加指定的月份到日期
* @param yyyMMdd 日期
* @param months 月份
* @return 结果
*/
public static String addMonthToDate(String yyyMMdd, int months){
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd");
LocalDate localDate = LocalDate.parse(yyyMMdd, formatter);
LocalDate newDate = localDate.plusMonths(months);
return newDate.format(formatter);
}
参考资料
- oracle
https://www.oracle.com/technetwork/articles/java/jf14-date-time-2125367.html
- other