- 在Java 1.0中,对日期和时间的支持只能依赖java.util.Date类。同时这个类还有两个很大的缺点:年份的起始选择是1900年,月份的起始从0开始。
- 在Java 1.1中,Date类中的很多方法被废弃,取而代之的是java.util.Calendar类。然而Calendar类也有类似的问题和设计缺陷,导致使用这些方法写出的代码非常容易出错。
DateFormat方法也有它自己的问题。比如,它不是线程安全的。这意味着两个线程如果尝试使用同一个formatter解析日期,你可能会得到无法预期的结果。
使用LocalDate 和LocalTime
1.1 LocalDate
Java 8提供新的日期和时间API,LocalDate类实例是一个不可变对象,只提供简单的日期并且不含当天时间信息。此外也不附带任何与时区相关的信息。
通过静态工厂方法of创建一个LocalDate实例。LocalDate实例提供了多种方法来读取常用的值,比如年份、月份、星期几等,如下所示。
1 2 3 4 5 6 7
| LocalDate LocalDate localDate = LocalDate.of(2019, 4, 11); log.info("年={} 月={} 日={}", localDate.getYear(), localDate.getMonth(), localDate.getDayOfMonth()); log.info("4月1日是2019的第 {} 天", localDate.getDayOfYear());
LocalDate nowTime = LocalDate.now(); LocalDate after = nowTime.plusDays(1); log.info("加一天后 {} 减少一周后{}", after.toString(), after.minusWeeks(1).toString());
|
打印结果:

1.2 LocalTime
使用LocalTime类表示时间,可以使用of重载的两个工厂方法创建LocalTime的实例。
- 第一个重载函数接收小时和分钟
- 第二个重载函数同时还接收秒。
LocalTime类也提供了一些get方法访问这些变量的值,如下所示。
1 2 3
| LocalTime localTime = LocalTime.of(12, 50, 32); LocalTime nowTime = LocalTime.now(); log.info("localTime={}, nowTime={}", localTime.toString(), nowTime.toString());
|
打印:
localTime=12:50:32 , nowTime=15:52:09.860
同理 也可以解析格式:
1 2
| LocalDate date = LocalDate.parse("2019-04-01"); LocalTime time = LocalTime.parse("20:17:08");
|
可以向parse方法传递一个DateTimeFormatter。该类的实例定义了如何格式化一个日期或者时间对象。用来替换老版java.util.DateFormat。 如果传递的字符串参数无法被解析为合法的LocalDate或LocalTime对象,这两个parse方法都会抛出一个继承自RuntimeException的DateTimeParseException异常。
合并日期和时间LocalDateTime
java提供了线程安全的LocalDateTime:
包含了自定义格式化等,还可以解析。
//合并
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| LocalDateTime now = LocalDateTime.now();
log.info("合并后的时间 now={}", now.toString());
String BASIC_ISO_DATE = now.format(DateTimeFormatter.BASIC_ISO_DATE);
log.info("BASIC_ISO_DATE = {}", BASIC_ISO_DATE);
String ISO_DATE_TIME = now.format(DateTimeFormatter.ISO_DATE_TIME);
log.info("ISO_DATE_TIME = {}", ISO_DATE_TIME);
DateTimeFormatter myFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); String myFormatStr = now.format(myFormat);
log.info("myFormatStr = {}", myFormatStr);
LocalDateTime parse = LocalDateTime.parse("2018-01-25 17:53:55", myFormat);
log.info("parse time, year={} month={} day={}, hour={},minute={}, seconds={}", parse.getYear(), parse.getMonthValue(), parse.getDayOfMonth(), parse.getHour(), parse.getMinute(), parse.getSecond());
|
DateTimeFormatter 也是线程安全的。
操作日期
LocalDateTime我们还可以操作加减日期,包括加减月份、周数等。
Duration
所有类都实现了Temporal接口,该接口定义如何读取和操纵为时间建模的对象的值。如果需要创建两个Temporal对象之间的duration,就需要Duration类的静态工厂方法between。 可以创建两个LocalTimes对象、两个LocalDateTimes对象,或者两个Instant对象之间的duration:
1 2 3 4 5 6 7 8 9 10 11 12
| LocalTime time1 = LocalTime.of(21, 50, 10); LocalTime time2 = LocalTime.of(22, 50, 10); LocalDateTime dateTime1 = LocalDateTime.of(2019, 03, 27, 21, 20, 40); LocalDateTime dateTime2 = LocalDateTime.of(2019, 03, 27, 21, 20, 40); Instant instant1 = Instant.ofEpochSecond(1000 * 60 * 2); Instant instant2 = Instant.ofEpochSecond(1000 * 60 * 3); Duration d1 = Duration.between(time1, time2); Duration d2 = Duration.between(dateTime1, dateTime2); Duration d3 = Duration.between(instant1, instant2); System.out.println("d1:" + d1); System.out.println("d2:" + d2); System.out.println("d3:" + d3);
|
LocalDateTime是为了便于人阅读使用,Instant是为了便于机器处理,所以不能将二者混用。如果在这两类对象之间创建duration,会触发一个DateTimeException异常。 此外,由于Duration类主要用于以秒和纳秒衡量时间的长短,你不能仅向between方法传递一个LocalDate对象做参数。
Period
使用Period类以年、月或者日的方式对多个时间单位建模。使用该类的工厂方法between,可以使用得到两个LocalDate之间的时长。
1 2
| Period period = Period.between(LocalDate.of(2019, 03, 7), LocalDate.of(2019, 03, 17)); System.out.println("Period between:" + period);
|
Duration和Period类都提供了很多非常方便的工厂类,直接创建对应的实例。
1 2 3 4
| Duration threeMinutes = Duration.ofMinutes(3); Duration fourMinutes = Duration.of(4, ChronoUnit.MINUTES); Period tenDay = Period.ofDays(10); Period threeWeeks = Period.ofWeeks(3); Period twoYearsSixMonthsOneDay = Period.of(2, 6, 1);
|
Duration类和Period类共享了很多相似的方法,有兴趣的可以参考官网的文档。
截至目前,我们介绍的这些日期-时间对象都是不可修改的,这是为了更好地支持函数式编程,确保线程安全,保持领域模式一致性而做出的重大设计决定。 当然,新的日期和时间API也提供了一些便利的方法来创建这些对象的可变版本。比如,你可能希望在已有的LocalDate实例上增加3天。
加减操作:
1 2 3 4 5 6 7
| LocalDateTime now1 = LocalDateTime.now(); log.info("当前时间={}", now1.format(myFormat)); LocalDateTime dateTime = now1.plusWeeks(1);
LocalDateTime minusWeek = now1.minusWeeks(1);
log.info("加了一周的={} 减了一周的={}", dateTime.format(myFormat), minusWeek.format(myFormat));
|
判断之前或之后、指定时区
1 2 3 4 5 6 7 8 9 10
| LocalDateTime now1 = LocalDateTime.now(); log.info("当前时间={}", now1.format(myFormat)); LocalDateTime datePlusWeek = now1.plusWeeks(1); LocalDateTime minusWeek = now1.minusWeeks(1);
ZoneId shanghaiZone = ZoneId.of("Asia/Shanghai"); ZonedDateTime zonedDateTime = now1.atZone(shanghaiZone); boolean after1 = zonedDateTime.isAfter(datePlusWeek.atZone(shanghaiZone)); boolean before = zonedDateTime.isBefore(minusWeek.atZone(shanghaiZone)); log.info("now time is before? ={} is after?={}", after1, before);
|
返回false false
TemporalAdjuster
有时需要进行一些更加复杂的操作,比如,将日期调整到下个周日、下个工作日,或者是本月的最后一天。可以使用重载版本的with方法,向其传递一个提供了更多定制化选择的TemporalAdjuster对象,更加灵活地处理日期。
1 2 3 4 5 6 7 8 9 10 11
| LocalDateTime firstDayOfMonth = now1.with(TemporalAdjusters.firstDayOfMonth());
LocalDateTime nextOrSameFriday = now1.with(TemporalAdjusters.nextOrSame(DayOfWeek.THURSDAY));
LocalDateTime nextMonday = now1.with(TemporalAdjusters.next(DayOfWeek.MONDAY));
DateTimeFormatter myFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); String myFormatStr = now.format(myFormat);
log.info("当前时间={},当前月第一天={}, 下周四或本周四={} ,下周一={}", now1.format(myFormat), firstDayOfMonth.format(myFormat), nextOrSameFriday.format(myFormat), nextMonday.format(myFormat));
|
扩展使用如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
|
System.out.println(LocalDateTime.now().withMonth(10)); System.out.println(LocalDateTime.now().withDayOfMonth(10)); System.out.println(LocalDateTime.now().withDayOfYear(10)); System.out.println(LocalDateTime.now().withHour(10)); System.out.println(LocalDateTime.now().withMinute(10)); System.out.println(LocalDateTime.now().withSecond(10)); System.out.println(LocalDateTime.now().withNano(10)); System.out.println(LocalDateTime.now().with(TemporalAdjusters.next(DayOfWeek.MONDAY))); System.out.println(LocalDateTime.now().with(TemporalAdjusters.next(DayOfWeek.TUESDAY))); System.out.println(LocalDateTime.now().with(TemporalAdjusters.next(DayOfWeek.WEDNESDAY))); System.out.println(LocalDateTime.now().with(TemporalAdjusters.next(DayOfWeek.THURSDAY))); System.out.println(LocalDateTime.now().with(TemporalAdjusters.next(DayOfWeek.FRIDAY))); System.out.println(LocalDateTime.now().with(TemporalAdjusters.next(DayOfWeek.SATURDAY))); System.out.println(LocalDateTime.now().with(TemporalAdjusters.next(DayOfWeek.SUNDAY)));
System.out.println(LocalDateTime.now().with(LocalDateTime.now().with(TemporalAdjusters.firstDayOfMonth()))); System.out.println(LocalDateTime.now().with(TemporalAdjusters.firstDayOfNextMonth())); System.out.println(LocalDateTime.now().with(TemporalAdjusters.firstDayOfNextYear())); System.out.println(LocalDateTime.now().with(TemporalAdjusters.firstDayOfYear())); System.out.println(LocalDateTime.now().with(TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY))); System.out.println(LocalDateTime.now().with(TemporalAdjusters.lastDayOfMonth())); System.out.println(LocalDateTime.now().with(TemporalAdjusters.lastInMonth((DayOfWeek.MONDAY)))); System.out.println(LocalDateTime.now().with(TemporalAdjusters.nextOrSame(DayOfWeek.MONDAY))); System.out.println(LocalDateTime.now().with(TemporalAdjusters.previous(DayOfWeek.MONDAY))); System.out.println(LocalDateTime.now().with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY))); System.out.println(LocalDateTime.now().with(TemporalAdjusters.dayOfWeekInMonth(1,DayOfWeek.MONDAY))); TemporalAdjuster temporalAdjuster = TemporalAdjusters.ofDateAdjuster(date -> date.plusDays(2)); System.out.println(LocalDateTime.now().with(temporalAdjuster));
|
4、处理不同时区问题
新版日期和时间API新增加的重要功能是时区的处理。新的java.time.ZoneId类替代老版java.util.TimeZone。跟其他日期和时间类一样,ZoneId类也是无法修改的。是按照一定的规则将区域划分成的标准时间相同的区间。在ZoneRules这个类中包含了40个时区实例,可以通过调用ZoneId的getRules()得到指定时区的规则,每个特定的ZoneId对象都由一个地区ID标识。
ZoneId shanghaiZone = ZoneId.of("Asia/Shanghai");
Java 8的新方法toZoneId将一个老的时区对象转换为ZoneId。地区ID都为“{区域}/{城市}”的格式,地区集合的设定都由英特网编号分配机构(IANA)的时区数据库提供。
ZoneId zoneId = TimeZone.getDefault().toZoneId();
ZoneId对象可以与LocalDate、LocalDateTime或者是Instant对象整合构造为成ZonedDateTime实例,它代表了相对于指定时区的时间点。
1 2 3 4 5 6 7 8 9 10
| LocalDate date = LocalDate.of(2019, 03, 27); ZonedDateTime zdt1 = date.atStartOfDay(shanghaiZone); LocalDateTime dateTime = LocalDateTime.of(2015, 12, 21, 11, 11, 11); ZonedDateTime zdt2 = dateTime.atZone(shanghaiZone); Instant instant = Instant.now(); ZonedDateTime zdt3 = instant.atZone(shanghaiZone); LocalDateTime dateTime = LocalDateTime.of(2016, 10, 14, 15, 35); Instant instantFromDateTime = dateTime.toInstant(shanghaiZone); Instant instant = Instant.now(); LocalDateTime timeFromInstant = LocalDateTime.ofInstant(instant, shanghaiZone);
|
另一种比较通用的表达时区的方式是利用当前时区和UTC/格林尼治的固定偏差。使用ZoneId的一个子类ZoneOffset,表示的是当前时间和伦敦格林尼治子午线时间的差异:
ZoneOffset newYorkOffset = ZoneOffset.of("-05:00");
总结
- Java 8之前老版的java.util.Date类以及其他用于建模日期时间的类有很多不一致及设计上的缺陷,包括易变性以及糟糕的偏移值、默认值和命名。
- 新版的日期和时间API中,日期-时间对象是不可变的。
- 新的API提供了两种不同的时间表示方式,有效地区分了运行时人和机器的不同需求。
- 你可以用绝对或者相对的方式操纵日期和时间,操作的结果总是返回一个新的实例,老的日期时间对象不会发生变化。
- TemporalAdjuster让你能够用更精细的方式操纵日期,不再局限于一次只能改变它的一个值,并且你还可按照需求定义自己的日期转换器。
- 你现在可以按照特定的格式需求,定义自己的格式器,打印输出或者解析日期时间对象。这些格式器可以通过模板创建,也可以自己编程创建,并且它们都是线程安全的。
- 你可以用相对于某个地区/位置的方式,或者以与UTC/格林尼治时间的绝对偏差的方式表示时区,并将其应用到日期时间对象上,对其进行本地化。
- 部分类实现了函数式接口 ,支持函数式编程。
- 都采用不变模式保证线程安全。