JAVA8实战 - 日期API 前言 这一节我们来讲讲JAVA8的日期类,源代码的作者其实就是Joda-Time,所以可以看到很多代码的API和Joda类比较像。日期类一直是一个比较难用的东西,但是JAVA8给日期类提供了一套新的API让日期类更加好用。
本文代码较多,建议亲自运行代码理解。 (微信公众号建议阅读原文)
思维导图: 地址:https://www.mubucm.com/doc/ck5ZCrgHkB
内容概述:
关于JDK8日期的三个核心类:LocalDate、LocalTime、LocalDateTime的相关介绍
机器时间和日期格式Instant
等关于细粒度的时间操作介绍
TemporalAdjusters 用于更加复杂的日期计算,比如计算下一个工作日的时候这个类提供了一些实现
DateTimeFormatter 格式化器,非常的灵活多变,属于SimpleDateFormat
的替代品。
日期API的一些个人工具封装举例,以及在使用JDK8的时候一些个人的踩坑
最后希望通过本文能帮你摆脱new Date()
什么是ISO-8601? 日期离不开ISO-8601,下面对ISO-8601简单描述一下,参考自百度百科:
ISO-8601: 国际标准化组织制定的日期和时间的表示方法,全称为《数据存储和交换形式·信息交换·日期和时间的表示方法》,简称为ISO-8601。
日的表示:小时、分和秒都用2位数表示,对UTC时间最后加一个大写字母Z,其他时区用实际时间加时差表示。如UTC时间下午2点30分5秒表示为14:30:05Z或143005Z,当时的北京时间表示为22:30:05+08:00或223005+0800,也可以简化成223005+08。
日期和时间的组合表示:合并表示时,要在时间前面加一大写字母T,如要表示北京时间2004年5月3日下午5点30分8秒,可以写成2004-05-03T17:30:08+08:00或20040503T173008+08。
LocalDate、LocalTime、LocalDateTime JDK8把时间拆分成了三个大部分,一个是时间,代表了年月日的信息,一个是日期,代表了时分秒的部分,最后是这两个对象总和具体的时间。
LocalDate LocalDate
:类表示一个具体的日期,但不包含具体时间,也不包含时区信息。可以通过LocalDate
的静态方法of()
创建一个实例,LocalDate
也包含一些方法用来获取年份,月份,天,星期几等,下面是LocalDate
的常见使用方式:
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 @Test public void localDateTest () throws Exception { LocalDate of = LocalDate.of(2021 , 8 , 9 ); LocalDate now = LocalDate.now(); LocalDate parse1 = LocalDate.parse("2021-05-11" ); LocalDate parse2 = LocalDate.parse("2021-05-11" , DateTimeFormatter.ofPattern("yyyy-MM-dd" )); LocalDate parse3 = LocalDate.parse("2021-05-11 11:53:53" , DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss" )); System.out.println("now() => " + now); int dayOfMonth = parse1.getDayOfMonth(); System.out.println("dayOfMonth => " + dayOfMonth); int dayOfYear = parse1.getDayOfYear(); System.out.println("getDayOfYear => " + dayOfYear); DayOfWeek dayOfWeek = parse1.getDayOfWeek(); System.out.println("getDayOfWeek => " + dayOfWeek); int monthValue = parse3.getMonthValue(); System.out.println("getMonthValue => " + monthValue); int year = parse3.getYear(); System.out.println("getYear => " + year); System.out.println("getChronology => " + parse3.getChronology()); System.out.println("getEra => " + parse3.getEra()); System.out.println("ChronoField.YEAR => " + parse1.get(ChronoField.YEAR)); }
TemporalField 是一个接口,定义了如何访问 TemporalField 的值,ChronnoField 实现了这个接口
LocalTime LocalTime
:和LocalDate
类似,区别在于包含具体时间,同时拥有更多操作具体时间时间的方法,下面是对应的方法以及测试:
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 @Test public void localTimeTest () throws Exception { LocalTime now = LocalTime.now(); System.out.println("LocalTime.now() => " + now); System.out.println("getHour => " + now.getHour()); System.out.println("getMinute => " + now.getMinute()); System.out.println("getNano => " + now.getNano()); System.out.println("getSecond => " + now.getSecond()); LocalTime systemDefault = LocalTime.now(Clock.systemDefaultZone()); LocalTime japan = LocalTime.now(Clock.system(ZoneId.of("Japan" ))); LocalTime japan2 = LocalTime.now(ZoneId.of("Japan" )); LocalTime localTime = LocalTime.of(15 , 22 ); LocalTime from = LocalTime.from(LocalDateTime.now()); LocalTime localTime1 = LocalTime.ofNanoOfDay(1 ); LocalTime localTime2 = LocalTime.ofSecondOfDay(1 ); System.out.println("LocalTime.now(Clock.systemDefaultZone()) => " + systemDefault); System.out.println("LocalTime.now(Clock.system(ZoneId.of(\"Japan\"))) => " + japan); System.out.println("LocalTime.now(ZoneId.of(\"Japan\")) => " + japan2); System.out.println("LocalTime.of(15, 22) => " + localTime); System.out.println("LocalTime.from(LocalDateTime.now()) => " + from); System.out.println("LocalTime.ofNanoOfDay(1) => " + localTime1); System.out.println("LocalTime.ofSecondOfDay(1) => " + localTime2); }
LocalDateTime LocalDateTime
:LocalDateTime
类是LocalDate
和LocalTime
的结合体 ,可以通过of()
方法直接创建,也可以调用LocalDate
的atTime()
方法或LocalTime
的atDate()
方法将LocalDate
或LocalTime
合并成一个LocalDateTime
,下面是一些简单的方法测试,由于篇幅有限,后续会结合这些内容编写一个工具类的代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Test public void localDateTimeTest () throws Exception { LocalDateTime parse1 = LocalDateTime.parse("2011-12-03T10:15:30" ); LocalDateTime parse = LocalDateTime.parse("2021-11-11 15:30:11" , DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss" )); System.out.println("LocalDateTime.parse(....) => " + parse1); System.out.println("LocalDateTime.parse(....) => " + parse); LocalDateTime of = LocalDateTime.of(LocalDate.now(), LocalTime.now()); LocalDateTime japan = LocalDateTime.now(ZoneId.of("Japan" )); System.out.println("LocalDateTime.of(LocalDate.now(), LocalTime.now()) => " + of); System.out.println("LocalDateTime.now(ZoneId.of(\"Japan\")) => " + japan); }
细粒度机器时间操作 JDK8还对机器的时间进行了分类,比如像下面这样
Instant
Instant
用于表示一个时间戳,它与我们常使用的System.currentTimeMillis()
有些类似,不过Instant
可以精确到纳秒(Nano-Second)
注意: 内部使用了两个常量,seconds
表示从1970-01-01 00:00:00开始到现在的秒数,nanos
表示纳秒部分(nanos
的值不会超过999,999,999
)
下面是一些具体的测试用例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Test public void instantTest () throws Exception { Instant now = Instant.now(); Instant instant = Instant.ofEpochSecond(3 , 0 ); Instant instant1 = Instant.ofEpochSecond(5 , 1_000_000_000 ); System.out.println("Instant.now() => " + now); System.out.println("Instant.ofEpochSecond => " + instant); System.out.println("Instant.ofEpochSecond => " + instant1); System.out.println("Instant.get(ChronoField.NANO_OF_SECOND) => " + now.get(ChronoField.NANO_OF_SECOND)); }
Duration
Duration
的内部实现与Instant
类似,也是包含两部分:seconds
表示秒,nanos
表示纳秒。两者的区别是Instant
用于表示一个时间戳(或者说是一个时间点),而Duration
表示一个时间段,比如想要获取两个时间的差值:
1 2 3 4 5 6 7 8 9 @Test public void durationTest () throws Exception { Duration between = Duration.between(LocalDateTime.parse("2011-12-03T10:15:30" ), LocalDateTime.parse("2021-08-08T10:15:30" )); System.out.println("Duration.between(LocalDateTime.parse(\"2011-12-03T10:15:30\"), LocalDateTime.parse(\"2021-08-08T10:15:30\")) => " + between); Duration duration = Duration.ofDays(7 ); System.out.println("Duration.ofDays(7) => " + duration); }
Period
Period
在概念上和Duration
类似,区别在于Period
是以年月日 来衡量一个时间段(比如2年3个月6天),下面是对应单元测试以及相关的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Test public void periodTest () throws Exception { Period between = Period.between(LocalDate.parse("2011-12-03" ), LocalDate.parse("2021-08-08" )); Period period = Period.ofWeeks(53 ); Period period1 = Period.ofWeeks(22 ); System.out.println("Period.between(LocalDate.parse(\"2011-12-03\"), LocalDate.parse(\"2021-08-08\")) => " + between); System.out.println("Period.ofWeeks(53) => " + period); System.out.println("Period.ofWeeks(53) getDays => " + period.getDays()); System.out.println("Period.ofWeeks(53) getMonths => " + period.getMonths()); System.out.println("Period.ofWeeks(22) getMonths => " + period1.getMonths()); System.out.println("Period.ofWeeks(22) getYears => " + period1.getYears()); }
TemporalAdjusters 复杂日期操作 这个类可以对于时间进行各种更加复杂的操作,比如下一个工作日,本月的最后一天,这时候我们可以借助with
这个方法进行获取:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Test public void testTemporalAdjusters () { LocalDate of = LocalDate.of(2021 , 8 , 1 ); LocalDate with = of.with(TemporalAdjusters.firstDayOfYear()); System.out.println(" TemporalAdjusters.firstDayOfYear => " + with); LocalDate with1 = of.with(TemporalAdjusters.next(DayOfWeek.SATURDAY)); System.out.println(" TemporalAdjusters.next(DayOfWeek.SATURDAY) => " + with1); LocalDate with2 = of.with(TemporalAdjusters.lastDayOfMonth()); System.out.println("TemporalAdjusters.lastDayOfMonth() => " + with2); }
下面从网络找到一份表,对应所有的方法作用
方法名
描述
dayOfWeekInMonth
返回同一个月中每周的第几天
firstDayOfMonth
返回当月的第一天
firstDayOfNextMonth
返回下月的第一天
firstDayOfNextYear
返回下一年的第一天
firstDayOfYear
返回本年的第一天
firstInMonth
返回同一个月中第一个星期几
lastDayOfMonth
返回当月的最后一天
lastDayOfNextMonth
返回下月的最后一天
lastDayOfNextYear
返回下一年的最后一天
lastDayOfYear
返回本年的最后一天
lastInMonth
返回同一个月中最后一个星期几
next / previous
返回后一个/前一个给定的星期几
nextOrSame / previousOrSame
返回后一个/前一个给定的星期几,如果这个值满足条件,直接返回
这个类可以认为是用来替代SimpleDateFormat
这个类,他拥有更加强大的定制化操作,同时他是线程安全的类,不用担心多线程访问会出现问题。
下面是根据DateTimeFormatter 构建一个本土化的格式化器,代码也十分的简单易懂:
1 2 3 4 5 6 7 private static DateTimeFormatter generateDefualtPattern (String timeFormat) { return new DateTimeFormatterBuilder().appendPattern(timeFormat) .parseDefaulting(ChronoField.HOUR_OF_DAY, 0 ) .parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0 ) .parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0 ) .toFormatter(Locale.CHINA); }
时区信息 时区信息一般用的比较少,在做和国际化相关的操作时候有可能会用到,比如最近个人从苹果买了一个东西,虽然我下单是在6号,但是电话说订单时间却是5号下单的,这里个人认为苹果的确切下单时间是按照美国时间算的。
JDK8日期类关于时区的强相关类(注意是JDK8才出现的类,不要误认为是对之前类的兼容),在之前的单元测试其实已经用到了相关时区的方法,在JDK8中使用了 ZoneId
这个类来表示,但是我们有时候不知道怎么获取地区,可以参考下面的内容:
1 2 LocalTime japan = LocalTime.now(Clock.system(ZoneId.of("Japan" )));
实战 - 封装日期工具类 当然更加建议读者自己多动手实验,最好的办法就是多给几个需求给自己,强制自己用JDK8的方法去实现,你会发现你掌握这些API会特别快。
注意事项: 所有的工具代码都使用了同一个本地格式化器构建方法:generateDefualtPattern()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 private static DateTimeFormatter generateDefualtPattern (String timeFormat) { return new DateTimeFormatterBuilder().appendPattern(timeFormat) .parseDefaulting(ChronoField.HOUR_OF_DAY, 0 ) .parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0 ) .parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0 ) .toFormatter(Locale.CHINA); }
获取指定时间的上一个工作日和下一个工作日 注意这个版本是不会判断节假日这些内容的,当然这里是手动实现的版本。
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 public static String getPreWorkDay (String time, String formattPattern) { DateTimeFormatter dateTimeFormatter = generateDefualtPattern(formattPattern); LocalDateTime compareTime1 = LocalDateTime.parse(time, dateTimeFormatter); compareTime1 = compareTime1.with(temporal -> { DayOfWeek dayOfWeek = DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK)); int dayToMinu = 1 ; if (dayOfWeek == DayOfWeek.SUNDAY) { dayToMinu = 2 ; } if (dayOfWeek == DayOfWeek.SATURDAY) { dayToMinu = 1 ; } return temporal.minus(dayToMinu, ChronoUnit.DAYS); }); return compareTime1.format(dateTimeFormatter); } public static String getNextWorkDay (String time, String formattPattern) { DateTimeFormatter dateTimeFormatter = generateDefualtPattern(formattPattern); LocalDateTime compareTime1 = LocalDateTime.parse(time, dateTimeFormatter); compareTime1 = compareTime1.with(temporal -> { DayOfWeek dayOfWeek = DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK)); int dayToAdd = 1 ; if (dayOfWeek == DayOfWeek.FRIDAY) { dayToAdd = 3 ; } if (dayOfWeek == DayOfWeek.SATURDAY) { dayToAdd = 2 ; } return temporal.plus(dayToAdd, ChronoUnit.DAYS); }); return compareTime1.format(dateTimeFormatter); }
判断当前时间是否小于目标时间 判断当前时间是否小于目标时间,这里结合了之前我们学到的一些方法,注意这里的时区使用的是当前系统的时区,如果你切换别的时区,可以看到不同的效果。另外这里使用的是LocalDateTime
不要混淆了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public static boolean isBefore (String time, String format) { DateTimeFormatter dateTimeFormatter = generateDefualtPattern(format); LocalDateTime compareTime = LocalDateTime.parse(time, dateTimeFormatter); LocalDateTime current = LocalDateTime.parse(getNowByNew(format), dateTimeFormatter); long compare = Instant.from(compareTime.atZone(ZoneId.systemDefault())).toEpochMilli(); long currentTimeMillis = Instant.from(current.atZone(ZoneId.systemDefault())).toEpochMilli(); return currentTimeMillis < compare; }
获取指定时间属于星期几 属于对JDK8自身的方法进行二次封装。
1 2 3 4 5 6 7 8 9 10 11 12 public static DayOfWeek getDayOfWeek (String date, String formattPattern) { DateTimeFormatter dateTimeFormatter = generateDefualtPattern(formattPattern); return LocalDate.parse(date, dateTimeFormatter).getDayOfWeek(); }
获取开始日期和结束日期之间的日期 这里需要注意不是十分的严谨,最好是在执行之前日期的判断
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public static final String yyyyMMdd = "yyyy-MM-dd" ;public static List<String> getMiddleDateToString (String startTime, String endTime) { LocalDate begin = LocalDate.parse(startTime, DateTimeFormatter.ofPattern(yyyyMMdd)); LocalDate end = LocalDate.parse(endTime, DateTimeFormatter.ofPattern(yyyyMMdd)); List<LocalDate> localDateList = new ArrayList<>(); long length = end.toEpochDay() - begin.toEpochDay(); for (long i = length; i >= 0 ; i--) { localDateList.add(end.minusDays(i)); } List<String> resultList = new ArrayList<>(); for (LocalDate temp : localDateList) { resultList.add(temp.toString()); } return resultList; }
日期API常见的坑: LocalDateTime
的格式化yyyy-MM-dd
报错: 第一次使用,最容易出现问题的diamante如下的形式所示,比如我们
1 LocalDateTime parse2 = LocalDateTime.parse("2021-11-11" , DateTimeFormatter.ofPattern("yyyy-MM-dd" ));
在运行的时候,会抛出如下的异常:
1 java.time.format.DateTimeParseException: Text '2021-11-11' could not be parsed: Unable to obtain LocalDateTime from TemporalAccessor: {},ISO resolved to 2021 -11 -11 of type java.time.format.Parsed
下面来说一下解决办法:
第一种解决办法比较蛋疼,但是确实是一种非常稳妥的解决方法。
1 2 3 4 5 6 7 try { LocalDate localDate = LocalDate.parse("2019-05-27" , DateTimeFormatter.ofPattern("yyyy-MM-dd" )); LocalDateTime localDateTime = localDate.atStartOfDay(); System.out.println(localDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss" ))); } catch (Exception ex) { ex.printStackTrace(); }
另外,还有一种方法是使用下面的方法,构建一个”中国化”的日期格式器:
1 2 3 4 5 6 7 8 9 10 11 12 13 private static DateTimeFormatter generateDefualtPattern (String timeFormat) { return new DateTimeFormatterBuilder().appendPattern(timeFormat) .parseDefaulting(ChronoField.HOUR_OF_DAY, 0 ) .parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0 ) .parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0 ) .toFormatter(Locale.CHINA); }
调用format出现xx not be parsed, unparsed text found at index 10
问题原因:使用错误的格式去格式字符串,比如yyyy-MM-dd
格式化 2020-05-12 12:15:33
这种格式就会出现溢出,解决办法:使用正确的格式即可
对于上面几个问题的根本解决办法 原因:因为localdatetime 在进行格式化的时候如何case没有找到对应的格式,那么就会出现类似unsupport
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 private static DateTimeFormatter generateDefualtPattern (String timeFormat) { return new DateTimeFormatterBuilder().appendPattern(timeFormat) .parseDefaulting(ChronoField.HOUR_OF_DAY, 1 ) .parseDefaulting(ChronoField.MINUTE_OF_HOUR, 1 ) .parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0 ) .toFormatter(Locale.CHINA); }
下面是其他的问题回答:
StackFlow地址:DateTimeParseException: Text could not be parsed: Unable to obtain LocalDateTime from TemporalAccessor
StackFlow地址:StackFlow无法解析文本:无法从TemporalAccessor获取LocalDateTime
StackFlow地址:解析LocalDateTime(Java 8)时,无法从TemporalAccessor获取LocalDateTime
DateTimeParseException一些小坑 参考了下面的异常日志,根本的原因是DateTimeFormatter
格式化没有HH
选项,这也是比较坑的地方
1 java.time.format.DateTimeParseException: Text '2017-02-02 08:59:12' could not be parsed: Unable to obtain LocalDateTime from TemporalAccessor: {MinuteOfHour=59 , NanoOfSecond=0 , SecondOfMinute=12 , MicroOfSecond=0 , MilliOfSecond=0 , HourOfAmPm=8 },ISO resolved to 2017 -02 -02 of type java.time.format.Parsed
总结: 在个人编写工具类的过程中,发现确实比之前的Date
和Calendar
这两个类用起来好很多,同时JDK8的日期类都是线程安全 的。当然JDK8对于国内使用不是十分友好,这也没有办法毕竟是老外的东西,不过解决办法也有不少,习惯了将解决套路之后也可以接受。最后,有条件最好使用谷歌的搜索引擎,不仅可以帮你把坑跨过去,老外很多大神还会给你讲讲原理,十分受用。
写在最后 写稿不易,求赞,求收藏。
最后推荐一下个人的微信公众号:“懒时小窝 ”。有什么问题可以通过公众号私信和我交流,当然评论的问题看到的也会第一时间解答。
其他问题
关于LocalDate的一个坑
关于LocalDate一些源码分析
直接上源代码,LocalDate
仅代表一个日期,而不代表DateTime。因此在格式化时“ HH:mm:ss ”是毫无意义的,如果我们的格式化参数不符合下面的规则,此方法会抛出异常并且说明不支持对应的格式化操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 private int get0 (TemporalField field) { switch ((ChronoField) field) { case DAY_OF_WEEK: return getDayOfWeek().getValue(); case ALIGNED_DAY_OF_WEEK_IN_MONTH: return ((day - 1 ) % 7 ) + 1 ; case ALIGNED_DAY_OF_WEEK_IN_YEAR: return ((getDayOfYear() - 1 ) % 7 ) + 1 ; case DAY_OF_MONTH: return day; case DAY_OF_YEAR: return getDayOfYear(); case EPOCH_DAY: throw new UnsupportedTemporalTypeException("Invalid field 'EpochDay' for get() method, use getLong() instead" ); case ALIGNED_WEEK_OF_MONTH: return ((day - 1 ) / 7 ) + 1 ; case ALIGNED_WEEK_OF_YEAR: return ((getDayOfYear() - 1 ) / 7 ) + 1 ; case MONTH_OF_YEAR: return month; case PROLEPTIC_MONTH: throw new UnsupportedTemporalTypeException("Invalid field 'ProlepticMonth' for get() method, use getLong() instead" ); case YEAR_OF_ERA: return (year >= 1 ? year : 1 - year); case YEAR: return year; case ERA: return (year >= 1 ? 1 : 0 ); } throw new UnsupportedTemporalTypeException("Unsupported field: " + field); }
格式化问题:
调用DateFomatter 有可能的报错,基本是由于使用错误到格式或者使用错误的时间类
Error java.time.format.DateTimeParseException: could not be parsed, unparsed text found at index 10
参考资料
侠说java8-LocalDateTime