兄弟们,聊个每家项目里都逃不掉的东西——DateUtil 日期工具类。这篇文章不整虚的,就说清楚一件事:一个能撑住生产环境的DateUtil到底该怎么设计、怎么写、怎么测、怎么避坑。
做后端几年,我几乎在每个项目里都能看到至少一版“自制”DateUtil。有的三五行代码放那儿当摆设,有的功能臃肿得像个日期全家桶,但真正落到生产环境,遇到跨时区部署、并发调用、跨年统计的时候,能稳稳扛住的没几个。这篇就把我在某项目里从零封装DateUtil的全过程、踩过的坑、查阅过的资料全部摊开讲,代码直接可抄,思路比代码更重要。
1. 为什么每个项目都离不开DateUtil:先看清问题的本质
1.1 你真的只是封装一个日期格式吗
刚工作那会儿我以为DateUtil就是处理“yyyy-MM-dd”转字符串的。后来某跨平台系统对接了三个不同业务方的数据,才发现日期处理根本是脏活累活的大集合:上游某个接口传来的是时间戳字符串,某张Excel导入出来的是“2024/01/15 14:30:22”这种自定义格式,还有一批老系统用的是两位年份“24/01/15”,再碰上客户要求的报表要按自然周汇总、按又一个财年季度分组,所有逻辑都东一处西一处散落在业务代码里。
这种散乱直接导致两个恶果。其一,同样的转换逻辑在十几个类里各写一遍,改格式规则要全局搜索替换,漏一处就出一次线上bug。其二,每个开发对日期处理的方式都不一样,有人用SimpleDateFormat裸奔,有人直接拼字符串,有人用Date的getMonth(),乱七八糟,代码评审都懒得看。
所以DateUtil存在的意义,不是让你“少写几行代码”,而是把项目中所有日期操作收口到一条受控的巷子里。所有的格式、时区、解析逻辑只能从这一个类进出,规范统一,出问题时也只有一个类需要排查。
1.2 从某次线上事故说起:不规范的日期处理有多贵
说个我印象很深的例子。某个统计服务的季度报表在该季度最后一天晚上多跑了一小时,跑出来的数据和人工核对差了整整一天。查了一宿,最后定位到核心原因:某平台统计时把“当前季度的最后一天”算成了“最后一天的当前时刻”,LocalDate和LocalDateTime混用,时间维度多了23小时59分59秒。
这个bug的根不是“程序员不懂日期”,而是没有任何统一的日期工具约束,每个开发都在随手new对象。如果从第一天就强制用DateUtil的endOfQuarter()、startOfQuarter()这类语义明确的方法,这种低级事故根本不会发生。
从那之后我形成了一个原则:项目里宁可多封装几个冷门方法,也不让业务代码直接碰底层时间API。这也是下面这套DateUtil设计哲学的开端。
2. DateUtil的设计思路与API规划
2.1 工具类的职责边界:该做什么、不该做什么
设计DateUtil最容易犯的毛病是“什么都往里塞”。我见过有人把dateToString、stringToDate、addDays、getMonth这种基础方法放一个类里,又把星座判断、生肖判断、干支纪年也塞进来,最后这个类几千行,改谁都怕。
我的做法是职责收敛。DateUtil只做“日期时间处理”,具体拆成五大类:
- 类型转换:
Date、LocalDate、LocalDateTime、Instant、时间戳之间互相转换 - 格式化解析:统一格式常量的定义,线程安全的格式化与解析
- 日期计算:加减天/月/年,季度初末、月初月末、上/下个工作日判定
- 区间判断:判断某天是否在区间内、是否闰年、是否同一天、跨年跨月处理
- 默认值兜底:解析失败时的安全回退,避免到处try-catch
凡是这五类之外的,比如时区名列表展示、多语言星期显示,我都建议单独建类,不要污染核心工具。工具类最怕的是职责蔓延,保持单一职责,后面维护的人才会感激你。
从Java 8之后,我强烈建议所有DateUtil内部一律基于java.time包来实现。java.util.Date和SimpleDateFormat遗留问题太多,线程安全、可变性、设计缺陷都是坑,新代码没有理由再用它们。团队用Java 8以下版本的情况另外说,那种环境建议直接用现成的第三方库,别自己造轮子。
2.2 统一格式常量:不把魔法字符串散落各处
一个很容易被忽略但极其重要的设计:格式化模式必须集中定义成常量。很多项目的日期bug,都起源于有人随手写了个"YYYY-MM-dd hh:mm:ss",这个模式里有两个致命错误——大写的YYYY代表“周年份”,跟实际年份只差一个跨年周就翻车;小写的hh代表12小时制,晚上8点会被格式化成“08点”。后面章节我会专门展示这个坑。
我项目里固定的常量大致长这样:
public static final String PATTERN_DATE = "yyyy-MM-dd"; public static final String PATTERN_DATETIME = "yyyy-MM-dd HH:mm:ss"; public static final String PATTERN_DATETIME_MS = "yyyy-MM-dd HH:mm:ss.SSS"; public static final String PATTERN_DATE_COMPACT = "yyyyMMdd"; public static final String PATTERN_DATETIME_COMPACT = "yyyyMMddHHmmss"; public static final String PATTERN_DATE_SLASH = "yyyy/MM/dd";这些常量放在DateUtil内部作为public static final,业务层只管引用。注意这里全是小写yyyy、大写MM、小写dd、24小时制HH,一个字符都不能错。
关于格式模式,再分享一个小经验。时间戳转换时,我建议所有对外接口统一使用标准ISO格式。DateTimeFormatter.ISO_LOCAL_DATE_TIME这类内置格式可以优先用,它处理的可读性比自定义模式好,而且能天然规避模式字符串拼写错误。
2.3 API命名:一眼看懂是基本素养
DateUtil的方法命名我遵循一个直觉第一的规则:看到方法名,不用查文档就知道返回什么。参考惯例,format系列返回字符串,parse系列返回时间对象,getXxx返回具体属性,isXxx返回布尔判断,addXxx表示在原对象上增加然后返回新对象。
项目里最终稳定下来的方法签名叫这些:
public static String format(LocalDateTime dateTime, String pattern) public static LocalDateTime parse(String dateTimeStr, String pattern) public static LocalDateTime parseOrNow(String dateTimeStr, String pattern) // 兜底 public static String formatTimestamp(long timestamp, String pattern) public static long toTimestamp(LocalDateTime dateTime) public static LocalDate toLocalDate(Object dateObj) // 兼容各种类型 public static LocalDateTime startOfDay(LocalDate date) public static LocalDateTime endOfDay(LocalDate date) public static LocalDateTime startOfMonth(LocalDate date) public static LocalDateTime endOfMonth(LocalDate date) public static LocalDateTime startOfQuarter(LocalDate date) public static boolean isWeekend(LocalDate date) public static long daysBetween(LocalDate start, LocalDate end) public static int ageOf(LocalDate birthDate)强调一下,方法不要重载得眼花缭乱。同一个语义,能用两个参数解决的不要搞五个参数,把可读性放在第一位。
3. 核心方法实现与细节讲解
3.1 线程安全的格式化与解析:所有并发问题的根源
远古时期大家用SimpleDateFormat有一个著名的坑:它不是线程安全的,多个线程共用同一个SimpleDateFormat实例解析日期,会偶发解析出完全错误的时间,甚至直接抛NumberFormatException。等并发量上来,这种bug呈现“偶发性”,极难排查。
既然底层基于java.time,DateTimeFormatter本身就是不可变且线程安全的,我们可以放心地把格式化器定义成static final。不过还是那句话:如果需要自定义格式,最佳实践是每次调用都创建新的DateTimeFormatter,或者缓存不涉及可变状态的实例。
看我的核心实现:
public static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern(PATTERN_DATE); public static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern(PATTERN_DATETIME); public static String format(LocalDateTime dateTime, String pattern) { if (dateTime == null) { return ""; } return dateTime.format(DateTimeFormatter.ofPattern(pattern)); } public static LocalDateTime parse(String dateTimeStr, String pattern) throws DateTimeParseException { final String trimmed = dateTimeStr == null ? "" : dateTimeStr.trim(); if (trimmed.isEmpty()) { throw new DateTimeParseException("Empty date string", "", 0); } return LocalDateTime.parse(trimmed, DateTimeFormatter.ofPattern(pattern)); }上面这段代码里有两个容易被新手忽略的细节。第一是null返回值,我统一返回空字符串而不是返回null,这样业务侧拿到值可以直接拼接或者入库,避免一堆空指针判断。第二是trim(),上游传过来的时间字符串经常带空格,不处理干净后面解析必然出幺蛾子。
解析方法抛DateTimeParseException很多人觉得麻烦,但我故意不吞异常。工具类不负责业务异常语义,吞掉了业务侧就没法感知“数据到底错在哪里”。如果业务希望容错,我单独提供兜底版本:
public static LocalDateTime parseOrDefault( String dateTimeStr, String pattern, LocalDateTime defaultValue) { try { return parse(dateTimeStr, pattern); } catch (DateTimeParseException ex) { return defaultValue; } }这是我来回调整几次之后才定下来的优雅结构:核心方法保持严格,扩展方法提供宽恕。
3.2 日期加减与区间计算:别小看月末和闰年
LocalDateTime本身自带plusDays()、minusMonths()这类方法,DateUtil封装它们的意义主要在“收口语义”。企业应用中最常见的三个业务场景是:月底结算、季度统计、跨周期判断。
先看月初和月末的实现。用LocalDate做,干净利落:
public static LocalDate firstDayOfMonth(LocalDate date) { return date.withDayOfMonth(1); } public static LocalDate lastDayOfMonth(LocalDate date) { return date.withDayOfMonth(date.lengthOfMonth()); }注意lengthOfMonth()返回的是该月实际天数,2月会自动区分28还是29。用with(TemporalAdjusters.lastDayOfMonth())也行,但显式写lengthOfMonth()逻辑更直白,新人看代码也容易理解。
季度初末是业务报表的高频需求。季度结束日那三行代码比较绕,我给了很详细的注释:
public static LocalDate firstDayOfQuarter(LocalDate date) { int quarterMonth = ((date.getMonthValue() - 1) / 3) * 3 + 1; return LocalDate.of(date.getYear(), quarterMonth, 1); } public static LocalDate lastDayOfQuarter(LocalDate date) { LocalDate firstDay = firstDayOfQuarter(date); // 下季度的第一天,再减一天就是当前季度的最后一天 return firstDay.plusMonths(3).minusDays(1); }这里核心的取季度首月公式:(月份-1)/3*3+1。比如8月,(8-1)/3=2,2*3+1=7,7月就是第三季度的开始月,这逻辑放到哪个月都对。
天数差计算则要注意“区间是开还是闭”。业务里“从10月1日到10月3日”到底算2天还是3天,每个团队都有自己的规矩,工具类必须在命名上说清楚:
public static long daysBetween(LocalDate start, LocalDate end) { return ChronoUnit.DAYS.between(start, end); // 结果是左闭右开,不含end当天 } public static long daysInclusive(LocalDate start, LocalDate end) { return ChronoUnit.DAYS.between(start, end) + 1L; // 头尾都算 }这个设计细节让我少挨了好几次骂。统计活跃天数、计算休假周期经常要“含头含尾”,用错了整个业务数字对不上。
3.3 工作日判断、年龄计算:那些“看着很简单”的方法
isWeekend()看起来容易,真正做的时候要注意时区上下文。默认情况用DayOfWeek判断周六周日就够了:
public static boolean isWeekend(LocalDate date) { DayOfWeek day = date.getDayOfWeek(); return day == DayOfWeek.SATURDAY || day == DayOfWeek.SUNDAY; }但很多业务要求“考虑法定节假日调休”,比如国庆假期之后的周六要上班。这不是DateUtil能解决的,因为节假日每年都由相关部门公布,没有固定算法。我的做法是:DateUtil只提供isWeekend,额外提供isHoliday(LocalDate, Set<LocalDate>)这样一个接口,让业务方自己注入节假日集合。节假日变化频繁,适合做成可配置数据,而不是固化在代码里。
年龄计算是另一个暗坑。如果直接用Period.between(birthday, today).getYears()来处理生日还没到的场景,结果天然正确,但很多人以前用Date算年龄时是怎么算的?直接减毫秒数除以年度毫秒数,这种算法在闰年面前完全不可靠。
public static int ageOf(LocalDate birthDate) { if (birthDate == null) { return 0; } LocalDate today = LocalDate.now(); int age = today.getYear() - birthDate.getYear(); // 当前日期还没到达今年的生日,则年龄减一 if (today.getDayOfYear() < birthDate.withYear(today.getYear()).getDayOfYear()) { age--; } return age; }注意这段代码里比较用的是getDayOfYear()而不是getMonthValue()加getDayOfMonth()组合判断,原因是前者一次调用就能搞定“月日序数”,逻辑更紧凑。不过这里有潜在边界:如果生日是2月29日,而今年不是闰年,birthDate.withYear(today.getYear())会直接报错。所以稳妥写法要换成MonthDay:
public static int ageOfSafe(LocalDate birthDate) { LocalDate today = LocalDate.now(); int age = today.getYear() - birthDate.getYear(); MonthDay birthMonthDay = MonthDay.from(birthDate); MonthDay todayMonthDay = MonthDay.from(today); if (todayMonthDay.isBefore(birthMonthDay)) { age--; } return age; }这个MonthDay方案彻底绕开了2月29日问题,目前是团队内部推荐版本。
3.4 时区问题:一个DateUtil藏得最深的雷
单机开发时很少意识到时区是个问题,一旦服务器跨地域部署,或者数据库连接串带serverTimezone参数,日期就悄悄歪掉。最典型的场景是:服务器在时区A,数据库在时区B,日志打印的是本地时刻,用户看到的时间跟数据库存储的差了8小时,谁看谁心态爆炸。
我的DateUtil里对时间戳转换定了一条铁律:凡是从外部传入时间戳或字符串,必须显式指定时区,默认不信任服务器本地时区。
public static final ZoneId DEFAULT_ZONE = ZoneId.of("Asia/Shanghai"); public static String formatTimestamp(long timestamp, String pattern, ZoneId zone) { ZoneId targetZone = zone != null ? zone : DEFAULT_ZONE; LocalDateTime dateTime = LocalDateTime.ofInstant( Instant.ofEpochMilli(timestamp), targetZone); return dateTime.format(DateTimeFormatter.ofPattern(pattern)); } public static long toTimestamp(LocalDateTime localDateTime, ZoneId zone) { ZoneId targetZone = zone != null ? zone : DEFAULT_ZONE; return localDateTime.atZone(targetZone).toInstant().toEpochMilli(); }这里用ofInstant而不是new Date(timestamp)是有意的,因为Instant更贴合“时间轴上的一瞬间”这个语义,配合ZoneId显式转成哪个时区的“墙上时间”完全由我控制。有一段时间我们还做过A/B验证,同一批时间戳用本地时区和用固定时区各算一遍,结果日志显示的数量级差异直接帮我们定位了一个延迟统计全错的问题。从那以后,团队的所有时间存储约定全部统一为UTC时间戳 + 展示侧区域转换。
这里再分享一个判断:什么时候该把时区参数暴露给调用方,什么时候直接隐藏?我的原则是“读写外部数据的方法暴露,内部计算的方法隐藏”。比如从第三方拉取时间戳时能拿到对方时区,就必须显式传;两个日期相减算差值,跟时区无关,就封装成根本不接收时区的参数,减少调用方心智负担。
4. 测试驱动:为DateUtil写一份靠谱的测试
4.1 边界条件测试设计:你的方法不是为“正常情况”写的
DateUtil这种基础工具,正常路径的测试写了等于没写,真正的价值全在边界。我测试时重点关注五类场景:
- 闰年的2月29日:解析、加减、年龄、月末都要过一遍
- 跨年与跨月:加减1天跨月,加减1个月跨年,季度初末跨年
- 时间边界:00:00:00、23:59:59.999、UTC时间整点边界
- 空值与非法值:null、空串、半角空格、格式错乱
- 冬令时/夏令时地区切换:如果业务有海外用户,这个必须测
下面展示一组核心测试,注意我测的全是容易出错的角落:
@Test void testLastDayOfMonth_leapYear() { LocalDate date = LocalDate.of(2024, 2, 1); LocalDate lastDay = DateUtil.lastDayOfMonth(date); assertEquals(LocalDate.of(2024, 2, 29), lastDay); } @Test void testLastDayOfQuarter_crossYear() { LocalDate date = LocalDate.of(2024, 12, 15); LocalDate lastDay = DateUtil.lastDayOfQuarter(date); assertEquals(LocalDate.of(2024, 12, 31), lastDay); } @Test void testParseDateTime_trimSpace() { assertEquals( LocalDateTime.of(2024, 5, 6, 7, 8, 9), DateUtil.parse(" 2024-05-06 07:08:09 ", DateUtil.PATTERN_DATETIME) ); } @Test void testParsePattern_WrongYYYY() { // 2024-12-29这个日期,祝用YYYY的人好运 assertThrows(DateTimeParseException.class, () -> { DateUtil.parse("2024-12-29", "YYYY-MM-dd"); }); }最后一个测试的评论是故意写的。YYYY解析2024-12-29碰到的坑不是解析异常,而是它会给出完全不同的“周年份”。这个专题下一节单独讲。
4.2 测试用例不是多就好,场景化组合才有效
很多团队测DateUtil,一个方法写一个正常用例就收工。我建议按“业务场景组合”来测,不然各方法分开看都对,串起来就出事。比如某个统计需求是“上个季度最后一天的结束时刻”,按场景串起来:
@Test void testEndOfLastQuarter_endOfDay() { LocalDate today = LocalDate.of(2024, 1, 20); // 一季度内 LocalDate lastQuarterLastDay = DateUtil.lastDayOfQuarter(today.minusMonths(3)); LocalDateTime endTime = DateUtil.endOfDay(lastQuarterLastDay); // 2023-12-31 23:59:59.999... assertEquals(LocalDate.of(2023, 12, 31), endTime.toLocalDate()); assertEquals(LocalTime.MAX, endTime.toLocalTime()); }这里用LocalTime.MAX而不是手写23:59:59,是因为LocalTime.MAX精确到纳秒23:59:59.999999999,自带边界意义。写测试的时候优先用JDK的常量,比自己在测试里造数字可靠。
测试数据这块我很建议做一个“日期测试数据生成器”辅助工具,专门生成所有边界日期集合,比如闰年列表、每月最后一天列表、跨年周列表,直接喂给参数化测试。手写几十条assertEquals是对生命的不尊重。
5. 常见问题与排查技巧实录
5.1 第一个大坑:SimpleDateFormat线程安全回溯
现在代码都用DateTimeFormatter了,但存量老系统里还有大把SimpleDateFormat尸体。如果你接手老项目,发现偶发性乱时间,第一反应就查有没有共享的SimpleDateFormat实例。排查思路很简单:
- 全局搜索
SimpleDateFormat字段定义,看是不是static - 如果是,再看有没有被多线程共用
- 出现
NumberFormatException、A java.text.ParseException: Unparseable date这类日志时,往这个方向查准没错
修复方案有两个。简单方案:每次调用都new SimpleDateFormat,并发安全但浪费对象;推荐方案:换成DateTimeFormatter线程安全实例,或使用ThreadLocal包裹旧实例做过渡。老系统迁移不能一蹴而就时,我用过ThreadLocal方案平滑过渡了一两周,再把所有调用点换到新工具。
5.2 第二个大坑:yyyy还是YYYY,一个字符毁了一季度的报表
这是我在项目中栽过最无语的跟头。某次季度报表的汇总统计,几乎整整一周的跨年数据全部归到了错误的年份里。一开始没人怀疑格式,后来查到一个查重接口里写的是"YYYY-MM-dd"。
yyyy是“纪元年份”,YYYY是“Week Based Year”(周周年份)。在ISO-8601定义中,每个周一作为一周的开始,一年的第一个周四所在的周算第一周。“YYYY-MM-dd”在12月29日到12月31日之间,完全可能因为“这一周属于下一年”而显示成下一年的日期。
排查这类问题最直接的手段:用DateTimeFormatter跑一遍2024-12-29到2025-01-04的格式化输出,肉眼就能看出分裂:
2024-12-30 -> yyyy: 2024, YYYY: 2025 2025-01-01 -> yyyy: 2025, YYYY: 2025血泪教训写进团队规范:任何时间格式化模式里,年份只能用小写yyyy,星期相关的YYYY只有在你需要周维度报表时才用。这个字符问题每年跨年都要坑一波人,我专门把它写进了DateUtil的注释头条,防止后来者改代码时顺手复制错误示例。
5.3 第三个大坑:时区配置混乱导致的时间偏移
有位兄弟遇到过这么个事:接口返回给前端的时间总是比数据库多8小时,后来发现数据库连接串上没有指定时区,而数据库默认时区是UTC,程序在本地时区组装完时间后存进去,再按UTC取出来显示,直接偏移。
这类问题的排查我一般按三步走:
- 先确认“存进去的时间和查出来的时间是否一致”,不一致基本是时区或驱动问题
- 再打点打印数据库连接串的
serverTimezone参数,以及JVM默认时区 - 最后看ORM框架有没有做自动时间转换(很多框架默认把切到UTC或本地时区)
根治办法就是我在DateUtil里强制显式指定时区,同时规范入库格式统一用UTC时间戳。一旦DateUtil成了访问时间的唯一入口,时区问题就等于在门口堵住了,不会再到处渗透。
5.4 第四个争议:工具类要不要全静态、要不要搞单例
很多教材说“工具类不要实例化,私有构造器就好,全用静态方法”。这个说法没毛病,但我建议在DateUtil身上投一个额外的思想:尽量无状态。静态方法的类必须是纯函数式的,入参进来,结果出去,不依赖任何全局可变配置。
团队里有同事曾经给DateUtil加过一个“全局默认格式”的可变静态字段,支持运行时修改。上线后果然出事:A服务动态改了默认格式,B服务老数据解析全乱。后来我把可变字段全部挪走,所有格式都要求从入参或者常量里取。经过这件事,DateUtil彻底保持纯净,项目里任何全局配置都藏到配置中心去,不会放在工具类的静态字段里。
如果你特别在意性能,可以把常用的DateTimeFormatter作为static final缓存。因为DateTimeFormatter不可变,这样做既是安全的,又免了频繁创建。但像ofPattern(pattern)这种自定义格式,能传常量就传常量,不要在一个高并发循环里new一万次。实测下来,缓存常用格式和不缓存之间的差距在高频日志场景下是肉眼可见的。
5.5 我的避坑操作清单(直接抄)
把这几年的经验浓缩成一份清单,开发提测前对着自查一遍:
- 格式化年份一律小写
yyyy,周年份一律大写YYYY并明确用途 - 24小时制用大写
HH,12小时制用小写hh,分清场景 - 不要信任服务器默认时区,跨时区部署必须显式传
ZoneId - 解析用户输入前先
trim(),空串分两个策略:抛异常或者返回null,但要用parseOrDefault包一层 - 高并发代码里不要反复创建格式化器,优先用
static final - 计算日期区间时先确认业务含不含尾,命名区分
daysBetween和daysInclusive - 年龄计算不用
Period.between和withYear处理闰日,用MonthDay
清单不长,但每一条背后都对应过至少一次生产事故。
6. 这套DateUtil还能怎么扩展
核心工具稳定后,我在这套DateUtil外又延伸了几个小功能,都不往主类里塞,而是各自独立成类。
一个是FiscalDateUtil(财年工具类)。某财务系统没有用自然年做结算周期,而是从每年4月1日起算财年,季度切割完全不同于自然季度。这种逻辑如果混进DateUtil,会让工具类被一个业务场景绑架。我单独建类处理,DateUtil保持通用。
另一个是HolidayCalendar(可配置节假日)。前面说的法定节假日调休,我把它做成基于配置文件的Set<LocalDate>,一年更新一次配置,不用发版。DateUtil只提供isHoliday(LocalDate, Set<LocalDate>)的纯函数式方法,不吃外部状态。
还有个经验:如果项目里有Excel导入导出,日期工具最好配合单元格格式统一处理。我在某数据迁移项目里发现,Excel日期序列号(比如44927代表某个日期)直接解析成文本后极其反直觉,后来单独写了个转换方法处理,测试时把1899年这种跨世纪边界也算上了。
这些扩展的目的只有一个:让DateUtil守住“日期通用能力”的边界,所有偏业务的规则都流出去,避免工具类腐化成业务类的垃圾桶。
最后再说一点体会,纯粹个人风格的选择。DateUtil这种工具类是最容易被低估的代码,因为它到处都在用,人人都能调,但写得好不好,决定了一个团队处理时间问题的整体底线。我做完那轮重构后最大的感受不是代码跑得多顺,而是下游同事提bug的次数明显少了,排查问题的时间也压缩了。如果你准备在项目里重做DateUtil,别急着写方法,先跟团队把时间规范、时区约定、格式标准定清楚,代码是最不着急的一环。