1.旧的时间和日期的API的缺陷
Java 的 java.util.Date 和 java.util.Calendar 类易用性差,不支持时区,而且都不是线程安全的。
1.1 Date如果不格式化,打印出的日期可读性差。
Thu Sep 12 13:47:34 CST 2019
1.2 SimpleDateFormat 是线程不安全的
-
format 是线程不安全
//SimpleDateFormat的format方法源码如下: private StringBuffer format(Date date, StringBuffer toAppendTo, FieldDelegate delegate) { // Convert input date to time field list calendar.setTime(date); boolean useDateFormatSymbols = useDateFormatSymbols(); for (int i = 0; i < compiledPattern.length; ) { int tag = compiledPattern[i] >>> 8; int count = compiledPattern[i++] & 0xff; if (count == 255) { count = compiledPattern[i++] << 16; count |= compiledPattern[i++]; } switch (tag) { case TAG_QUOTE_ASCII_CHAR: toAppendTo.append((char)count); break; case TAG_QUOTE_CHARS: toAppendTo.append(compiledPattern, i, count); i += count; break; default: subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols); break; } } return toAppendTo; } //其中 calendar 是共享变量,并且这个共享变量没有做线程安全控制。 //当多个线程同时使用相同的 SimpleDateFormat 对象【如用static修饰的 SimpleDateFormat 】调用format方法时, //多个线程会同时调用 calendar.setTime 方法,可能一个线程刚设置好 time 值另外的一个线程马上把设置的 time 值给修改了导致返回的格式化时间可能是错误的。
-
parse 也是线程不安全
parse 方法实际调用 alb.establish(calendar).getTime() 方法来解析,alb.establish(calendar) 方法里主要完成了:
(1) 重置日期对象cal的属性值
(2) 使用calb中中属性设置cal
(3) 返回设置好的cal对象但是这三步不是原子操作,导致解析出来的时间可以是错误的。
-
多线程并发如何保证线程安全
(1) 避免线程之间共享一个 SimpleDateFormat 对象,每个线程使用时都创建一次 SimpleDateFormat 对象 => 创建和销毁对象的开销大
(2) 对使用 format 和 parse 方法的地方进行加锁 => 线程阻塞性能差
(3) 使用 ThreadLocal 保证每个线程最多只创建一次 SimpleDateFormat 对象 => 较好的方法
1.3 Date对时间处理比较麻烦
比如想获取某年、某月、某星期,以及 n 天以后的时间,如果用Date来处理的话真是太难了。
2.新的日期和时间API
2.1 Java8时间
Java8时间相关类在java.time包路径下,表示时间的主要类如下:
类名 | 描述 |
---|---|
Instant | 时间戳(时刻) |
LocalDate | 与时区无关的日期 |
LocalTime | 与时区无关的时间 |
LocalDateTime | 与时区无关的日期和时间 |
ZonedDateTime | 与时区相关的日期和时间 |
ZoneId | 时区 |
ZoneOffset | 相对于格林尼治时间的时间偏差,比如:+08:00 |
时间输出格式类型:
输出类型 | 描述 |
---|---|
2019-06-10T03:48:20.847Z | 世界标准时间,T:日期和时间分隔,Z:世界标准时间 |
2019-06-10T11:51:48.872 | 不含时区信息的时间 |
2019-06-10T11:55:04.421+08:00[Asia/Shanghai] | 包含时区信息的时间,+08:00表示相对于0时区加8小时,[Asia/Shanghai]:时区 |
2.2 Java8时间使用
-
Instant
public static void main(String ... args) { // 获取当前时间戳 Instant instant = Instant.now(); System.out.println(instant); // 指定系统时间戳 Instant instant1 = Instant.ofEpochMilli(System.currentTimeMillis()); System.out.println(instant1); // 解析指定时间戳 Instant instant2 = Instant.parse("2019-06-10T03:42:39Z"); System.out.println(instant2); } --- 2019-06-10T03:48:20.847Z 2019-06-10T03:48:20.941Z 2019-06-10T03:42:39Z Instant instant = Instant.parse("2018-12-30T19:34:50.63Z"); // 此方法返回自1970-01-01T00:00:00Z的纪元以来的毫秒数 long value = instant.toEpochMilli(); // print result System.out.println("Milisecond value: " + value); --- Milisecond value: 1546198490630
-
LocalDateTime
public static void main(String ... args) { // 获取当前时间 LocalDateTime localDateTime = LocalDateTime.now(); System.out.println(localDateTime); // 指定时间 LocalDateTime localDateTime1 = LocalDateTime.of(2019, 06, 10, 10, 30,30); System.out.println(localDateTime1); // 解析时间 LocalDateTime localDateTime2 = LocalDateTime.parse("2019-06-10 11:55:04", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); System.out.println(localDateTime2); } --- 2019-06-10T11:59:58.765 2019-06-10T10:30:30 2019-06-10T11:55:04
-
ZonedDateTime
public static void main(String ... args) { // 获取当前时区当前时间 ZonedDateTime zonedDateTime = ZonedDateTime.now(); System.out.println(zonedDateTime); // 指定时间及时区 ZonedDateTime zonedDateTime1 = ZonedDateTime.of(2019, 06, 10, 10, 30,30, 00, ZoneId.of("UTC")); System.out.println(zonedDateTime1); // 解析指定时间 ZonedDateTime zonedDateTime2 = ZonedDateTime.parse("2019-06-10T12:03:19.367+08:00[Asia/Shanghai]", DateTimeFormatter.ISO_ZONED_DATE_TIME); System.out.println(zonedDateTime2); } --- 2019-06-10T12:08:44.405+08:00[Asia/Shanghai] 2019-06-10T10:30:30Z[UTC] 2019-06-10T12:03:19.367+08:00[Asia/Shanghai]
-
时间对象转换
public static void main(String ... args) { Instant instant = Instant.now(); // 时间戳转LocalDateTime LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, ZoneId.systemDefault()); System.out.println(localDateTime); // 时间戳转时区时间 ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(instant, ZoneId.of("Asia/Shanghai")); System.out.println(zonedDateTime); // LocalDateTime转时间戳,因为LocalDateTime不带时区信息,因此需要指定当前时区到UTC的offset // ZoneOffset.ofHours(8), ZoneOffset.of("+08:00"), ZoneOffset.UTC Instant instant1 = localDateTime.toInstant(ZoneOffset.ofHours(8)); System.out.println(instant1); // 时区时间转时间戳,ZonedDateTime自带时区信息 Instant instant2 = zonedDateTime.toInstant(); System.out.println(instant2); // LocalDateTime转时区时间(为时间加上时区信息) ZonedDateTime zonedDateTime1 = ZonedDateTime.of(localDateTime, ZoneId.of("Asia/Tokyo")); System.out.println(zonedDateTime1); // 时区时间转换为LocalDateTime,将时区时间的时区信息去除 LocalDateTime localDateTime1 = zonedDateTime.toLocalDateTime(); System.out.println(localDateTime1); } --- 2019-06-10T14:02:50.483 2019-06-10T14:02:50.483+08:00[Asia/Shanghai] 2019-06-10T06:02:50.483Z 2019-06-10T06:02:50.483Z 2019-06-10T14:02:50.483+09:00[Asia/Tokyo] 2019-06-10T14:02:50.483
-
LocalDateTime与Date转换
Instant和Date是新老时间转换的桥梁,二者都是时间戳的表现形式,但Date在打印时会转换成当前时区时间(可通过设置TimeZone调整),Instant打印时默认是0时区时间public static void main(String ... args) { Date date = Date.from(Instant.now()); System.out.println(date); TimeZone.setDefault(TimeZone.getTimeZone("UTC")); System.out.println(date); Instant instant = date.toInstant(); System.out.println(instant); } --- Mon Jun 10 14:09:27 CST 2019 Mon Jun 10 06:09:27 UTC 2019 2019-06-10T06:09:27.307Z
-
时区转换
public static void main(String ... args) { // 初始化北京时间 ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(Instant.now(), ZoneId.of("Asia/Shanghai")); System.out.println(zonedDateTime); // 北京时间转换为UTC时间 ZonedDateTime utcZonedDateTime = zonedDateTime.toInstant().atZone(ZoneId.of("UTC")); System.out.println(utcZonedDateTime); // 初始化东京时间 ZonedDateTime tokyoDateTime = ZonedDateTime.of(2019, 06, 10, 11, 20, 20, 00, ZoneId.of("Asia/Tokyo")); System.out.println(tokyoDateTime); // 东京时间转换为芝加哥时间 ZonedDateTime chicagoDateTime = tokyoDateTime.toInstant() .atZone(ZoneId.of("America/Chicago")); System.out.println(chicagoDateTime); } --- 2019-06-10T14:19:19.053+08:00[Asia/Shanghai] 2019-06-10T06:19:19.053Z[UTC] 2019-06-10T11:20:20+09:00[Asia/Tokyo] 2019-06-09T21:20:20-05:00[America/Chicago]
-
时间调整(TemporalAdjuster)
public static void main(String ... args) { LocalDateTime localDateTime = LocalDateTime.now(); // 本年本月最后一天 System.out.println(localDateTime.with(TemporalAdjusters.lastDayOfMonth())); // 本年本月第一天 System.out.println(localDateTime.with(TemporalAdjusters.firstDayOfMonth())); // 本年下一月第一天 System.out.println(localDateTime.with(TemporalAdjusters.firstDayOfNextMonth())); // 下一年第一天 System.out.println(localDateTime.with(TemporalAdjusters.firstDayOfNextYear())); // 本年最后一天 System.out.println(localDateTime.with(TemporalAdjusters.lastDayOfYear())); // 下一个周五 System.out.println(localDateTime.with(TemporalAdjusters.next(DayOfWeek.FRIDAY))); // 本月第一个周五 System.out.println(localDateTime.with(TemporalAdjusters.firstInMonth(DayOfWeek.FRIDAY))); // 本月最后一个周五 System.out.println(localDateTime.with(TemporalAdjusters.lastInMonth(DayOfWeek.FRIDAY))); // 下一个周五,如果当前是周五则返回当前时间 System.out.println(localDateTime.with(TemporalAdjusters.nextOrSame(DayOfWeek.FRIDAY))); // 前一个周五 System.out.println(localDateTime.with(TemporalAdjusters.previous(DayOfWeek.FRIDAY))); // 前一个周五,如果当前是周五则返回当前时间 System.out.println(localDateTime.with(TemporalAdjusters.previousOrSame(DayOfWeek.FRIDAY))); // 当前时间+100天,plusYeas/plusMonths/plusWeeks/plusHours/plusMinutes/plusSeconds形式相同, // 同于System.out.println(localDateTime.plus(100, ChronoUnit.DAYS)); System.out.println(localDateTime.plusDays(100)); // 当前时间-100天,minusYeas/minusMonths/minusWeeks/minusHours/minusMinutes/minusSeconds形式相同, // 同于System.out.println(localDateTime.minus(100, ChronoUnit.DAYS)); System.out.println(localDateTime.minusDays(100)); } --- 2019-07-31T20:19:49.250 2019-07-01T20:19:49.250 2019-08-01T20:19:49.250 2020-01-01T20:19:49.250 2019-12-31T20:19:49.250 2019-07-19T20:19:49.250 2019-07-05T20:19:49.250 2019-07-26T20:19:49.250 2019-07-19T20:19:49.250 2019-07-12T20:19:49.250 2019-07-12T20:19:49.250 2019-10-22T20:19:49.250 2019-04-05T20:19:49.250
-
计算日期时间差异
public static void main(String ... args) { // 通过Period计算年龄 LocalDate birthDay = LocalDate.of(1992, 02, 25); LocalDate localDate = LocalDate.now(); Period period = Period.between(birthDay, localDate); System.out.printf("%d 岁 %d 月 %d 天 %n", period.getYears(), period.getMonths(), period.getDays()); // 计算时间差值 Instant instant = Instant.now(); Instant instant1 = instant.plus(Duration.ofMinutes(2)); Duration duration = Duration.between(instant, instant1); System.out.println(duration.getSeconds()); System.out.println(duration.toMillis()); // 按某一单位维度计算差值 LocalDateTime localDateTime = LocalDateTime.of(2019, 05, 20, 10, 10, 10); LocalDateTime localDateTime1 = LocalDateTime.now(); Long dayDiff = ChronoUnit.DAYS.between(birthDay, localDate); System.out.println(dayDiff); Long miniteDiff = ChronoUnit.MINUTES.between(localDateTime, localDateTime1); System.out.println(miniteDiff); } --- 27 岁 3 月 16 天 120 120000 9967 30553
-
时间打印
public static void main(String ... args) { LocalDateTime localDateTime = LocalDateTime.of(2019, 06,10, 10, 10, 10); System.out.println(localDateTime.format(DateTimeFormatter.BASIC_ISO_DATE)); System.out.println(localDateTime.format(DateTimeFormatter.ofPattern("yyyyMMdd HH:mm:ss"))); LocalDateTime localDateTime1 = LocalDateTime.parse("20190610 00:00:00", DateTimeFormatter.ofPattern("yyyyMMdd HH:mm:ss")); System.out.println(localDateTime1); } --- 20190610 20190610 10:10:10 2019-06-10T00:00
-
判断是否闰年
public static void main(String ... args) { LocalDate localDate = LocalDate.now(); System.out.println(localDate.isLeapYear()); } --- false
-
判断前后
public static void main(String ... args) { LocalDateTime localDateTime = LocalDateTime.now(); LocalDateTime localDateTime1 = LocalDateTime.of(2019, 05, 20, 10, 10, 10); System.out.println(localDateTime.isAfter(localDateTime1)); System.out.println(localDateTime.isBefore(localDateTime1)); } --- true false
-
在Java 8中如何检查重复事件,比如说生日
在Java中还有一个与时间日期相关的实际任务就是检查重复事件,比如说每月的帐单日,结婚纪念日,每月还款日或者是每年交保险费的日子。如果你在一家电商公司工作的话,那么肯定会有这么一个模块,会去给用户发送生日祝福并且在每一个重要的假日给他们捎去问候,比如说圣诞节,感恩节,在印度则可能是万灯节(Deepawali)。如何在Java中判断是否是某个节日或者重复事件?使用MonthDay类。这个类由月日组合,不包含年信息,也就是说你可以用它来代表每年重复出现的一些日子。当然也有一些别的组合,比如说YearMonth类。它和新的时间日期库中的其它类一样也都是不可变且线程安全的,并且它还是一个值类(value class)。我们通过一个例子来看下如何使用MonthDay来检查某个重复的日期:LocalDate dateOfBirth = LocalDate.of(2010, 01, 14); MonthDay birthday = MonthDay.of(dateOfBirth.getMonth(), dateOfBirth.getDayOfMonth()); MonthDay currentMonthDay = MonthDay.from(today); if(currentMonthDay.equals(birthday)){ System.out.println("Many Many happy returns of the day !!"); }else{ System.out.println("Sorry, today is not your birthday"); } ------------------------ Many Many happy returns of the day !
虽然年不同,但今天就是生日的那天,所以在输出那里你会看到一条生日祝福。你可以调整下系统的时间再运行下这个程序看看它是否能提醒你下一个生日是什么时候,你还可以试着用你的下一个生日来编写一个JUnit单元测试看看代码能否正确运行。