Java Calendar类实战:日期运算、格式化与时区避坑指南
做Java开发这些年要说哪个类最让人“又爱又恨”java.util.Calendar绝对排得上号。这节内容在课程里排到2.20看着像是个入门章节但真到实战里Calendar类的用法能玩出不少花活——月份从0开始、时区偏移、夏令时、线程安全哪一个拎出来都够新手喝一壶的。今天我就把这几年来用Calendar踩过的坑、总结出来的经验一次性倒出来从最基础的拿实例开始到日期运算、格式化、时区处理再结合fossify calendar这类开源日历应用里的真实场景尽量把这块讲透。不管是刚学Java的初学者还是写了好几年业务代码想回头补基础的老手这篇都值得花几分钟过一遍。1. 为什么都在劝退Date却绕不开Calendar1.1 Date那点糟心事在Java 8的java.time包出现之前java.util.Date是所有日期操作的起点。但这家伙的设计放到今天看确实一言难尽。年份从1900年开始算月份从0开始所有字段都是可变的而且它对时区几乎没有真正的感知能力——Date内部存的只是一个long类型的毫秒时间戳打印出来默认用本地时区但你想拿它做“给某个日期加3天”这种操作得先转成Calendar或者自己写毫秒换算。我自己早期写代码就干过这种事算某天之后7天是哪天直接用date.getTime() 7 * 24 * 60 * 60 * 1000写成new Date(old.getTime() 7L * 24 * 60 * 60 * 1000)。看着没问题但一旦遇到夏令时切换一天不是24小时这种写法就直接翻车。而且这种魔法数字堆在代码里过两个月自己回来看都脑袋疼。1.2 Calendar的设计思路Calendar这个抽象类就是冲着解决上面这些问题来的。它把“时间”拆成一组字段年、月、日、时、分、秒、星期、年内第几周、周内第几天等等然后提供统一的读写和运算接口。你不再需要自己去处理毫秒换算add、roll这些方法会帮你处理好进位、跨月、跨年甚至闰年。有人可能会问既然Java 8都推出java.time了为什么还要学Calendar两个原因。第一大量存量项目、Android生态、老框架里还在用你接手代码不可能让老板先把所有日期代码重写一遍。第二理解Calendar的设计思路能帮你更好地理解后来java.time为什么那么设计——它是踩在Calendar的坑上重建的。所以我倾向于把Calendar当作“必学的历史课实战技能”而不是“过时的老古董”。注意Calendar是个抽象类你不能直接new Calendar()得通过工厂方法取实例。这点和java.time里的LocalDate.now()思路一脉相承。2. Calendar类的基础用法取实例、读写字段、避开常量陷阱2.1 获取Calendar实例的正确姿势最常见的写法是Calendar.getInstance()这个方法内部会根据默认时区和语言环境返回一个GregorianCalendar实例。你也可以指定时区和Locale// 默认时区、默认Locale Calendar cal Calendar.getInstance(); // 指定时区 Calendar calWithZone Calendar.getInstance(TimeZone.getTimeZone(Asia/Shanghai)); // 同时指定时区和Locale Calendar calWithAll Calendar.getInstance( TimeZone.getTimeZone(America/New_York), Locale.US );这里要提醒一句getInstance()拿到的Calendar它的“当前时间”是初始化那一刻的时间。如果你在一个方法里连续取两次实例中间隔了几毫秒两个实例的时间戳可能是不一样的。这在某些毫秒级敏感的逻辑里会造成隐蔽的问题后面排查章节我会细说。2.2 get、set与字段常量拿到实例之后读写字段靠get(int field)和set(int field, int value)。field是Calendar类里定义的一堆常量最常用的有这么几个常量含义取值范围Calendar.YEAR年份正负整数Calendar.MONTH月份0~110代表1月Calendar.DAY_OF_MONTH日月内1~31Calendar.DAY_OF_WEEK星期几1~71是周日Calendar.HOUR_OF_DAY24小时制小时0~23Calendar.HOUR12小时制小时0~11Calendar.MINUTE分钟0~59Calendar.SECOND秒0~59Calendar.MILLISECOND毫秒0~999Calendar.WEEK_OF_YEAR年内第几周1~53Calendar.DAY_OF_YEAR年内第几天1~366一个常被忽略的点HOUR和HOUR_OF_DAY的区别。如果你用HOUR必须配合AM_PM字段一起设置否则上午下午会乱套。我建议一律用HOUR_OF_DAY省心。写段示例Calendar cal Calendar.getInstance(); cal.set(Calendar.YEAR, 2025); cal.set(Calendar.MONTH, Calendar.FEBRUARY); // 注意2月对应MONTH1 cal.set(Calendar.DAY_OF_MONTH, 20); cal.set(Calendar.HOUR_OF_DAY, 14); cal.set(Calendar.MINUTE, 30); cal.set(Calendar.SECOND, 0); cal.set(Calendar.MILLISECOND, 0); int year cal.get(Calendar.YEAR); // 2025 int month cal.get(Calendar.MONTH); // 1 int day cal.get(Calendar.DAY_OF_MONTH); // 20 int hour cal.get(Calendar.HOUR_OF_DAY); // 142.3 月份从0开始的百年大坑Calendar.MONTH从0开始这是Java日期API里最著名的坑没有之一。Calendar.JANUARY的值是0Calendar.DECEMBER的值是11。很多人写代码时习惯性把用户输入的“2月”直接塞进去结果发现日期变成了3月排查半天找不到原因。我自己的习惯是凡是涉及月份的常量一律用Calendar.JANUARY、Calendar.FEBRUARY这种语义化常量不要用魔法数字。这样代码至少能自解释// 错误示范用户输入2月直接set MONTH 2 cal.set(Calendar.MONTH, 2); // 实际是3月 // 正确示范 cal.set(Calendar.MONTH, Calendar.FEBRUARY); // 实际是2月另外DAY_OF_WEEK也有自己的规则Calendar.SUNDAY是1Calendar.SATURDAY是7跟我们习惯的“周一是一周第一天”完全相反。如果你的业务里要判断“今天是周几”记得先想清楚返回值到底对应星期几别想当然。2.4 设置字段时的一个隐藏陷阱clear与set的纠缠Calendar的set方法是“延迟生效”的它不会立刻重算内部的时间戳要等到你调用get、getTime、getTimeInMillis这些方法时才会统一计算。这本身没什么问题但如果你先set了一部分字段又用new GregorianCalendar(2025, 0, 1)这种构造方式创建对象两者的字段初始化状态不一样。更关键的是如果你想设置“只改日期不改时间”最好先用clear()把所有字段清掉再set否则Calendar里可能残留着HOUR_OF_DAY、MINUTE等字段的旧值导致输出结果带了莫名其妙的时间。举个实际例子Calendar cal Calendar.getInstance(); cal.set(2025, Calendar.FEBRUARY, 20); // 这里get出来的时间时分秒是本机当前时间 // 因为set(int, int, int)只是设置了年月日时分秒还是原来的这一点在生成“某天的0点0分0秒”这种场景下特别容易踩雷。正确的做法是Calendar cal Calendar.getInstance(); cal.clear(); // 全部字段清零 cal.set(2025, Calendar.FEBRUARY, 20); // 或者 cal.set(2025, Calendar.FEBRUARY, 20, 0, 0, 0); cal.set(Calendar.MILLISECOND, 0);还有个小技巧setLenient(false)可以让Calendar进入严格模式遇到不合理的字段组合比如2月30日直接抛异常而不是自动给你进位到3月2日。这在做表单校验的时候特别有用。3. 日期运算与比较add、roll、before、after的实战差异3.1 add和roll的区别用一次就忘不掉做日期运算Calendar提供了两个核心方法add(int field, int amount)和roll(int field, int amount)。这两个方法表面看都是“给某个字段加一个数”但行为差异很大。add是“完整进位”运算给月份加1如果跨年了年份也会跟着变给日期加30天月份、年份会自动调整。比如2025年1月31号加1个月结果是2月28号平年而不是3月3号因为add会做溢出调整。roll则是“只动当前字段不进位”给1月31号加1个月结果还是1月31号不对月份会变成2月但日子保持在31号最终得到2月31号——Calendar会把“不存在的2月31号”解析成3月3号。这看起来和前一句矛盾其实不然roll不管其他字段的死活它就是把MONTH字段从0加到1至于DAY_OF_MONTH31这个组合合法不合法它不管。所以实战中我几乎不用roll它的语义太容易引发误解add才是符合日常逻辑的选择。Calendar cal Calendar.getInstance(); cal.set(2025, Calendar.JANUARY, 31); cal.add(Calendar.MONTH, 1); System.out.println(cal.getTime()); // 2025年2月28日自动调整到月末 cal Calendar.getInstance(); cal.set(2025, Calendar.JANUARY, 31); cal.roll(Calendar.MONTH, 1); System.out.println(cal.getTime()); // 注意不同JDK版本输出可能不一致可能变成3月3日所以在“下个月这一天”“三个月后的今天”这类业务里请坚定地使用add。3.2 before、after、equals与日期区间判断Calendar实现了Comparable接口同时也有before(Date)、after(Date)、equals(Object)这些方法。判断两个时间点谁先谁后直接用Calendar start Calendar.getInstance(); start.set(2025, Calendar.FEBRUARY, 1, 0, 0, 0); Calendar end Calendar.getInstance(); end.set(2025, Calendar.FEBRUARY, 28, 23, 59, 59); Calendar now Calendar.getInstance(); if (now.after(start) now.before(end)) { // 落在2月区间内 }这里有个细节before和after比较的是毫秒时间戳层面跟时区无关因为时间戳是绝对时间。但如果你要比较“两个Calendar代表的是不是同一个日历日期”不能直接用equals因为它比较的是所有字段值两个实例只要时分秒不同就不相等。正确做法是分别取YEAR、MONTH、DAY_OF_MONTH三个字段都相等才算同一天或者用一个工具方法把两边都归一到当天0点再比较。3.3 计算两个日期相差多少天别死磕getTimeInMillis老手拿到“两个日期相差几天”这种需求第一反应多半是long diff cal2.getTimeInMillis() - cal1.getTimeInMillis(); long days diff / (24 * 60 * 60 * 1000);这写法在绝大多数场景能用但有俩问题。一是没考虑夏令时某些时区在夏令时切换日一天只有23小时或25小时除以固定24小时会少算或多算一天。二是如果你要的是“自然日差值”——比如2月28号23点到3月1号凌晨1点按毫秒算是差2小时但按“自然日”用户会觉得这是2天后——那就不能用毫秒差。处理“自然日差值”更稳的做法是取DAY_OF_YEAR注意跨年场景或者先把两个Calendar都归一到当天0点再算毫秒差Calendar c1 Calendar.getInstance(); c1.set(2025, Calendar.FEBRUARY, 28, 0, 0, 0); c1.set(Calendar.MILLISECOND, 0); Calendar c2 Calendar.getInstance(); c2.set(2025, Calendar.MARCH, 1, 0, 0, 0); c2.set(Calendar.MILLISECOND, 0); long diffDays (c2.getTimeInMillis() - c1.getTimeInMillis()) / (24 * 60 * 60 * 1000); // 结果为1跨年场景下DAY_OF_YEAR会从365回落到1所以最稳的方案还是“归一化到0点再算毫秒差”或者直接上java.time的LocalDate.toEpochDay()。我后面会再提一句两者如何配合。4. 格式化与解析Calendar和SimpleDateFormat的配合实战4.1 从Calendar到字符串Calendar本身没有format方法你得先通过getTime()拿到Date再交给SimpleDateFormat去格式化。这是最标准的一条链路Calendar cal Calendar.getInstance(); cal.set(2025, Calendar.FEBRUARY, 20, 14, 30, 0); SimpleDateFormat sdf new SimpleDateFormat(yyyy-MM-dd HH:mm:ss); String formatted sdf.format(cal.getTime()); // 输出2025-02-20 14:30:00常用的格式模板我整理了一个速查表模板结果示例说明yyyy-MM-dd2025-02-20标准日期yyyy-MM-dd HH:mm:ss2025-02-20 14:30:00标准时间yyyy/MM/dd2025/02/20斜杠分隔yyyy年M月d日2025年2月20日中文习惯M不用MM也没关系HH:mm14:3024小时制时分hh:mm a02:30 下午12小时制a是上午下午标记yyyy-MM-dd HH:mm:ss.SSS2025-02-20 14:30:00.123带毫秒注意想清楚一个问题SimpleDateFormat里的yyyy和YYYY不一样。小写yyyy是“日历年份”大写YYYY是“周年份”Week Year跨年那一周两者可能不同。比如2025年1月1日如果落在2024年的最后一周YYYY格式化出来可能是2024。我见过有人用YYYY-MM-dd格式化导致元旦日期年份显示异常的Bug排查半天其实就是大小写写错了。这是非常经典的一个暗坑。4.2 从字符串到Calendar反向解析先parse成Date再setTime进CalendarSimpleDateFormat sdf new SimpleDateFormat(yyyy-MM-dd HH:mm:ss); Date date sdf.parse(2025-02-20 14:30:00); Calendar cal Calendar.getInstance(); cal.setTime(date);这里有几个解析相关的坑值得单独说第一SimpleDateFormat默认是宽松模式lenient解析2025-02-30不会报错而是会给你进位成2025-03-02。如果你要严格校验用户输入必须调用sdf.setLenient(false)。否则你做的日期校验形同虚设。第二如果解析的字符串里只有日期没有时间Date里时分秒会被置为0这通常符合预期。但如果字符串里带时区信息比如2025-02-20T14:30:0008:00SimpleDateFormat默认不会自动处理你需要把时区也写进模板yyyy-MM-ddTHH:mm:ssZ。老实说处理ISO 8601格式我更建议直接用java.time的OffsetDateTime.parse那就是另一个话题了。第三SimpleDateFormat是线程不安全的。多个线程共享同一个SimpleDateFormat实例做格式化会出现数据错乱甚至ArrayIndexOutOfBoundsException。项目里要么每次new一个要么用ThreadLocal包一层要么直接切到java.time。这一点在后面的排查章节我再展开。4.3 结合fossify calendar看真实场景的格式化需求讲到格式化正好说说fossify calendar这类开源日历应用。它是一款开源的Android日历App界面简洁没有广告日期逻辑完全跑在Java/Android框架上。日历应用最核心的界面元素之一就是事件列表里的时间显示。同一个事件在列表页显示“2月20日 14:30”在详情页显示“2025年2月20日 星期四 14:30”在月视图的小格子里可能只显示“14:30”这些都是典型的格式化场景。如果让我来设计这部分逻辑我会把格式化模板集中放在一个工具类里按显示粒度选模板而不是在页面里到处写SimpleDateFormat。比如public class DateFormatUtil { public static String formatDate(Calendar cal) { return new SimpleDateFormat(yyyy年M月d日).format(cal.getTime()); } public static String formatTime(Calendar cal) { return new SimpleDateFormat(HH:mm).format(cal.getTime()); } public static String formatDateTime(Calendar cal) { return new SimpleDateFormat(yyyy-MM-dd HH:mm:ss).format(cal.getTime()); } }这样做的另一个好处是集中管理模板以后要换格式显示风格只改一个地方。fossify calendar的源码里也是类似思路把日期格式化和字符串资源分开管理方便做多语言适配。5. 时区、语言环境与夏令时Calendar的国际视野5.1 显式设置时区别让代码“看心情”Calendar默认使用JVM的默认时区这个值会受到操作系统时区设置影响。在服务器部署到不同区域、或者用户手机设置不同时区时你要是没显式指定时区同样的代码在不同环境下跑出来的结果可能完全不一样。指定时区有两种方式创建实例时指定或者创建后setTimeZoneCalendar cal Calendar.getInstance(TimeZone.getTimeZone(Asia/Shanghai)); // 或者 Calendar cal2 Calendar.getInstance(); cal2.setTimeZone(TimeZone.getTimeZone(Europe/London));判断一个时间点的“本地表示”属于哪一天完全取决于你用的时区。同一个getTimeInMillis()在Asia/Shanghai看是2025年2月20日的下午在America/Los_Angeles看可能就是2025年2月20日的凌晨日期字段都可能不一样。所以如果业务里涉及到“按用户所在时区显示日期”一定要保证每个Calendar实例都绑定了正确的时区千万别混用。5.2 Locale影响一周从哪天开始、月份怎么显示很多人忽略Locale对Calendar的影响。Calendar.getFirstDayOfWeek()在不同Locale下返回值不同美国习惯周日是一周第一天中国和大部分欧洲国家习惯周一是第一天。这个值直接影响到WEEK_OF_YEAR的计算结果。如果你要做“本周”“本月”这种视图fossify calendar这类日历App里最典型的功能就必须明确一周从哪天开始。Android上系统的CalendarContract会提供用户的周起始偏好但你在Java代码里用Calendar算周区间时还是要自己确认Calendar cal Calendar.getInstance(Locale.CHINA); int firstDay cal.getFirstDayOfWeek(); // 周一值为Calendar.MONDAY2如果应用要做国际化比如同一个App在中文环境和英文环境下都要正确显示“周视图”建议获取周起始日时用Calendar.getInstance(Locale.getDefault()).getFirstDayOfWeek()而不是硬编码Calendar.MONDAY。5.3 夏令时那个“消失的一小时”夏令时可能是Calendar用法里最容易被忽略的坑。在实行夏令时的时区比如欧洲大部分地区、北美部分地区春季某天凌晨2点会直接跳到3点那一天只有23个小时秋季某天凌晨3点会回拨到2点那一天有25个小时。如果你用add(Calendar.DAY_OF_MONTH, 1)给某个日期加一天Calendar会正确处理这种异常时长因为add是按“日历字段”运算不是按固定的24小时。但如果你用getTimeInMillis()加24 * 60 * 60 * 1000就会踩中夏令时导致结果偏移一小时。所以涉及跨天运算绝对优先使用add这是我在生产环境用血泪换来的教训。另外提醒一句中国从1991年后就不再实行夏令时了所以国内很多开发者对这个问题没感觉。但一旦你的应用要服务海外用户或者你在处理存储在欧洲、北美服务器上的数据夏令时问题就会冒出来。6. 开源日历应用里的Calendar实战以fossify calendar为例6.1 事件重复规则add就是为这个设计的fossify calendar是当前很受关注的一个开源日历项目它是Simple Calendar的社区分支主打离线、隐私、无广告。calendar类应用里最有代表性的复杂逻辑就是“重复事件”每天重复、每周重复、每月重复、每年重复。这些重复规则用Calendar的add方法实现非常顺手。比如“每周一重复”的事件下一个实例就是Calendar next Calendar.getInstance(); next.set(2025, Calendar.FEBRUARY, 3, 9, 0, 0); // 周一上午9点 if (next.get(Calendar.DAY_OF_WEEK) ! Calendar.MONDAY) { // 找到本周一 next.add(Calendar.DAY_OF_MONTH, (Calendar.MONDAY - next.get(Calendar.DAY_OF_WEEK) 7) % 7); } // 下一个事件 next.add(Calendar.DAY_OF_MONTH, 7);“每月最后一个工作日”这种复杂规则就需要组合getActualMaximum和add了。getActualMaximum(Calendar.DAY_OF_MONTH)能拿到当月的实际最大天数这在处理2月、大小月的时候特别省心Calendar cal Calendar.getInstance(); cal.set(2025, Calendar.FEBRUARY, 1); int lastDay cal.getActualMaximum(Calendar.DAY_OF_MONTH); // 28平年2月这个getActualMaximum在fossify calendar这类日历应用里几乎是天天用因为月视图需要知道“这个月画几个格子”“最后一天是几号”。如果用固定数组或者自己写闰年判断很容易出边界Bug而Calendar已经帮你把这些都处理好了。6.2 月视图与周视图的区间计算日历应用最基础的UI是月视图一个网格每行7个格子对应一周七天。要渲染这个视图你得知道三个数据本月1号是星期几决定第一行从第几列开始、本月有多少天、要不要补上个月和下个月的“填充日期”。用Calendar可以这样算Calendar cal Calendar.getInstance(); cal.set(Calendar.DAY_OF_MONTH, 1); // 调到本月1号 int firstDayOfWeek cal.get(Calendar.DAY_OF_WEEK); // 返回1~7需要结合getFirstDayOfWeek()转换成列下标 int daysInMonth cal.getActualMaximum(Calendar.DAY_OF_MONTH); // 月视图第一个格子的日期 cal.add(Calendar.DAY_OF_MONTH, -(firstDayOfWeek - cal.getFirstDayOfWeek() 7) % 7);周视图就更直接了先归一到本周的第一天然后连续加7次DAY_OF_MONTH, 1就能生成这一周全部7天。这些都是我在实际做日历类项目时反复用到的代码模式fossify calendar这样的成熟开源项目里也有类似的实现思路。6.3 全天事件的边界处理全天事件All-day Event是日历应用另一个容易出Bug的点。全天事件的语义是“某一天的一整天”它不绑定具体时分通常存储时取当天零点。但如果跨时区使用某个用户在东八区创建了一个2月20日的全天事件另一个用户在纽约看这个事件应该显示为2月19日还是2月20日不同产品定义不一样。用Calendar处理全天事件时我建议在存储层面统一用UTC或固定一个业务时区计算日边界时显式指定时区Calendar utcCal Calendar.getInstance(TimeZone.getTimeZone(UTC)); utcCal.set(2025, Calendar.FEBRUARY, 20, 0, 0, 0); utcCal.set(Calendar.MILLISECOND, 0); long eventStartUtcMillis utcCal.getTimeInMillis();然后在展示层再转成用户本地时区。这个“存储用固定时区展示用本地时区”的原则能帮你避开绝大多数全天事件的时区坑。7. 常见问题与排查技巧实录7.1 月份总是少一个月先查MONTH这是Calendar最经典的Bug症状是“我明明输入的2月存进去变成1月”。原因大概率是你在set月份时直接用了用户输入的数字没有做减1处理。反过来从Calendar取月份展示给用户时也要记得加1。这里我提供一个自查清单所有set MONTH的地方确认源数据的月份基数是不是0所有get MONTH后拼字符串的地方确认有没有加1如果用了Calendar.JANUARY这类常量确认常量值和你预期的月份对应7.2 线程安全Calendar和SimpleDateFormat都不能共享Calendar不是线程安全的多个线程同时修改同一个实例字段值会互相覆盖甚至出现中间态。SimpleDateFormat的线程不安全问题更严重它内部有个Calendar实例在做解析和格式化多线程共享时会报出诡异异常。我的建议是三条每个线程自己的局部变量随便用Calendar如果非要全局共享用ThreadLocalCalendar或ThreadLocalSimpleDateFormat包装最省心的方案新代码直接上java.time它的核心类都是不可变、线程安全的private static final ThreadLocalSimpleDateFormat SDF ThreadLocal.withInitial(() - new SimpleDateFormat(yyyy-MM-dd HH:mm:ss)); public static String formatDate(Date date) { return SDF.get().format(date); }7.3 性能别在循环里反复创建CalendarCalendar的getInstance()内部会加载时区数据、初始化一堆字段不是个廉价操作。如果你在循环里对几千条记录逐一创建Calendar再格式化性能会肉眼可见地变差。我自己实测过循环10万次Calendar.getInstance()和10万次LocalDate.now()对比前者要慢一个数量级。优化思路有两个方向。一是循环外复用一个SimpleDateFormat结合ThreadLocal保证线程安全二是批量场景直接用java.time性能更好、代码也更简洁。Calendar适合做“偶发、单点”的日期操作海量日期计算还是交给新API。7.4 新旧API怎么选我的判断标准看到这里你可能会问既然java.time这么好我是不是彻底抛弃Calendar我的判断标准是这样维护存量代码必须读懂、能改Calendar别一上来就重写新项目新代码默认用java.time除非团队约定或框架要求涉及Android的旧API Level如果minSdkVersion低于26那还是得依赖Calendar或者引入脱糖desugaring这是Android项目的现实约束在迁移过程中两者可以混用。比如你拿到一个旧系统返回的Date在代码里转成LocalDateTime再处理Date oldDate getOldDate(); LocalDateTime ldt oldDate.toInstant() .atZone(ZoneId.systemDefault()) .toLocalDateTime();反过来某些老接口只认Date你也可以从java.time转回去。过渡期这样做既不用推翻旧代码也能逐步享受新API的好处。写在最后关于Calendar我的几点体会做了这么多年Java我越来越觉得Calendar这类“老API”真正考验人的不是语法而是边界意识。月份从0开始、周从周日开始、时区不显式设置就会悄悄变化、夏令时会让一天不是24小时——每一条都是文档里有、但新手不会注意的细节。fossify calendar这类开源项目之所以能做好不是因为用了什么高深技术而是把这类边界处理得滴水不漏。如果你现在正在学这一节我的建议是别只看文档动手写几个小例子算一下你出生那天是星期几、算一下这个月最后一天是几号、把一个带时区的字符串解析成Calendar再格式化回去。这几个练习做完Calendar的基本用法基本就烂熟于心了。后面遇到更复杂的日期需求再逐步往java.time迁移你会发现很多设计都是相通的。日历这块内容不难难的是细心共勉。