16 | 用好Java 8的日期时间类,少踩一些“老三样”的坑
16 | 用好Java 8的日期时间类,少踩一些“老三样”的坑
讲述:王少泽
时长21:28大小19.66M
初始化日期时间
“恼人”的时区问题
日期时间格式化和解析
日期时间的计算
重点回顾
思考与讨论
赞 21
提建议
精选留言(28)
- Darren2020-04-16试着回到下问题: 第一个: Date的toString()方法处理的,同String中有BaseCalendar.Date date = normalize(); 而normalize中进行这样处理cdate = (BaseCalendar.Date) cal.getCalendarDate(fastTime,TimeZone.getDefaultRef(); 因此其实是获取当前的默认时区的。 第二个: 从下面几个维度进行区分: 占用空间:datetime:8字节。timestamp 4字节 表示范围:datetime '1000-01-01 00:00:00.000000' to '9999-12-31 23:59:59.999999' timestamp '1970-01-01 00:00:01.000000' to '2038-01-19 03:14:07.999999' 时区:timestamp 只占 4 个字节,而且是以utc的格式储存, 它会自动检索当前时区并进行转换。 datetime以 8 个字节储存,不会进行时区的检索. 也就是说,对于timestamp来说,如果储存时的时区和检索时的时区不一样,那么拿出来的数据也不一样。对于datetime来说,存什么拿到的就是什么。 更新:timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP; 这个特性是自动初始化和自动更新(Automatic Initialization and Updating)。 自动更新指的是如果修改了其它字段,则该字段的值将自动更新为当前系统时间。 它与“explicit_defaults_for_timestamp”参数有关。 By default, the first TIMESTAMP column has both DEFAULT CURRENT_TIMESTAMP and ON UPDATE CURRENT_TIMESTAMP if neither is specified explicitly。 很多时候,这并不是我们想要的,如何禁用呢? 1. 将“explicit_defaults_for_timestamp”的值设置为ON。 2. “explicit_defaults_for_timestamp”的值依旧是OFF,也有两种方法可以禁用 1> 用DEFAULT子句该该列指定一个默认值 2> 为该列指定NULL属性。 在MySQL 5.6.5版本之前,Automatic Initialization and Updating只适用于TIMESTAMP,而且一张表中,最多允许一个TIMESTAMP字段采用该特性。从MySQL 5.6.5开始,Automatic Initialization and Updating同时适用于TIMESTAMP和DATETIME,且不限制数量。展开
作者回复: 很全面
共 3 条评论49 - 👽2020-04-16对于时间,我个人的理解和目前的使用经验是——能用时间戳就用时间戳。 时间戳有几个优势: 1,便于比较和排序,无论数据库还是后台业务中都是如此。 2,也比较便于计算,虽然文中提到了Long的问题,但是,我认为L的问题的根本在于Long类型的理解,不是时间戳这个业务的问题。对Long的基础比较好了之后,也就足以应对计算中的问题了。 3,多端统一,现在提供给前端的很多服务都采用直接转换好年月日的字符串了,但是有时候,前端需要对时间进行比较的时候还是需要额外转化,会很麻烦。而且不利于格式化。时间戳的话就避免了这个问题,自己进行计算,自己格式化。前端自己随便玩。展开
作者回复: 保存和传输用时间戳的确比较方便,用带有时区的字面量时间表示字符串,还有格式不统一的问题
共 3 条评论18 - pedro2020-04-16第一个问题,虽然 Date 本质是一个时间戳没有时区的概念,但是在 toString 的时候为了可读性会推测当前时区,如果得不到就会使用 GMT。
作者回复: 是
13 - 👽2020-04-16思考题1: 根本原因在于toString的源代码: sb.append(zi.getDisplayName(date.isDaylightTime(), TimeZone.SHORT, Locale.US)); // zzz 这一行。 Date的toString实际上,是新建一个StringBuilder,然后根据Date对象里的年月日周,将其格式化。 格式化过程中,似乎会获取系统默认的时区,如果取不到系统默认时区,就使用GMT。 为了测试我的猜想,我尝试更改本机时区,结果输出: Wed Apr 15 21:40:10 EDT 2020 相比较正常自动时区 Thu Apr 16 09:41:31 CST 2020 基本与我的猜测一致。展开13
- eazonshaw2020-04-16问题二: 首先,为了让docker容器的时间格式和宿主机一致,可以在environment中添加TZ: Asia/Shanghai。 实验发现,切换mysql的TIME_ZONE到“america/new_york”后,发现datetime格式字段不发生变化,而timestamp格式会换算成纽约时区时间,所以timestamp格式的日期保存了时区信息,而datetime没有。 感觉在业务场景中,有可能出现服务器或容器系统时间并未设置时区,导致保存的数据并不是我们想要的。因此,是不是更推荐使用timestamp格式来保存日期,避免这种情况发生呢?展开
作者回复: TIMESTAMP保存的时候根据当前时区转换为UTC,查询的时候再根据当前时区从UTC转回来,而DATETIME就是一个死的字符串时间(仅仅对MySQL本身而言)表示。有关mysql时间类型可以详细看一下这个ppt http://cdn.oreillystatic.com/en/assets/1/event/36/Time%20Zones%20and%20MySQL%20Presentation.pdf 如果你的项目有国际化需求,推荐使用时间戳,并且需要确保你的应用服务器和数据库服务器设置了正确的匹配当地时区的时区配置(其实,即便你的项目没有国际化需求,设置正确的需求,至少是应用服务器和数据库服务器设置一致的时区,也是需要的)
3 - 俊柱2020-04-16老师,映射表的bean,若数据库字段为 Timestamp,那 java 的字段应该设为 ZonedDateTime 最为合理吗? 因为我看网上很多人都是用 LocalDateTime 进行映射
作者回复: 大多数时候项目没有全球化需求映射到本地时区即可,可以使用LocalDateTime。 不过我们说datetime不包含时区,是固定的时间表示仅仅是指MySQL本身。使用timestamp,需要考虑Java进程的时区和MySQL连接的时区。而使用datetime类型,则只需要考虑Java进程的时区(因为MySQL datetime没有时区信息了,JDBC时间戳转换成MySQL datetime,会根据MySQL的serverTimezone做一次转换) 具体你可能需要自己多做一些实验来理解,几个层面 1、mysql本身对于datetime和timestamp的区别 2、java应用和mysql交互时的关系
4 - Monday2020-06-03今天踩坑private static simpledateformat,高并发下出现numberformatexception错误。单笔数据重放没有问题。初看到这个异常一脸懵,完全联系不到是simpledateformat的坑。 后面突然想起老师这篇文章。。。完全是坑二的重现。因为是jdk6所以选择了去掉static解决,每次都会新建一个对象
作者回复: 好吧,不过ThreadLocal可以用的
3 - lee2020-05-11老师好,上海时区和纽约时区下,格式化同一个时间串得到的当前时差有时候是12小时,有时候是13小时呢,把stringDate改成2020-05-02 22:00:00得到的相差12小时 String stringDate = "2020-05-02 22:00:00"; SimpleDateFormat inputFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); //同一Date Date date = inputFormat.parse(stringDate); //默认时区格式化输出: System.out.println(new SimpleDateFormat("[yyyy-MM-dd HH:mm:ss Z]").format(date)); //纽约时区格式化输出 TimeZone.setDefault(TimeZone.getTimeZone("America/New_York")); System.out.println(new SimpleDateFormat("[yyyy-MM-dd HH:mm:ss Z]").format(date));展开
作者回复: 夏令时的关系,有关夏令时你可以网上搜索一些资料 System.out.println(TimeZone.getDefault().inDaylightTime(date));
3 - 👽2020-04-16思考题2: 说实话,数据库相关知识是我的弱项。 查了一下,大概是TIMESTAMP包含了时区信息,而DATETIME不包含。另外有一个是,我印象中5.7之后的mysql版本,最多只能有一个TIMESTAMP的字段。这也算是个区别吧。
作者回复: https://dev.mysql.com/doc/refman/5.6/en/timestamp-initialization.html 你说的应该是『As of MySQL 5.6.5, TIMESTAMP and DATETIME columns can be automatically initializated and updated to the current date and time (that is, the current timestamp). Before 5.6.5, this is true only for TIMESTAMP, and for at most one TIMESTAMP column per table. The following notes first describe automatic initialization and updating for MySQL 5.6.5 and up, then the differences for versions preceding 5.6.5.』这个问题,其实并不是指只能有一个TIMESTAMP 列,而是只能有一个TIMESTAMP列使用CURRENT_TIMESTAMP来初始化或自动更新时间戳
3 - 俊柱2020-04-22老师,我有一个问题困扰已久,希望能够解答一下,目前我们对外输出的 API ,时间都是时间戳的形式, 内部系统的交互,时间也是时间戳的形式。 那我用 Instant 去映射数据库的 Timestamp/DateTime 字段,会不会更好? 否则的话,需要在多处都要注意 LocalDateTime 和 时间戳的相互转换 (比如 redis 的序列化反序列化,json 的序列化、反序列化)
作者回复: 当然可以使用Instant映射,参考https://i.stack.imgur.com/idLPT.png
2 - 大大大熊myeh2020-04-16我虽然现在用的是jdk1.8,但对于日期的操作一般还是习惯于用Date或long,以后可以尝试用LocalDateTime等类。 思考题1 Date#toString方法中,会将当前时间转化为BaseCalendar.Date类,这个类有一个Zone属性,在toString的时候会被追加到字符串中(默认是GMT) 思考题2 datetime占用8字节,不受时区影响,表示范围'1000-01-01 00:00:00' to '9999-12-31 23:59:59' timestamp占用4字节,受时区影响,表示范围'1970-01-01 00:00:01' to '2038-01-19 03:14:07',若插入null会自动转化为当前时间展开
作者回复: 👍🏻
2 - pedro2020-04-16第二个问题,timestamp 会把传入的时间转化为 UTC 即时间戳进行存储,而 datetime 也直接将传入的时间存储。
作者回复: 大方向对,可以再考虑一下我们如何选择
2 - VIC2020-08-21threadlocal simpledateformat,有完整例子吗
作者回复: SimpleDateFormat用ThreadLocal包一下,搜索一下解决SimpleDateFormat线程安全问题,网上文章一大堆
1 - 程序员小跃2020-07-22学习这个专栏的前几篇的时候,就发现需要有很强的Java 8功底,作为一名Javaer,我买了本《Java 实战》来更好的学习Java 8的新特性,也能跟上课程的节奏。1
- senekis2020-06-191. 时区问题的原因 Date和Instant都可以根据时间戳来创建,其中并不包含时区信息,因此并不会出现所谓的时间错误。 因此在保存数据时,都可以通过Long型来保存时间戳信息。 当需要实现日期格式化时,通过将时间戳转换为对应的时区的时间进行显示。 2. 测试Demo public class DateTest { @Test public void timeStampTest() { Date date = new Date(); Instant instant = date.toInstant(); assertEquals(date.getTime(), instant.toEpochMilli()); ZoneId shZoneId = ZoneId.of("Asia/Shanghai"); ZoneId cgZoneId = ZoneId.of("America/Chicago"); LocalDateTime shLocalDateTime = LocalDateTime.ofInstant(instant, shZoneId); LocalDateTime cgLocalDateTime = LocalDateTime.ofInstant(instant, cgZoneId); assertEquals(shLocalDateTime.getSecond(), cgLocalDateTime.getSecond()); assertEquals(Math.abs(shLocalDateTime.getHour() - cgLocalDateTime.getHour()), 13); ZonedDateTime shZonedDateTime = ZonedDateTime.of(shLocalDateTime, shZoneId); ZonedDateTime cgZonedDateTime = ZonedDateTime.of(cgLocalDateTime, cgZoneId); assertEquals(shLocalDateTime.getSecond(), cgZonedDateTime.getSecond()); DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); String shDateTimeStr = dateTimeFormatter.format(shZonedDateTime); String cgDateTimeStr = dateTimeFormatter.format(cgZonedDateTime); assertFalse(shDateTimeStr.equals(cgDateTimeStr)); } }展开1
- Michael2020-04-19private static final DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); ** * 获取本周一开始时间 * @return */ public static String getFirstTimeByWeek() { LocalDateTime currentTime = LocalDateTime.now(); LocalDateTime weekStartTime = currentTime.with(TemporalAdjusters.previous(DayOfWeek.SUNDAY)).plusDays(1).truncatedTo(ChronoUnit.DAYS); //求出这个日期所在周的星期一 return weekStartTime.format(df); } /** * 获取本周一截止时间 * @return */ public static String getLastTimeByWeek() { LocalDateTime currentTime = LocalDateTime.now(); LocalDateTime weekEndTime = currentTime.with(TemporalAdjusters.next(DayOfWeek.MONDAY)).minusDays(1); //求出这个日期所在周的星期一 return weekEndTime.format(df); } /** * 获取当月第一天其实时间 * @return */ public static String getFirstTimeByMonth() { LocalDateTime currentTime = LocalDateTime.now(); LocalDateTime monthStartDate = currentTime.withDayOfMonth(1).truncatedTo(ChronoUnit.DAYS); return monthStartDate.format(df); } /** * 获取当月最后一天截止时间 * @return */ public static String getLastTimeByMonth() { LocalDateTime currentTime = LocalDateTime.now(); LocalDateTime monthEndDate = currentTime.plusMonths(1L).withDayOfMonth(1).truncatedTo(ChronoUnit.DAYS).plus(-1L, ChronoUnit.MILLIS); return monthEndDate.format(df); } 获取本周日截止时间的时候总是获取不到时分秒的那部分:23:59:59展开
作者回复: 不太明白啥意思,代码中没有贴出获取本周日截止时间的方法,获取到下周一-1s即可
共 3 条评论1 - Wiggle Wiggle2020-04-18我在外企,公司大部分数据用的洛杉矶时间(带了夏令时),处理数据的时候要面对各种不带时区的string或datetime,无比酸爽
作者回复: 处理数据的时候要面对各种不带时区的string或datetime。。。这显然是会有问题的
1 - Geek_0b535f2022-12-08 来自上海请教一下,原子钟校准会不会导致两个时间戳对应一个时间
- 奔跑2022-10-19 来自北京映射数据库时,Date用什么替换呢,LocalDateTime还是Instant? JDK8的LocalDate和joda.time的LocalDate哪个更适合用在项目里?
- 车鸿韡2022-01-30MySQL时间戳有范围限制,那么date时间戳有范围限制吗?