目录
- 一、新的时间和日期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引入了三个新的日期时间类,分别是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 = 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 */
注意:
- 这里的
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"; 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 线程安全问题
我们写一个案例:
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);
运行程序后大量报错,且没有报错的输出结果也不正常。
五、总结
老三样还是不要用了,新的日期时间类不香么?
以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程客栈(www.devze.com)。
精彩评论