关于Java新旧时间日期API的使用和避坑

目录

文章

在 Java 8 之前,我们处理日期时间需求时,使用 Date、Calender 和 SimpleDateFormat,来声明时间戳、使用日历处理日期和格式化解析日期时间。但是,这些类的 API 的缺点比较明显,比如可读性差、易用性差、使用起来冗余繁琐,还有线程安全问题。因此,Java 8 推出了新的日期时间类。每一个类功能明确清晰、类之间协作简单、API 定义清晰不踩坑,API 功能强大无需借助外部工具类即可完成操作,并且线程安全。Java 8引入了三个新的日期时间类,分别是LocalDateLocalTimeLocalDateTime,分别处理日期、时间和日期时间。

一、新的时间和日期API

1.1 获取当前时间
LocalDateTime localDateTime = LocalDateTime.now();

System.out.println("当前时刻:" + localDateTime );
System.out.println("当前年:" + localDateTime.getYear() +
                   "\n当前月:" + localDateTime.getMonth() +
                   "\n当前日:" + localDateTime.getDayOfMonth());

System.out.println("当前时/分/秒:" +
                   localDateTime.getHour() +" / " +
                   localDateTime.getMinute() + "/" +
                   localDateTime.getSecond());


/* 
 * 打印结果
 * 
 * 当前时刻:2020-09-04T22:11:27.505361600
 * 当前年:2020
 * 当前月:SEPTEMBER
 * 当前日:4
 * 当前时/分/秒:  22/13/48
 */
1.2 构造一个指定年月日的时间

比如构造:2019年8月30日18时26分30秒,大约是我对小方表白的时刻。

LocalDateTime specifiedTime = LocalDateTime.of(2019, Month.AUGUST, 30, 18, 26, 30);
System.out.println("构造时间:" + specifiedTime );

/**
 * 打印结果
 * 
 * 构造时间:2019-08-30T18:26:30
 */
1.3 修改日期
LocalDateTime updateTime = LocalDateTime.now();
// 增加1个月
updateTime.plusMonths(1);
// 减少2天
updateTime.minusDays(2);
// 直接修改到2028年
updateTime.withYear(2028);
// 直接修改到本月的第28天
updateTime.withDayOfMonth(28);
// 组合条件修改
updateTime.withDayOfMonth(12).withYear(2060).minusDays(1);
1.4 格式化日期
LocalDateTime formatTime = LocalDateTime.now();
String type1 = formatTime.format(DateTimeFormatter.BASIC_ISO_DATE);
String type2 = formatTime.format(DateTimeFormatter.ISO_DATE);
String type3 = formatTime.format(DateTimeFormatter.ofPattern("yyyy-/-MM-/-dd"));

System.out.println("formatTime1:" + type1 +
                   "\nformatTime2: " + type2 +
                   "\nformatTime3: " + type3);

/**
 * 输出:
 * formatTime1:20200904
 * formatTime2: 2020-09-04
 * formatTime3: 2020-/-09-/-04
 */
1.5 计算时间差

Java 8 中有一个专门的类 Period 定义了日期间隔,通过Period.between 得到了两个LocalDate 的差,返回的是两个日期差几年零几月零几天。如果希望得知两个日期之间差几天,直接调用 PeriodgetDays(). 方法得到的只是最后的“零几天”,而不是算总的间隔天数。

LocalDate today = LocalDate.of(2020, 9, 5);
LocalDate specifyDate = LocalDate.of(2019, 8, 30);
System.out.println(Period.between(specifyDate, today).getDays());
System.out.println(Period.between(specifyDate, today));
System.out.println(ChronoUnit.DAYS.between(specifyDate, today));

/**
 * 输出:
 * 6
 * P1Y6D
 * 372
 */
1.6 时间反解析
LocalDate inverseAnalysisTime = LocalDate.parse("2020-/-09-/-04" ,
                DateTimeFormatter.ofPattern("yyyy-/-MM-/-dd"));
System.out.println("反解析后时间为:" + inverseAnalysisTime);

/**
 * 输出:
 * 反解析后时间为:2020-09-04
 */

LocalDateTime inverseAnalysisTime = LocalDateTime.parse("2020-/-09-/-04 22:42" ,
                DateTimeFormatter.ofPattern("yyyy-/-MM-/-dd HH:mm"));
System.out.println("反解析后时间为:" + inverseAnalysisTime);
/**
 * 输出:
 * 反解析后时间为:2020-09-04T22:42
 */

注意:

  • 这里的LocalDateLocalTimeLocalDateTime的使用要区别好,不然解析过程会出现错误。
1.7 Instant类

Instant对象和时间戳是一一对应的,它是精确到纳秒的(而不是象旧版本的Date精确到毫秒)。

Instant instant = Instant.now();
System.out.println(instant);

// 输出, ISO-8601 标准
// 2020-09-04T15:13:50.152933300Z

Instant 类返回的值计算从 1970 年 1 月 1 日(1970-01-01T00:00:00Z)第一秒开始的时间, 也称为 EPOCH。 发生在时期之前的瞬间具有负值,并且发生在时期后的瞬间具有正值 (1970-01-01T00:00:00Z 中的 Z 其实就是偏移量为 0)。Instant 类提供的其他常量是 MIN, 表示最小可能(远远)的瞬间,MAX表示最大(远期)瞬间。

  • 该类还提供了多种方法操作 Instant。加和减的增加或减少时间的方法。以下代码将 1 小时添加到当前时间:
Instant oneHourLater = Instant.now().plusHours(1);
  • 比较时间的方法
long secondsFromEpoch = Instant.ofEpochSecond(0L).until(Instant.now(),ChronoUnit.SECONDS);
// 1599233977

LocalDateTime start = LocalDateTime.of(2020, 9, 4, 0, 0, 0);
LocalDateTime end = LocalDateTime.of(2020, 9, 8, 0, 0, 0);
// 两个时间之间相差了4天
System.out.println(start.until(end, ChronoUnit.DAYS));

// 4
  • Instant 不包含年,月,日等单位。但是可以转换成 LocalDateTime 或 ZonedDateTime, 如下 把一个 Instant + 默认时区转换成一个 LocalDateTime。
LocalDateTime ldt = LocalDateTime.ofInstant(Instant.now(), ZoneId.systemDefault());

System.out.printf("%s %d %d at %d:%d%n", ldt.getMonth(), ldt.getDayOfMonth(),
                                                        ldt.getYear(), ldt.getHour(), ldt.getMinute());
// SEPTEMBER 4 2020 at 23:40

无论是 ZonedDateTime 或 OffsetTimeZone 对象可被转换为 Instant 对象,因为都映射到时间轴上的确切时刻。 但是,相反情况并非如此。要将 Instant 对象转换为 ZonedDateTime 或 OffsetDateTime 对象,需要提供时区或时区偏移信息。

二、线程安全性问题

放两张图就一目了然:

三、数据库中时间存储

3.1 区别
  • int

    • 占用4个字节
    • 建立索引之后,查询速度快
    • 条件范围搜索可以使用使用between
    • 不能使用mysql提供的时间函数
  • datetime

    • 占用8个字节,允许为空值,可以自定义值
    • 系统不会自动修改其值
    • 与时区无关,存什么拿到的就是什么。
    • 可以在指定datetime字段的值的时候使用now()变量来自动插入系统的当前时间。
  • timestamp

    • 类型在默认情况下,insert、update 数据时,timestamp列会自动以当前时间(CURRENT_TIMESTAMP)填充/更新。
    • 受时区timezone的影响以及MYSQL版本和服务器的SQL MODE的影响 ,存储时对当前的时区进行转换,检索时再转换回当前的时区。
3.2 使用建议
  • int适合需要进行大量时间范围查询的数据表
  • datetime适合用来记录数据的原始的创建时间,因为无论你怎么更改记录中其他字段的值,datetime字段的值都不会改变,除非你手动更改它。
  • timestamp适合用来记录数据的最后修改时间,因为只要你更改了记录中其他字段的值,timestamp字段的值都会被自动更新。(如果需要可以设置timestamp不自动更新)。

四、“老三样”的坑

老三样指:DateCalenderSimpleDateFormat。这部分整理自: Java业务开发常见错误100例

4.1 初始化日期时间

如果要初始化一个 2020 年 9 月 5 日 11 点 12 分 13 秒这样的时间:

 Date date = new Date(2020, 9, 5, 11, 12, 13);

// 输出:
// Tue Oct 05 11:12:13 CST 3920

这里就要注意:年应该是和 1900 的差值,月应该是从 0 到 11 而不是从 1 到 12。

我们也可以直接使用Calander

Calendar calendar = Calendar.getInstance();
// 月份依旧是 0-11
calendar.set(2020,8,5,11,16,25);

System.out.println(calendar.getTime());

// 输出:
// Sat Sep 05 11:16:25 CST 2020
4.2 时区问题

关于 Date 类,我们要有两点认识:

  • Date 并无时区问题,世界上任何一台计算机使用 new Date() 初始化得到的时间都一样。因为,Date 中保存的是 UTC 时间,UTC 是以原子钟为基础的统一时间,不以太阳参照计时,并无时区划分。
  • Date 中保存的是一个时间戳,代表的是从 1970 年 1 月 1 日 0 点(Epoch 时间)到现在的毫秒数。尝试输出 Date(0):
System.out.println(new Date(0));
System.out.println(TimeZone.getDefault().getID() + ":" + TimeZone.getDefault().getRawOffset()/3600000);

// 输出:
// Thu Jan 01 08:00:00 CST 1970
// 因为我机器当前的时区是中国上海,相比 UTC 时差 +8 小时。

对于国际化的项目,处理好时间和时区问题首先就是要正确保存日期时间。这里有两种保存方式:

  • 方式一,以 UTC 保存,保存的时间没有时区属性,是不涉及时区时间差问题的世界统一时间。我们通常说的时间戳,或 Java 中的 Date 类就是用的这种方式,这也是推荐的方式。
  • 方式二,以字面量保存,比如年 / 月 / 日 时: 分: 秒,一定要同时保存时区信息。只有有了时区信息,我们才能知道这个字面量时间真正的时间点,否则它只是一个给人看的时间表示,只在当前时区有意义。Calendar 是有时区概念的,所以我们通过不同的时区初始化 Calendar,得到了不同的时间。正确保存日期时间之后,就是正确展示,即我们要使用正确的时区,把时间点展示为符合当前时区的时间表示。
4.3 日期时间格式化和解析

每到年底,就有很多踩时间格式化的坑,比如“这明明是一个 2019 年的日期,怎么使用 SimpleDateFormat 格式化后就提前跨年了”。我们来重现一个这个问题。初始化一个 Calendar,设置日期时间为 2019 年 12 月 29 日,使用大写的 YYYY 来初始化 SimpleDateFormat:

Locale.setDefault(Locale.SIMPLIFIED_CHINESE);
System.out.println("defaultLocale:" + Locale.getDefault());
Calendar calendar = Calendar.getInstance();
calendar.set(2019, Calendar.DECEMBER, 29,0,0,0);
SimpleDateFormat YYYY = new SimpleDateFormat("YYYY-MM-dd");
System.out.println("格式化: " + YYYY.format(calendar.getTime()));
System.out.println("weekYear:" + calendar.getWeekYear());
System.out.println("firstDayOfWeek:" + calendar.getFirstDayOfWeek());
System.out.println("minimalDaysInFirstWeek:" + calendar.getMinimalDaysInFirstWeek());

/**
 * 输出:
 *  
 * defaultLocale:zh_CN
 * 格式化: 2020-12-29
 * weekYear:2020
 * firstDayOfWeek:1
 * minimalDaysInFirstWeek:1
 */

更改时区试试:

Locale.setDefault(Locale.FRANCE);

//        格式化: 2019-12-29
//        weekYear:2019
//        firstDayOfWeek:2
//        minimalDaysInFirstWeek:4

那么 week year 就还是 2019 年,因为一周的第一天从周一开始算,2020 年的第一周是 2019 年 12 月 30 日周一开始,29 日还是属于去年。JDK 的文档中有说明:小写 y 是年,而大写 Y 是 week year,也就是所在的周属于哪一年,所以没有特殊需求,针对年份的日期格式化,应该一律使用 “y” 而非 “Y”。

另一个是:当需要解析的字符串和格式不匹配的时候,SimpleDateFormat 表现得很宽容,还是能得到结果

String dateString = "20200905";
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMM");
try {
    System.out.println("result:" + dateFormat.parse(dateString));
} catch (ParseException e) {
    e.printStackTrace();
}

// 输出:
// result:Sun May 01 00:00:00 CST 2095

这里把0905当初月份,往后推迟了905个月,但是并没有爆出任何警告或错误。

我们可以用Java8中的DateTimeFormatter代替:

String dateString = "20200905";
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMM");
System.out.println("result:" + dateTimeFormatter.parse(dateString));

// 控制台报错:
//        Exception in thread "main" java.time.format.DateTimeParseException:Text '20200905' could not be parsed at index 0
//        at java.base/java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:2046)
//        at java.base/java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1874)
//        at cn.litblue.datedemo.DateDemo.main(DateDemo.java:56)
4.4 线程安全问题

我们写一个案例:

SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

ExecutorService threadPool = Executors.newFixedThreadPool(100);
for (int i = 0; i < 20; i++) {
    //提交20个并发解析时间的任务到线程池,模拟并发环境
    threadPool.execute(() -> {
        for (int j = 0; j < 10; j++) {
            try {
                System.out.println(simpleDateFormat.parse("2020-09-05 12:10:30"));
            } catch (ParseException e) {
                e.printStackTrace();
            }
        }
    });
}
threadPool.shutdown();
threadPool.awaitTermination(1, TimeUnit.HOURS);

运行程序后大量报错,且没有报错的输出结果也不正常。

五、总结

老三样还是不要用了,新的日期时间类不香么?

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值