@[toc]
由于java7及以前的版本对时间的处理都存在诸多的问题。自java8之后,引入了新的时间API,现在对这些新的API及其使用进行介绍。
1.Instant
Instant与Date对象类似,都是表示一个时间戳,不同的在于,Instant充分考虑了之前Date精度不足的问题。Date最多支持到毫秒,而cpu对时间精度的要求可能是纳秒。所以Instant在date的基础上进行了扩展,支持纳秒结构。我们可以看看Instant的源码:
/**
* The number of seconds from the epoch of 1970-01-01T00:00:00Z.
*/
private final long seconds;
/**
* The number of nanoseconds, later along the time-line, from the seconds field.
* This is always positive, and never exceeds 999,999,999.
*/
private final int nanos;
/**
* Constructs an instance of {@code Instant} using seconds from the epoch of
* 1970-01-01T00:00:00Z and nanosecond fraction of second.
*
* @param epochSecond the number of seconds from 1970-01-01T00:00:00Z
* @param nanos the nanoseconds within the second, must be positive
*/
private Instant(long epochSecond, int nanos) {
super();
this.seconds = epochSecond;
this.nanos = nanos;
}
在Instant内部,除了与Date一样用一个long类型来表示毫秒之外,还维护了一个int型的nanos用于表示纳秒数。
/**
* Constant for the 1970-01-01T00:00:00Z epoch instant.
*/
public static final Instant EPOCH = new Instant(0, 0);
/**
* The minimum supported epoch second.
*/
private static final long MIN_SECOND = -31557014167219200L;
/**
* The maximum supported epoch second.
*/
private static final long MAX_SECOND = 31556889864403199L;
在其中维护了EPOCH Time(0ms 0ns)。之后我们可以相对EPOCH轻松的初始化时间,需要注意的是,Instant统一采用的都是systemUTC时间。不再像Date一样根据本地时区进行转换。
System.out.println(Instant.now());
System.out.println(Instant.ofEpochMilli(0));
System.out.println(Instant.ofEpochSecond(0,10));
结果如下:
2020-08-06T08:23:18.652Z
1970-01-01T00:00:00Z
1970-01-01T00:00:00.000000010Z
格式都是统一的yyyy-MM-dd,T表示后面接的是时间。Z表示采用统一的UTC时间。
Instant与时区无关,时钟只输出与格林尼治统一时间。
2.无时区的日期和时间LocalDate、LocalTime、LocalDateTime
与Calendar不同的是,在新版本的API中,将日期和时间做了分离,用单独的类进行处理。LocalDate表示日期,LocalTime表示时间,而LocalDateTime则是二者的综合。
2.1 LocalDate
LocalDate表示日期,内部分别维护了year、month、day三个变量。
/**
* The year.
*/
private final int year;
/**
* The month-of-year.
*/
private final short month;
/**
* The day-of-month.
*/
private final short day;
与Date初始化方法不同的是,这里在不是像之前那样有各种特殊的要求,比如date中构造方法要求year从1900开始,month 0 - 11.
LocalDate定义在ChronoField中,如下:
YEAR("Year", YEARS, FOREVER, ValueRange.of(Year.MIN_VALUE, Year.MAX_VALUE), "year"),
MONTH_OF_YEAR("MonthOfYear", MONTHS, YEARS, ValueRange.of(1, 12), "month"),
DAY_OF_MONTH("DayOfMonth", DAYS, MONTHS, ValueRange.of(1, 28, 31), "day"),
这样也跟容易理解。
LocalDate可以采用如下办法初始化:
System.out.println(LocalDate.now());
System.out.println(LocalDate.of(2020,8,6));
System.out.println(LocalDate.ofYearDay(2020,100));
System.out.println(LocalDate.ofEpochDay(18000));
输出都是:
2020-08-06
2020-08-06
2020-04-09
2019-04-14
ofYearDay ofEpochDay都非常容易理解。不会再像之前Calendar那么生涩。
不过需要注意的是,LocalDate输出的是默认的系统时区。
还有很多方法如:
方法名 | 说明 |
---|---|
getYear | 获取当前年份 |
getMonthValue | 获取当前月份 |
getDayOfMonth | 获取当前日期 |
getDayOfYear | 获取当前是一年中的第几天 |
isLeapYear | 是否闰年 |
lengthOfYear | 一年有多少天 |
getDayOfWeek | 返回星期信息 |
2.2 LocalTime
与LocalDate类似,LocalTime专注于时间处理,它提供小时、秒、毫秒、微秒、纳秒等各种事件单位的处理。
其内部主要有hour、minute、second、nano等变量:
/**
* The hour.
*/
private final byte hour;
/**
* The minute.
*/
private final byte minute;
/**
* The second.
*/
private final byte second;
/**
* The nanosecond.
*/
private final int nano;
初了nano 是int类型之外,其他都是byte类型,占一个字节。
of方法提供了很多重载来实现不同参数输入时间的情况。
我们可以如下使用:
System.out.println(LocalTime.now());
System.out.println(LocalTime.of(22,10));
System.out.println(LocalTime.of(22,10,20));
System.out.println(LocalTime.of(22,10,20,1000));
输出结果:
16:51:24.193
22:10
22:10:20
22:10:20.000001
实际上我们可以发现,当用now的时候,精度只有毫秒,这大概还是用的linux的毫秒时间戳。而我们可以让LocalTime显示到纳秒级别。
当然,LocalTime也有很多类似的方法提供:
方法名称 | 说明 |
---|---|
getHour | 获取当前小时数 |
getMinute | 获取当前分钟数 |
getSecond | 获取当前秒数 |
getNano | 获取当前纳秒数 |
withHour | 修改当前的Hour |
withMinute | 修改当前的分钟 |
withSecond | 修改当前的秒数 |
还有很多方法,但是这些方法都很简单,一看就知道什么意思。
2.2 LocalDateTime
LocalDateTime实际上是LocalDate和LocalTime的综合体:
/**
* The date part.
*/
private final LocalDate date;
/**
* The time part.
*/
private final LocalTime time;
其内部提供了date和time两个final的私有变量。之后提供了LocalDate和LocalTime的大部分工具方法。
System.out.println(LocalDateTime.now());
System.out.println(LocalDateTime.of(LocalDate.now(),LocalTime.now()));
System.out.println(LocalDateTime.of(2020,8,6,17,23));
System.out.println(LocalDateTime.of(2020,8,6,17,23,15));
System.out.println(LocalDateTime.of(2020,8,6,17,23,15,100000));
输出结果如下:
2020-08-06T17:24:41.516
2020-08-06T17:24:41.516
2020-08-06T17:23
2020-08-06T17:23:15
2020-08-06T17:23:15.000100
3.与时区相关的时间 ZonedDateTime
前面的LocalDate、LocalTime、LocalDateTime都是与时区无关,默认是本地时区的日期和时间。这也符合Local的定义。但是如果时间需要再多个时区进行转换呢?这就需要ZonedDateTime。
System.out.println(ZonedDateTime.now());
System.out.println(ZonedDateTime.of(LocalDate.now(),LocalTime.now(),ZoneId.of("America/Los_Angeles")));
System.out.println(ZonedDateTime.of(LocalDate.now(),LocalTime.now(),ZoneId.of("EST",ZoneId.SHORT_IDS)));
输出如下:
2020-08-06T17:34:41.337+08:00[Asia/Shanghai]
2020-08-06T17:34:41.337-07:00[America/Los_Angeles]
2020-08-06T17:34:41.340-05:00
这个ZnodeDateTime再初始化的时候,通过of方法,需要传入一个时区。而时区通过简码存储在ZoneId.SHORT_IDS这个Map中。如果需要使用简码,则需要传入这个Map。
public static ZoneId of(String zoneId, Map<String, String> aliasMap) {
Objects.requireNonNull(zoneId, "zoneId");
Objects.requireNonNull(aliasMap, "aliasMap");
String id = aliasMap.get(zoneId);
id = (id != null ? id : zoneId);
return of(id);
}
public static ZoneId of(String zoneId) {
return of(zoneId, true);
}
我们可以在ZoneId类中查看到其所支持的全世界的时区编码。
而ZnodeDateTime本身与LocalDateTime区别就在于加上了ZnodeID。LocalDateTime则采用本地的时区。ZnodeDateTime则可以根据我们需要的时区进行转换。
/**
* The local date-time.
*/
private final LocalDateTime dateTime;
/**
* The offset from UTC/Greenwich.
*/
private final ZoneOffset offset;
/**
* The time-zone.
*/
private final ZoneId zone;
理解了前面的LocalDateTime,就能很好理解ZnodeDateTime的使用。同时除之前LocalDateTime的一些工具方法之外,还提供若干与时区有关的方法。
需要注意的是,在新版本API中的日期,都是final修饰的内部属性,是不可变类。而Date则是transient的可变类。
4.日期格式化神器DateTimeFormatter
前文介绍了SampleDateFormat等传统的时间格式化工具存在线程安全问题。而且格式化字符串会导致宽松匹配等问题。那么在新版本的DateTimeFormatter中,则很好的解决了这些问题:
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
System.out.println(formatter.format(LocalDateTime.now()));
String str = "2019-12-07 07:43:53";
System.out.println(LocalDateTime.parse(str,formatter));
输出结果:
2020-08-06 19:08:55
2019-12-07T07:43:53
在所有的LocalDateTime、LocalDate、LocalTime都有parse方法,这是一个很好的设计模式,值得借鉴。这样把转换的结果对象都放在了所需对象的静态方法中。
上述模式字符串非常严格,有严格的校验规则。如我们不小心将HH写成了hh则会出错:
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss");
System.out.println(formatter.format(LocalDateTime.now()));
String str = "2019-12-07 13:43:53";
System.out.println(LocalDateTime.parse(str,formatter));
输出:
2020-08-06 07:31:12
Exception in thread "main" java.time.format.DateTimeParseException: Text '2019-12-07 13:43:53' could not be parsed: Invalid value for ClockHourOfAmPm (valid values 1 - 12): 13
at java.time.format.DateTimeFormatter.createError(DateTimeFormatter.java:1920)
at java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1855)
at java.time.LocalDateTime.parse(LocalDateTime.java:492)
at com.dhb.date.test.DateTimeFormatterTest.main(DateTimeFormatterTest.java:12)
Caused by: java.time.DateTimeException: Invalid value for ClockHourOfAmPm (valid values 1 - 12): 13
at java.time.temporal.ValueRange.checkValidValue(ValueRange.java:311)
at java.time.temporal.ChronoField.checkValidValue(ChronoField.java:703)
at java.time.format.Parsed.resolveTimeFields(Parsed.java:382)
at java.time.format.Parsed.resolveFields(Parsed.java:258)
at java.time.format.Parsed.resolve(Parsed.java:244)
at java.time.format.DateTimeParseContext.toResolved(DateTimeParseContext.java:331)
at java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:1955)
at java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1851)
... 2 more
可以看到,实际上要求非常严格。会提示hh的范围是1-12。这样就解决了之前提到的由于模式字符串匹配宽松导致的隐形BUG。
另外对于线程安全问题,可以看看DateTimeFormatter开篇注释。
* @implSpec
* This class is immutable and thread-safe.
*
* @since 1.8
*/
public final class DateTimeFormatter {
因为新版本的API都是不可变类,immutable的。这样一来所有的实例都只能赋值一次。之后如果需要用DateTimeFormatter进行转换,实际上是产生了一个新的实例,用这个新的实例输出。用一个不可变的设计模式,永远都不会有线程安全问题。
我们可以回顾在之前旧版本的时候,每个SampleTimeFormat都会持有一个Calendar实例,每次进行格式转换的时候都要对这个实例不断的clear之后重新赋值。
immutable也是一个非常棒的设计模式。
5.时差工具 Period和Duration
新版本的API对于两个时间的差值,专门设计了两个类来实现。Period用于处理两个日期之间的差值。Duration用于处理两个时间之间的差值。
5.1 Period
Period主要处理两个LocalDate之间的差值:
LocalDate now = LocalDate.now();
LocalDate date = LocalDate.of(2019,11,12);
Period period = Period.between(date,now);
System.out.println(period.getYears() + "年" +
period.getMonths() + "月" +
period.getDays() + "天");
其结果为:
0年8月25天
主要是用第一个值减去第二个值之间的差异,注意,这个years、months、days得放到一起看才有意义。
5.2 Duration
Duration主要处理两个LocalTime之间的差值:
LocalTime time = LocalTime.of(20,30);
LocalTime time1 = LocalTime.of(23,59);
Duration duration = Duration.between(time,time1);
System.out.println(duration.toMinutes() + "分钟");
System.out.println(duration.toString());
输出结果:
209分钟
PT3H29M
可以看到,这里输出的是总分钟。toString可以看到是3小时29分钟。实际上,我们可以通过方法的命名规则很好的理解,get方法和to方法。get方法是得到实际的单位差值。而to则是将全部的单位差值都转换为这个单位。这在实际操作的过程中需要注意,避免因为理解误差而导致出错。
这一块方法的命名规则也是我们在实际过程中值得参考的。
6.新旧日期格式转换
在java8的Date中增加了和Instant转换的方法。分别是from和toInstant来实现Instant和Date转换。
Instant instant = Instant.now();
System.out.println(Date.from(instant));
Date date = new Date();
System.out.println(date.toInstant());
输出结果:
Thu Aug 06 20:14:49 CST 2020
2020-08-06T12:14:49.935Z
可以看到Date和Instant是非常方便转换的。
Instant也可以在LocalDateTime之间转换。不过需要指定时区。之后LocalDateTime可以转换为LocalDate和LocalTime,我们用系统默认时区示例如下:
System.out.println(LocalDateTime.ofInstant(Instant.now(), ZoneId.systemDefault()));
LocalDateTime localDateTime = LocalDateTime.now();
System.out.println(localDateTime.toLocalDate());
System.out.println(localDateTime.toLocalTime());
输出:
2020-08-06T20:19:47.322
2020-08-06
20:19:47.322
上述就是本文对java8中新版本API的一些介绍。并没设计太深入的源码。作为自我学习的一个过程,后续将值得借鉴的地方进行总结。
网友评论