专栏首页做不甩锅的后端Java中的时间和日期(三):java8中新的时间API介绍

Java中的时间和日期(三):java8中新的时间API介绍

由于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的一些介绍。并没设计太深入的源码。作为自我学习的一个过程,后续将值得借鉴的地方进行总结。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 对java中的泛型的理解

    在Thinking in java 第五版的第二十章中,开篇说到,在普通的类和方法中只能用特定的类型:基本数据类型和类类型。如果在编写代码的过程中需要用到多种类...

    冬天里的懒猫
  • 多线程基础(十七):Condition及ConditionObjet源码分析

    在java中,为了配合ReentrantLock等Lock的实现类实现锁的多条件等待,为此java设计了Condition接口。在AQS中的主要结构如下:

    冬天里的懒猫
  • 多线程基础(一): 线程概念及生命周期

    什么是进程,相信大家都知道什么是进程却很难解释清楚。百科中的解释是:进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调...

    冬天里的懒猫
  • Jdk14 都要出了,Jdk8 的时间处理姿势还不了解一下?

    当前时间:2019年10月24日。距离 JDK 14 发布时间(2020年3月17日)还有多少天?

    未读代码
  • Jdk14 都要出了,Jdk8 的时间处理姿势还不了解一下?

    JDK 8 已经在 2014年 3月 18日正式可用 ,距离现在已经 5年多时间过去了。5年时间里很多企业也都换上了 JDK 8,明年 3月份 Jdk14 也要...

    Java_老男孩
  • jdk8获取当前时间|时间加减|java8时间格式化|时间处理工具|时间比较|线程安全的时间处理方法

    在很久之前,我总结了一些jdk7版本之前的关于时间处理的一些公共方法,日期转换成字符串、指定时间加上指定天数后的日期、获取上周周一时间 等等;具体的可以戳链接查...

    小小鱼儿小小林
  • JDK8 LocalDateTime转换成时间戳

    以上就是将LocalDateTime转换成时间戳的方式,方法很简单,参考:https://coderanch.com/t/651936/databases/Co...

    我是李超人
  • 获取MP3信息

      很多时候,我们有必要的到一些歌曲的信息,比如歌手的专辑,歌手名 歌曲名,下面就是java写的获取MP3歌曲信息 首先加入 jid3lib-0.5.4.jar...

    py3study
  • 知名中文信息处理专家董振东教授去世:他是知网发明者,中国机器翻译先驱

    董振东教授自70年代即开始从事机器翻译研究工作。1987年成功开发了我国第一个商品化机器翻译系统原型《科译1号》。

    量子位
  • Java Collection知识总结

    首先说说java中常用的集合容器:ArrayList,LinkedList,Vector,HashMap,Hashtable,HashSet,TreeSet。【...

    赵小忠

扫码关注云+社区

领取腾讯云代金券