在 JDK1.0 的时候, Java 引入了 java.util.Date 来处理日期和时间; 在 JDK1.1 的时候又引入了功能更强大的 java.util.Calendar , 但是 Calendar 的 API 还是不尽如人意,, 存在实例易变, 没有处理闰秒等等的问题. 所以在 JDK1.8 的时候, Java 引入了 java.time API, 这才真正修改了过去的缺陷, 且更为好用. 本篇就详细介绍一下 JDK1.8 的日期和时间 API.
本篇主要包括以下内容:
Java8 之前的日期和时间 API 的缺陷
java.time 类图介绍
概况
- chrono
- format
- temporal
- zone
Java 8 日期 / 时间类
- Instant
- Duration
- Period
LocalDate 和 LocalTime
LocalDateTime
日期操作和格式化
时区
Java8 之前的日期和时间 API 的缺陷
在 Java 8 之前, 所有关于时间和日期的 API 都存在各种使用方面的缺陷, 主要有:
Java 的 java.util.Date 和 java.util.Calendar 类易用性差, 不支持时区, 而且他们都不是线程安全的;
用于格式化日期的类 DateFormat 被放在 java.text 包中, 它是一个抽象类, 所以我们需要实例化一个 SimpleDateFormat 对象来处理日期格式化, 并且 DateFormat 也是非线程安全, 这意味着如果你在多线程程序中调用同一个 DateFormat 对象, 会得到意想不到的结果.
对日期的计算方式繁琐, 而且容易出错, 因为月份是从 0 开始的, 从 Calendar 中获取的月份需要加一才能表示当前月份.
由于以上这些问题, 出现了一些第三方的日期处理框架, 例如 Joda-Time,date4j 等开源项目. 但是, Java 需要一套标准的用于处理时间和日期的框架, 于是 Java 8 中引入了新的日期 API. 新的日期 API 是 JSR-310 规范的实现, Joda-Time 框架的作者正是 JSR-310 的规范的倡导者, 所以能从 Java 8 的日期 API 中看到很多 Joda-Time 的特性.
java.time 类图介绍
概况
首先来看一下 java.time 这个包下的类结构图:
可以看到, 除了一些日期, 时间类之外, 还有四个包: chrono,format,temporal,zone.
先简略介绍下这四个包的用途.
chrono
chrono 包提供历法相关的接口与实现.
Java 中默认使用的历法是 ISO 8601 日历系统, 它是世界民用历法, 也就是我们所说的公历. 平年有 365 天, 闰年是 366 天. 闰年的定义是: 非世纪年, 能被 4 整除; 世纪年能被 400 整除. 为了计算的一致性, 公元 1 年的前一年被当做公元 0 年, 以此类推.
此外 chrono 包提供了四种其他历法, 每种历法有自己的纪元 (Era) 类, 日历类和日期类, 分别是:
泰国佛教历: ThaiBuddhistEra,ThaiBuddhistChronology 和 ThaiBuddhistDate;
民国历: MinguoEra,MinguoChronology 和 MinguoDate;
日本历: JapaneseEra,JapaneseChronology 和 JapaneseDate
伊斯兰历: HijrahEra,HijrahChronology 和 HijrahDate:
每个纪元类都是一个枚举类, 实现 Era 接口. Era 表示的是一个时间线的分割, 比如 Java 默认的 ISO 历法中的 IsoEra, 就包含两个枚举量: BCE 和 CE, 前者表示 "公元前", 后者表示 "公元"; 再比如 MinguoEra, 包含了两个枚举量: BEFORE_ROC 和 ROC,ROC 的意思是 Republic of China, 也即新中国, 前者表示的就是新中国之前, 也即民国, 后者表示新中国; 所以中国的历法用了 "Minguo" 这个名字.
每种历法的日历系统的实现都是依赖于其纪元的. 每个日历类都实现了抽象类 AbstractChronology, 其中定义了从时间, id, 地域设置获取具体日历系统的接口和实现, 以及获取特定日历系统下的时间的方法.
定义了纪元和日历系统之后, 日期类自然就确定好了, 每种历法的日期类提供的接口并无大的不同, 在实际开发中应用的比较少, 也不是本篇的重点, 暂且略过.
format
format 包提供了日期格式化的方法.
format 包中定义了时区名称, 日期解析和格式化的各种枚举, 以及最为重要的格式化类 DateTimeFormatter. 需要注意的是, format 包类中的类都是 final 的, 都提供了线程安全的访问.
在 DateTimeFormatter 类中提供了 ofPattern 的静态方法来获得一个 DateTimeFormatter, 但细看其实现, 其实还是调用的 DateTimeFormatterBuilder 的静态方法:
DateTimeFormatterBuilder.appendPattern(pattern).toFormatter();
所以我们在实际格式化日期和时间的时候, 是两种方式都可以使用的.
temporal
temporal 包中定义了整个日期时间框架的基础: 各种时间单位, 时间调节器, 以及在年月日时分秒中用到的各种属性.
Java8 中的日期时间类都是实现了 temporal 包中的时间单位 (Temporal), 时间调节器(TemporalAdjuster) 和各种属性的接口, 所以在后面的日期的操作方法中都是以最基本的时间单位和各种属性为参数的.
zone
这个包没啥多说的, 就是定义了时区转换的各种方法.
Java 8 日期 / 时间类
Java 8 的日期和时间类包括 Instant,Duration,Period,LocalDate,LocalTime, 这些类都包含在 java.time 包中. 下面逐一来看看这些类的用法.
Instant
Instant 是时间线上的一个点, 表示一个时间戳. Instant 可以精确到纳秒, 这超过了 long 的最大表示范围, 所以在 Instant 的实现中是分成了两部分来表示, 一部分是 seconds , 表示从 1970-01-01 00:00:00 开始到现在的秒数, 另一个部分是 nanos , 表示纳秒部分.
以下是创建 Instant 的两种方法:
- Instant now = Instant.now();
- Instant instant = Instant.ofEpochSecond(60, 100000);
获取当前时刻的时间戳, 结果为:
- 2020-02-20T14:14:15.913Z
- ;
ofEpochSecond()方法的第一个参数为秒, 第二个参数为纳秒, 上面的代码表示从 1970-01-01 00:00:00 开始后一分钟的 10 万纳秒的时刻, 其结果为:
- 1970-01-01T00:01:00.000100Z
- .
- Duration
有了时间点, 自然就衍生出时间段了, 那就是 Duration.Duration 的内部实现与 Instant 类似, 也是包含两部分: seconds 表示秒, nanos 表示纳秒.
Duration 是两个时间戳的差值, 所以使用 java.time 中的时间戳类, 例如 Instant,LocalDateTime 等实现了 Temporal 类的日期时间类为参数, 通过 Duration.between()方法创建 Duration 对象:
- LocalDateTime from = LocalDateTime.of(2020, Month.JANUARY, 22, 16, 6, 0); // 2020-01-22 16:06:00
- LocalDateTime to = LocalDateTime.of(2020, Month.FEBRUARY, 22, 16, 6, 0); // 2020-02-22 16:06:00
- Duration duration = Duration.between(from, to); // 表示从 2020-01-22 16:06:00 到 2020-02-22 16:06:00 这段时间
Duration 对象还可以通过 of()方法创建, 该方法接受一个时间段长度, 和一个时间单位作为参数:
- Duration duration1 = Duration.of(5, ChronoUnit.DAYS); // 5 天
- Duration duration2 = Duration.of(1000, ChronoUnit.MILLIS); // 1000 毫秒
- Period
Period 在概念上和 Duration 类似, 区别在于 Period 是以年月日来衡量一个时间段, 比如 1 年 2 个月 3 天:
Period period = Period.of(1, 2, 3);
Period 对象也可以通过 between()方法创建, 值得注意的是, 由于 Period 是以年月日衡量时间段, 所以 between()方法只能接收 LocalDate 类型的参数:
- // 2020-01-22 到 2020-02-22 这段时间
- Period period = Period.between(
- LocalDate.of(2020, 1, 22),
- LocalDate.of(2020, 2, 22));
LocalDate 和 LocalTime
LocalDate 类表示一个具体的日期, 但不包含具体时间, 也不包含时区信息. 可以通过 LocalDate 的静态方法 of()创建一个实例, LocalDate 也包含一些方法用来获取年份, 月份, 天, 星期几等:
- LocalDate localDate = LocalDate.of(2020, 2, 22); // 初始化一个日期: 2022-02-22
- int year = localDate.getYear(); // 年份: 2020
- Month month = localDate.getMonth(); // 月份: February
- int dayOfMonth = localDate.getDayOfMonth(); // 月份中的第几天: 22
- DayOfWeek dayOfWeek = localDate.getDayOfWeek(); // 一周的第几天: Saturday
- int length = localDate.lengthOfMonth(); // 月份的天数: 29
- boolean leapYear = localDate.isLeapYear(); // 是否为闰年: true
也可以调用静态方法 now()来获取当前日期: LocalDate now = LocalDate.now();
LocalTime 和 LocalDate 类似, 他们之间的区别在于 LocalDate 不包含具体时间, 而 LocalTime 包含具体时间, 例如:
- LocalTime localTime = LocalTime.of(16, 14, 52); // 初始化一个时间: 16:14:52
- int hour = localTime.getHour(); // 时: 16
- int minute = localTime.getMinute(); // 分: 14
- int second = localTime.getSecond(); // 秒: 52
- LocalDateTime
LocalDateTime 类是 LocalDate 和 LocalTime 的结合体, 可以通过 of()方法直接创建, 也可以调用 LocalDate 的 atTime()方法或 LocalTime 的 atDate()方法将 LocalDate 或 LocalTime 合并成一个 LocalDateTime:
- LocalDateTime ldt1 = LocalDateTime.of(2020, Month.FEBRUARY, 22, 16, 23, 12);
- LocalDate localDate = LocalDate.of(2020, Month.FEBRUARY, 22);
- LocalTime localTime = LocalTime.of(16, 23, 12);
- LocalDateTime ldt2 = localDate.atTime(localTime);
LocalDateTime 也提供用于向 LocalDate 和 LocalTime 的转化:
- LocalDate date = ldt1.toLocalDate();
- LocalTime time = ldt1.toLocalTime();
日期操作和格式化
在上面对 java.time 包中的类的介绍中已经提到, Java8 的的日期和时间类都实现了 Temporal,TemporalAdjuster, 然后在 temporal 包中定义了日期操作的方法, 在 format 中定义了日期格式化的方法, 由此实现了比较通用的日期操作和格式化的方式.
首先需要再次明确的一点是, Java8 中提供的日期时间对象都是不可变的, 因而也是线程安全的. 所以每次对日期时间对象进行操作的时候都是返回新的日期时间对象.
比较简单的日期操作, 比如增加, 减少一天, 修改年月日等, 代码如下:
- LocalDate date = LocalDate.of(2020, 2, 22); // 2020-02-22
- LocalDate date1 = date.withYear(2021); // 修改为 2021-02-22
- LocalDate date2 = date.withMonth(3); // 修改为 2020-03-22
- LocalDate date3 = date.withDayOfMonth(1); // 修改为 2020-02-01
- LocalDate date4 = date.plusYears(1); // 增加一年 2021-02-22
- LocalDate date5 = date.minusMonths(2); // 减少两个月, 到 2019 年的 12 月 2019-12-22
- LocalDate date6 = date.plus(5, ChronoUnit.DAYS); // 增加 5 天 2020-02-27
比较复杂的日期操作, 比如将时间调到下一个工作日, 或者是下个月的最后一天, 这时候我们可以使用 with()方法的另一个重载方法, 它接收一个 TemporalAdjuster 参数, 可以使我们更加灵活的调整日期:
- LocalDate date7 = date.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY)); // 返回下一个距离当前时间最近的星期日 2020-02-23
- LocalDate date9 = date.with(TemporalAdjusters.lastInMonth(DayOfWeek.SATURDAY)); // 返回本月最后衣蛾周六 2020-02-29
下面列出时间调节器类 TemporalAdjuster 提供的一些方法, 可供选用:
方法名 | 描述
dayOfWeekInMonth | 返回同一个月中每周的第几天
firstDayOfMonth | 返回当月的第一天
firstDayOfNextMonth | 返回下月的第一天
firstDayOfNextYear | 返回下一年的第一天
firstDayOfYear | 返回本年的第一天
firstInMonth | 返回同一个月中第一个星期几
lastDayOfMonth | 返回当月的最后一天
lastDayOfNextMonth | 返回下月的最后一天
lastDayOfNextYear | 返回下一年的最后一天
lastDayOfYear | 返回本年的最后一天
lastInMonth | 返回同一个月中最后一个星期几
next / previous | 返回后一个 / 前一个给定的星期几
nextOrSame / previousOrSame | 返回后一个 / 前一个给定的星期几, 如果这个值满足条件, 直接返回
日期格式化的用法请看上面对 format 包的介绍.
时区
对时区处理的优化也是 Java8 中日期时间 API 的一大亮点. 之前在业务中是真的遇到过一些奇葩的时区问题, 在旧的 java.util.TimeZone 提供的时区不全不说, 操作还非常繁琐.
新的时区类 java.time.ZoneId 是原有的 java.util.TimeZone 类的替代品. ZoneId 对象可以通过 ZoneId.of()方法创建, 也可以通过 ZoneId.systemDefault()获取系统默认时区:
- ZoneId shanghaiZoneId = ZoneId.of("Asia/Shanghai");
- ZoneId systemZoneId = ZoneId.systemDefault();
of()方法接收一个 "区域 / 城市" 的字符串作为参数, 你可以通过 getAvailableZoneIds()方法获取所有合法的 "区域 / 城市" 字符串:
Set<String> zoneIds = ZoneId.getAvailableZoneIds();
对于老的时区类 TimeZone,Java 8 也提供了转化方法:
ZoneId oldToNewZoneId = TimeZone.getDefault().toZoneId();
有了 ZoneId, 我们就可以将一个 LocalDate,LocalTime 或 LocalDateTime 对象转化为 ZonedDateTime 对象:
- LocalDateTime localDateTime = LocalDateTime.now();
- ZonedDateTime zonedDateTime = ZonedDateTime.of(localDateTime, shanghaiZoneId);
将 zonedDateTime 打印到控制台为:
2020-02-22T16:50:54.658+08:00[Asia/Shanghai]
ZonedDateTime 对象由两部分构成, LocalDateTime 和 ZoneId, 其中 2020-02-22T16:50:54.658 部分为 LocalDateTime,+08:00[Asia/Shanghai]部分为 ZoneId.
另一种表示时区的方式是使用 ZoneOffset, 它是以当前时间和世界标准时间 (UTC)/ 格林威治时间(GMT) 的偏差来计算, 例如:
- ZoneOffset zoneOffset = ZoneOffset.of("+09:00");
- LocalDateTime localDateTime = LocalDateTime.now();
- OffsetDateTime offsetDateTime = OffsetDateTime.of(localDateTime, zoneOffset);
以上就是 Java8 中关于日期和时间 API 的内容了.
来源: http://www.tuicool.com/articles/NBBjMjZ