在 java 8 之前,我们处理日期时间需求时,使用 date、calender 和 simpledateformat,来声明时间戳、使用日历处理日期和格式化解析日期时间。但是,这些类的 api 的缺点比较明显,比如可读性差、易用性差、使用起来冗余繁琐,还有线程安全问题。因此,java 8 推出了新的日期时间类。
每一个类功能明确清晰、类之间协作简单、api 定义清晰不踩坑,api 功能强大无需借助外部工具类即可完成操作,并且线程安全。
java 8引入了三个新的日期时间类,分别是localdate
、localtime
和localdatetime
,分别处理日期、时间和日期时间。
一、新的时间和日期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
的差,返回的是两个日期差几年零几月零几天。如果希望得知两个日期之间差几天,直接调用 period
的getdays()
. 方法得到的只是最后的“零几天”,而不是算总的间隔天数。
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 */
注意:
- 这里的
localdate
、localtime
和localdatetime
的使用要区别好,不然解析过程会出现错误。
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
不自动更新)。
四、“老三样”的坑
老三样指:date
、calender
和simpledateformat
。
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);
运行程序后大量报错,且没有报错的输出结果也不正常。
五、总结
老三样还是不要用了,新的日期时间类不香么?
以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。
发表评论