Yuchuan Weng.Miko Tech-Blog.

请使用java8的线程安全日期类

2019/03/14 Share
  • 在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
//java 8时间类 
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()); // 合并后的时间 now=2019-04-12T15:56:36.813

//格式化指定的时间

String BASIC_ISO_DATE = now.format(DateTimeFormatter.BASIC_ISO_DATE);

log.info("BASIC_ISO_DATE = {}", BASIC_ISO_DATE);

//20190412

String ISO_DATE_TIME = now.format(DateTimeFormatter.ISO_DATE_TIME);

log.info("ISO_DATE_TIME = {}", ISO_DATE_TIME); //2019-04-12T16:00:49.776

//自定义格式化日期
DateTimeFormatter myFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); String myFormatStr = now.format(myFormat);

log.info("myFormatStr = {}", myFormatStr); //2019-04-12 16:02:09

//解析时间

LocalDateTime parse = LocalDateTime.parse("2018-01-25 17:53:55", myFormat); //parse time, year=2018 month=1 day=25, hour=17,minute=53, seconds=55

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); // PT1H 相差1小时
System.out.println("d1:" + d1); // PT2H 相差2小时
System.out.println("d2:" + d2); // PT16H40M 相差16小时40分钟
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)); // 相差10天 
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);
//加了一周的=2019-04-19 16:24:03 减了一周的=2019-04-05 16:24:03
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);
//下周一
/** * 当前时间=2019-04-12 16:40:36,当前月第一天=2019-04-01 16:40:36, 下周四或本周四=2019-04-18 16:40:36,下周一=2019-04-15 16:40:36 */
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().withYear(10));      
// 年:0010-01-25T13:35:46.594
System.out.println(LocalDateTime.now().withMonth(10)); // 月:2019-10-25T13:35:46.594
System.out.println(LocalDateTime.now().withDayOfMonth(10)); // 月第几天:2019-01-10T13:35:46.594
System.out.println(LocalDateTime.now().withDayOfYear(10)); // 年第几天:2019-01-10T13:35:46.594
System.out.println(LocalDateTime.now().withHour(10)); // 时:2019-01-25T10:35:46.594
System.out.println(LocalDateTime.now().withMinute(10)); // 分:2019-01-25T13:10:46.594
System.out.println(LocalDateTime.now().withSecond(10)); // 秒:2019-01-25T13:35:10.594
System.out.println(LocalDateTime.now().withNano(10)); // 纳秒:2019-01-25T13:35:46.000000010
System.out.println(LocalDateTime.now().with(TemporalAdjusters.next(DayOfWeek.MONDAY))); // 下一个星期一(不包含当天) 2019-01-28T21:39:26.459
System.out.println(LocalDateTime.now().with(TemporalAdjusters.next(DayOfWeek.TUESDAY))); // 下一个星期二(不包含当天) 2019-01-29T21:39:26.464
System.out.println(LocalDateTime.now().with(TemporalAdjusters.next(DayOfWeek.WEDNESDAY))); // 下一个星期三(不包含当天) 2019-01-30T21:39:26.464
System.out.println(LocalDateTime.now().with(TemporalAdjusters.next(DayOfWeek.THURSDAY))); // 下一个星期四(不包含当天) 2019-01-31T21:39:26.465
System.out.println(LocalDateTime.now().with(TemporalAdjusters.next(DayOfWeek.FRIDAY))); // 下一个星期五(不包含当天) 2019-02-01T21:39:26.465
System.out.println(LocalDateTime.now().with(TemporalAdjusters.next(DayOfWeek.SATURDAY))); // 下一个星期六(不包含当天) 2019-01-26T21:39:26.465
System.out.println(LocalDateTime.now().with(TemporalAdjusters.next(DayOfWeek.SUNDAY))); // 下一个星期日(不包含当天) 2019-01-27T21:39:26.465
// 本月第一天 2019-01-01T21:39:26.466
System.out.println(LocalDateTime.now().with(LocalDateTime.now().with(TemporalAdjusters.firstDayOfMonth()))); System.out.println(LocalDateTime.now().with(TemporalAdjusters.firstDayOfNextMonth())); // 下个月第一天 2019-02-01T21:39:26.468
System.out.println(LocalDateTime.now().with(TemporalAdjusters.firstDayOfNextYear())); // 下一年第一天 2020-01-01T21:39:26.469
System.out.println(LocalDateTime.now().with(TemporalAdjusters.firstDayOfYear())); // 今年第一天 2019-01-01T21:39:26.469
System.out.println(LocalDateTime.now().with(TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY))); // 本月第一个星期一 System.out.println(LocalDateTime.now().with(TemporalAdjusters.lastDayOfYear())); // 今年最后一天 2019-12-31T21:39:26.471
System.out.println(LocalDateTime.now().with(TemporalAdjusters.lastDayOfMonth())); // 本月最后一天 2019-01-31T21:39:26.472
System.out.println(LocalDateTime.now().with(TemporalAdjusters.lastInMonth((DayOfWeek.MONDAY)))); // 本月最后一个星期一 2019-01-28T21:39:26.473
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))); // 返回当前月的第一个星期一 2019-01-07T22:08:48.387 System.out.println(LocalDateTime.now().with(TemporalAdjusters.dayOfWeekInMonth(2,DayOfWeek.FRIDAY))); // 返回当前月的第二个星期五 2019-01-11T22:08:48.387 // 自定义时间矫正器 返回当前时间+2天 2019-01-27T22:27:23.734
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); //通过ZoneId,你还可以将LocalDateTime转换为Instant:
LocalDateTime dateTime = LocalDateTime.of(2016, 10, 14, 15, 35);
Instant instantFromDateTime = dateTime.toInstant(shanghaiZone); //你也可以通过反向的方式得到LocalDateTime对象:
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/格林尼治时间的绝对偏差的方式表示时区,并将其应用到日期时间对象上,对其进行本地化。
  • 部分类实现了函数式接口 ,支持函数式编程。
  • 都采用不变模式保证线程安全。
CATALOG
  1. 1. 使用LocalDate 和LocalTime
    1. 1.1. 1.1 LocalDate
    2. 1.2. 1.2 LocalTime
    3. 1.3. 合并日期和时间LocalDateTime
    4. 1.4. 操作日期
      1. 1.4.1. Duration
      2. 1.4.2. Period
      3. 1.4.3. 加减操作:
      4. 1.4.4. 判断之前或之后、指定时区
    5. 1.5. TemporalAdjuster
    6. 1.6. 4、处理不同时区问题
  2. 2. 总结