现在网上的Java教程,关于Java日期和时间,很多都介绍的是java.util.Date。
Date date = new Date(); SimpleDateFormat dateFormat = new SimpleDateFormat ("yyyy-MM-dd hh:mm:ss"); String dateStr = dateFormat.format(date); Date parsedDate = dateFormat.parse("2022-02-06 10:16:34");
‘java.util.Date’是Java 1.0引入的,Date中的大部分方法在Java 1.1引入Calendar之后被废弃了(标注为Deprecated)。Date的API非常难用,它的实例都是可变的,而且不能处理闰秒等问题。Java 8引入的java.time API纠正了过去的缺陷,将来会长时间为我们服务。
在Java 8新日期和时间API出现之前,大部分项目都使用Joda-Time来处理日期和时间,Java 8新的日期和时间API和Joda-Time很相似,很多类的命名都和Joda-Time一样。自从Java 8之后,Joda-Time官网发布说:
Joda-Time is the de facto standard date and time library for Java prior to Java SE 8. Users are now asked to migrate to java.time (JSR-310).
至此Java 8的java.time API成为标准,Java 8之前的老项目才需要使用Joda-Time。学习Java 8的新日期时间API,要记住它的对象实例都是不可变的,任何操作都只是生成一个新的对象。
1. Instant和Duration
Instant对象表示时间轴上的一个点,原点(也称为元年)被规定为1970年1月1日的午夜,此时本初子午线正在穿过伦敦格林威治皇家天文台。UNIX/POSIX时间也是用了一样的约定。从原点开始,时间按照每天86400秒进行计算,向前向后分别以纳秒为单位。Instant最小值(Instant.MIN)可以回退到10亿年之久;Instant最大值(Instant.MAX)表示1000000000年12月31日。
Duration表示两个Instant之间的距离,使用Duration.between静态方法,下面的例子是计算某算法的运行时间:
Instant start = Instant.now(); runAlgorithm(); Instant end = Instant.now(); Duration timeElapsed = Duration.between(start, end); long millis = timeElapsed.toMillis(); // timeElapsed.toDays(); // timeElapsed.toHours(); // timeElapsed.toMinutes(); // timeElapsed.toNanos();
Instant和Duration的数学操作:
方法 | 描述 |
plus, minus | 对当前Instant或Duration,增加或减少一段时间 |
plusNanos, plusMillis, plusSeconds, plusMinutes, plusHours, plusDays | 根据指定单位,对当前Instant或Duration添加一段时间 |
minusNanos, minusMillis, minusSeconds, minusMinutes, minusHours, minusDays | 根据指定单位,对当前Instant或Duration减少一段时间 |
multiplieBy, divideBy, negated | 仅对Duration,乘以,除以一个long值;negated表示取相反数。 |
isZero, isNegative | 仅对Duration,判断是否为0,是否为负数 |
下面代码,检查某个算法是否比另一个算法快10倍。
Duration timeElapsed1 = Duration.between(start. 1, end1); Duration timeElapsed2 = Duration.between(start2, end2); boolean overTenTimesFaster = timeElapsed2.multipliedBy(10).minus(timeElapsed1).isNegative();
2. LocalDate和Period
LocalDate表示一个没有时区的日期(年月日),使用LocalDate的now或这of方法来创建LocaDate。
LocalDate today = LocalDate.now(); LocalDate alonzosBirthday = LocalDate.of(1903, 6, 4); // The eighth day of March 2022 LocalDate womenDay = LocalDate.of(2022, Month.MARCH, 8); // The 256th day of 2022. LocalDate programmersDay = LocalDate.ofYearDay(2022, 256);
前面提到两个Instant之间的距离叫做Duration。对于两个LocalDate,它们之间的距离叫做Period,使用until方法得到Period:
LocalDate today = LocalDate.of(2022, 2, 15); LocalDate nextSpringFestival = LocalDate.of(2023, 1, 22); Period period = today.until(nextSpringFestival); System.out.println(period.getYears()); // 0 System.out.println(period.getMonths()); // 11 System.out.println(period.getDays()); // 7 // Get specific years, months, days long years = today.until(nextSpringFestival, ChronoUnit.YEARS); long months = today.until(nextSpringFestival, ChronoUnit.MONTHS); long weeks = today.until(nextSpringFestival, ChronoUnit.WEEKS); long days = today.until(nextSpringFestival, ChronoUnit.DAYS); System.out.println(String.format( "Years: %s, months: %s, weeks: %s, days: %s", years, months, weeks, days)); // Output: Years: 0, months: 11, weeks: 48, days: 341
LocalDate常用的方法:
方法 | 描述 |
plusDays, plusWeeks, plusMonths, plusYears | 增加几天,几周,几个月或者几年 |
minusdays, minusWeeks, minusMonths, mimusYears | 减少几天,几周,几个月或者几年 |
plus, minus | 添加或减少一个Duration或者Period |
withDayOfMonth withDayOfYear withMonth withYear | 将月份天数、年份天数,月份,年份修改为指定值,返回一个新的LocalDate |
getYear getMonth getDayOfYear getDayOfMonth getDayOfWeek | 获得年份 获得月份:Month枚举值 获得该年的第几天(1~366之间) 获得该月的第几天(1-31之间) 获得星期几:DayOfWeek的枚举 |
isBefore, isAfter | 比较两个LocalDate |
isLeapYear | 判断是否为闰年 |
使用plusXXX 或 minusXXX 修改LocalDate可能会产生不存在的日期。例如,给1月31日增加一个月不应该产生2月31日。与抛出异常相反,这些方法会返回该月最后一个有效的日期。例如:LocalDate.of(2022, 1, 31).plusMonths(1) 和 LocalDate.of(2022, 3, 31).minusMonths(1) 都会返回2016年2月29日。
DayOfWeek是星期的枚举:MONDAY, TUESDAY, WEDNESDAY, etc.
Month是月份的枚举:JANUARY, FEBRUARY, MARCH, etc.
3. TemporalAdjuster(日期校正器)
对于一些需要安排调度的应用程序来说,通常需要计算例如“每个月的第一个周二”这样的日期。TemporalAdjusters类提供了许多静态方法进行常用的校正。例如获得某月的第二个周二:
LocalDate firstTuesday = LocalDate.of(2022, 2, 1) .with(TemporalAdjusters.nextOrSame(DayOfWeek.TUESDAY));
新API对象都是不可变的,with方法会返回一个新的LocalDate对象,而不会修改原有的对象。下面列出了可用的矫正器, DayOfWeek是星期的枚举:
方法 | 描述 |
next(DayOfWeek), previous(DayOfWeek) | 前一个或下一个DayOfWeek |
nextOrSame(DayOfWeek), previousOrSame(DayOfWeek) | 同next和previous,不过如果当前日期就是输入的DayOfWeek,返回当前日期 |
dayOfWeekInMonth(n, DayOfWeek) | 本月的第n个DayOfWeek |
lastInMonth(weekday) | 本月的最后一个DayOfWeek |
firstDayOfMonth(), firstDayOfNextMonth(), firstDayOfNextYear() | 如方法名所示 |
lastDayOfMonth(), lastDayOfPreviousMonth(), firstDayOfYear(), | 如方法名所示 |
根据需要我们还可以实现自己的校正器。
TemporalAdjuster nextWorkdayAdjuster = TemporalAdjusters.ofDateAdjuster(localDate -> { LocalDate result = localDate; do { result = result.plusDays(1); } while (result.getDayOfWeek().getValue() >= 6); return result; }); LocalDate.of(2022, 2, 11).with(nextWorkdayAdjuster); // 2022-02-14
4. LocalTime
LocalTime表示一天中的某个时间,例如15:30:00。创建LocalTime:
LocalTime rightNow = LocalTime.now(); LocalTime.of(17, 30); // 17:30 LocalTime.of(17, 30, 12); // 17:30:12 LocalTime.of(17, 30, 12, 450); // 17:30:12:450
LocalTime的相关方法:
方法 | 描述 |
plusHours, plusMinutes, plusSeconds, plusNanos | 增加一段时间 |
minusHours, minusMinutes, minusSeconds, minusNanos | 减去一段时间 |
plus, minus | 增加或前去一个Duration |
withHour, withMinute, withSecond, withNano | 修改对应值,并返回新的LocalDate |
getHour, getMinute, getSecond, getNano | 获得对应的值 |
toSecondOfDay, toNanoOfDay | 转换成秒,纳秒。 |
isBefore, isAfter | 比较两个LocalTime |
LocalTime本身不关心是AM还是PM,而是交给格式化程序来做这件事。
LocalDateTime 是 LocalDate 和 LocalTime 的合体,它的实例对象包含了 LocalDate 和 LocalTime 的方法。
5. ZonedDateTime
Internet Assigned Numbers Authority(IANA) 维护着一份全球所以已知时区的数据库(https://www.iana.org/time-zones),每年会更新几次,这些更新主要是处理夏令时规则的改变。Java就使用了IANA的数据库。
每个时区都有一个ID,例如America/Los_Angeles或者Asia/Shanghai。调用ZoneId.getAvailableZoneIds()可获得所有的时区,写该博客时有590个。
ZoneId zoneShangHai = ZoneId.of("Asia/Shanghai"); // ZoneId systemZone = ZoneId.systemDefault(); LocalDateTime dateTime = LocalDateTime.of(2022, 1, 1, 12, 30); ZonedDateTime dateTimeAtShanghai = dateTime.atZone(zoneShangHai); // Static methods of creating ZonedDateTime ZonedDateTime.of(localDateTime, zone) ZonedDateTime.of(date, time, zone) ZonedDateTime.of(year, month, day, hour, minute, second, nanoOfSecond, zone) ZonedDateTime.ofInstant(instant, zone) ZonedDateTime.ofInstant(localDateTime, offset, zone)
上面介绍的LocalDataTime的方法,ZonedDataTime也有,这里不多列举。下面列举些仅ZonedDataTime有的方法:
LocalDateTime dateTime = LocalDateTime.of(2022, 1, 1, 12, 30); ZonedDateTime dateTimeAtShanghai = dateTime.atZone(ZoneId.of("Asia/Shanghai")); ZoneId zoneNewYork = ZoneId.of("America/Los_Angeles"); System.out.println(dateTimeAtShanghai.withZoneSameInstant(zoneNewYork)); // Output: 2021-12-31T20:30-08:00[America/Los_Angeles] System.out.println(dateTimeAtShanghai.withZoneSameLocal(zoneNewYork)); // Output: 2022-01-01T12:30-08:00[America/Los_Angeles] // Convert to LocalDate, LocalTime, Instant LocalDate localDate = dateTimeAtShanghai.toLocalDate(); LocalTime localTime = dateTimeAtShanghai.toLocalTime(); Instant instant = dateTimeAtShanghai.toInstant();
ZoneId.of(“UTC”)获得的UTC时区。UTC表示“协调世界时”,该缩写是英语”coordinated universal time“和法语“Temps universel coordonné”二者妥协的产物,与任何一个都不直接匹配。UTC是格林威治皇家天文台的时间,不包含夏令时。
当夏令时开始时,时钟会前进一小时。例如,在2013年,中欧从3月31日2:00进入夏令时,此时时钟会被调快1小时,变成3:00。如果你创建一个不存在的时间点2013年3月2:30,那么实际上你会得到3月31日3:30。
ZonedDateTime.of( LocalDate.of(2013, 3, 31), LocalTime.of(2, 30), ZoneId.of("Europe/Berlin")); // Create: 2013-3-31-3:30
反过来,当夏令时介绍时,时钟会回退一小时,因此有两个拥有相同本地时间的瞬时点!当你创建这一小时之内的时间时,你会得到二者中较早的哪个。
ZonedDateTime ambiguous = ZonedDateTime.of( LocalDate.of(2013, 10, 27), // End daylight saving time LocalTime.of(2, 30), ZoneId.of("Europe/Berlin")); // 2013-10-27T02:30+02:00[Europe/Berlin] ZonedDateTime anHourLater = ambiguous.plusHours(1); // 2013-10-27T02:30+01:00[Europe/Berlin]
1小时之后,时间的小时和分钟没变,但是时区偏移量改变了。
当调整一个跨夏令时的时区时,要多加小心。例如,如果你安排了下周的一个会议,不要添加7天的Duration对象。
meeting.plus(Duration.ofDays(7)); // Caution. Cannot handle 'daylight saving time'
相反应当使用Period:
ZonedDateTime nextMeeting = meeting.plus(Period.ofDays(7)); // Right. It takes 'daylight saving time' into account
6. DateTimeFormatter(格式化和解析)
DateTimeFormatter类提供了三种格式话日期/时间的方法:
- 预定义的标准格式
- 语言环境相关的格式
- 自定义的格式
6.1 预定义标准格式
ZonedDateTime newYear = ZonedDateTime.of( LocalDateTime.of(2022, 1, 1, 9, 0), ZoneId.systemDefault()); String formatted = DateTimeFormatter.ISO_DATE_TIME.format(newYear); // 2022-01-01T09:00:00+08:00[Asia/Shanghai]
下面表格示例格式化的日期是:2022年1月1日9:00。
格式 | 描述 | 示例 |
BASIC_ISO_DATE | 日期没有分隔符 | 20220101+0800 |
ISO_LOCAL_DATE ISO_LOCAL_TIME ISO_LOCAL_DATE_TIME | 分隔符为- : T | 2022-01-01 09:00:00 2022-01-01T09:00:00 |
ISO_OFFSET_DATE ISO_OFFSET_TIME ISO_OFFSET_DATE_TIME | 同ISO_LOCAL_XXX,但有时区 | 2022-01-01+08:00 09:00:00+08:00 2022-01-01T09:00:00+08:00 |
ISO_ZONED_DATE_TIME | 含有时区和时区ID | 2022-01-01T09:00:00+08:00[Asia/Shanghai] |
ISO_INSTANT | 用z时区ID表示的UTC时间 | 2022-01-01T01:00:00Z |
ISO_ORDINAL_DATE | 年份-天数 | 2022-001+08:00 |
ISO_WEEK_DATE | 年份-第几周-星期几 | 2021-W52-6+08:00 |
RFC_1123_DATE_TIME | RFC定义的邮件时间戳 | Sat, 1 Jan 2022 09:00:00 +0800 |
6.2 语言相关的格式化
下面表格示例格式化的日期是:1969年7月16号9:32。EDT表示 Eastern Daylight Time。
风格 | 日期 | 时间 |
SHORT | 7/16/69 | 9:32 AM |
MEDIUM | Jul 16, 1969 | 9:32:00 AM |
LONG | July 16, 1969 | 9:32:00 AM EDT |
FULL | Wednesday, July 16, 1969 | 9:32:00 AM EDT |
因为日期时区是 America/New_York, 所以下面示例默认就按照美国的语言格式化。
ZonedDateTime dateTime = ZonedDateTime.of( LocalDateTime.of(1969, 7, 16, 9, 32), ZoneId.of("America/New_York")); DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG); String dateTimeStr = formatter.format(dateTime); // July 16, 1969 9:32:00 AM EDT
如果要切换为其它语言,就使用withLocale方法。
formatter.withLocale(Locale.CHINESE).format(dateTime); // 1969年7月16日 上午09时32分00秒
6.3 自定义格式格式化
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); formatter.format(dateTime); // 1969-07-16 09:32
6.4 解析日期/时间字符串
使用LocalDate, Localtime, LocalDateTime, ZonedDateTime的parse方法便可。
LocalDate.parse("1903-06-14", DateTimeFormatter.ISO_LOCAL_DATE); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ssxx"); ZonedDateTime.parse("1969-07-16 03:32:00-0400", formatter);
7. 与遗留代码互操作
Java 8引入的日期和时间API不得不与已有的类之间互相操作,尤其是无处不在的Java.util.Date, java.util.GregorianCalendar 及 Java.sql.Date/Time/Timestamp。
类 | To遗留类 | From遗留类 |
Instant ↔ Java.util.Date | Date.from(instant) | date.toInstant() |
ZonedDateTime ↔ java.util.GregorianCalendar | GregorianCalendar.from(zonedDateTime) | cal.toZonedDateTime() |
Instant ↔ java.sql.Timestamp | TimeStamp.from(instant) | timestamp.toInstant() |
LocalDateTime ↔ java.sql.Timestamp | TimeStamp.valueOf(localDateTime) | timeStamp.toLocalDateTime() |
LocalDate ↔ java.sql.Time | Date.valueOf(localDate) | date.toLocalDate() |
LocalTime ↔ java.sql.time | Time.valueOf(localTime) | date.toLocalTime() |
DateTimeFormatter → java.text.DateFormat | formatter.toFormat() | 无 |
java.util.TimeZone ↔ ZoneId | Timezone.getTimeZone(id) | timeZone.toZoneId |
java.nio.file.attribute.FileTime ↔ Instant | FileTime.from(instant) | fileTime.toInstant |