开发者

Java新旧时间日期API的使用和避坑指南

开发者 https://www.devze.com 2025-05-26 10:49 出处:网络 作者: litbluehandy
目录一、新的时间和日期API 1.1 获取当前时间1.2 构造一个指定年月日的时间1.3 修改日期1.4 格式化日期1.5 计算时间差1.6 时间反解析1.7 Instant类二、线程安全性问题三、数据库中时间存储3.1 区别3.2 使用建议四、&
目录
  • 一、新的时间和日期API
    • 1.1 获取当前时间
    • 1.2 构造一个指定年月日的时间
    • 1.3 修改日期
    • 1.4 格式化日期
    • 1.5 计算时间差
    • 1.6 时间反解析
    • 1.7 Instant类
  • 二、线程安全性问题
    • 三、数据库中时间存储
      • 3.1 区别
      • 3.2 使用建议
    • 四、“老三样”的坑
      • 4.1 初始化日期时间
      • 4.2 时区问题
      • 4.3 日期时间格式化和解析
      • 4.4 线程安全问题
    • 五、总结

      在 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 = Loc编程alDate.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" php,
                      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 对象,需要提供时区或时区偏移信息。

      二、线程安全性问题

      放两张图就一目了然:

      Java新旧时间日期API的使用和避坑指南

      Java新旧时间日期API的使用和避坑指南

      三、数据库中时间存储

      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

      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";
      Simpl编程客栈eDateFormat 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 0tSNpVu
      //        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 线程安全问题

      Java新旧时间日期API的使用和避坑指南

      我们写一个案例:

      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);

      Java新旧时间日期API的使用和避坑指南

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

      五、总结

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

      Java新旧时间日期API的使用和避坑指南

      以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程客栈(www.devze.com)。

      0

      精彩评论

      暂无评论...
      验证码 换一张
      取 消

      关注公众号