当前位置: 代码网 > it编程>编程语言>Java > Java新旧时间日期API的使用和避坑指南

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

2025年05月25日 Java 我要评论
在 java 8 之前,我们处理日期时间需求时,使用 date、calender 和 simpledateformat,来声明时间戳、使用日历处理日期和格式化解析日期时间。但是,这些类的 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

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

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

五、总结

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

以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com