十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java时间处理实战:从LocalDate核心API到工作日计算优化

Java时间处理实战:从LocalDate核心API到工作日计算优化 1. 项目概述从一道国赛真题看Java时间处理的实战精要去年带学生备赛蓝桥杯国赛复盘2021年那场时一个关于LocalDate的题目成了不少人的“滑铁卢”。题目本身不复杂就是给个起始日期和结束日期让你计算中间的工作日天数排除周末。听起来像是LeetCode上的简单题对吧但考场上的高压环境加上对Java时间API的细节掌握不牢很多人栽在了闰年判断、月份天数累加这些“基础”操作上要么超时要么结果差一两天。这让我意识到很多朋友对java.time包的理解还停留在“知道有这个东西”的层面一旦遇到需要灵活组合、高效计算的场景就容易手忙脚乱。今天我们就以这道题为引子彻底拆解LocalDate及其相关类。目标很明确不止于解题更要让你掌握一套在竞赛和实际开发中都能游刃有余处理日期时间的“肌肉记忆”。无论是蓝桥杯中对时间序列的模拟、日志分析中的日期统计还是业务系统里的周期计算这套思路都能直接套用。我会从最基础的创建与解析讲起深入到日期运算、周期判断、性能优化最后手把手带你用不止一种方法啃下那道国赛真题并分享考场上的实战技巧和避坑指南。如果你曾被Date和Calendar的混乱所困扰或者觉得LocalDate的API虽多却不知如何串联那么这篇结合了竞赛思维和工程实践的长文正是为你准备的。2. 核心需求解析为什么是java.time在深入代码之前我们必须先搞清楚为什么蓝桥杯乃至现代Java开发都强烈推荐使用java.timeJSR-310而摒弃古老的java.util.Date和Calendar这不仅仅是“潮流”而是源于实实在在的痛点。2.1 旧API的“罪状”与新时代的诉求回想一下用Date的经历Date对象明明叫“日期”却偷偷包含了时间甚至时区信息SimpleDateFormat不是线程安全的在多线程环境下必须每次new一个或者用ThreadLocal麻烦且易错Calendar的月份是从0开始的一月是0反人类的设计导致无数个“off-by-one”错误。更致命的是这些类都是可变的mutable你可以在一个地方修改一个Calendar实例而另一个持有该实例引用的地方却毫不知情这种副作用在复杂业务中是灾难性的。而java.time包的设计哲学截然不同清晰的值对象LocalDate只包含日期年-月-日LocalTime只包含时间LocalDateTime包含日期和时间ZonedDateTime包含时区。职责单一意图明确。不可变性Immutability所有核心类都是不可变的。任何修改操作如加一天、减一月都会返回一个新的对象原对象不变。这带来了线程安全性和可预测性是函数式编程的友好特性。流畅的APIFluent API方法链式调用非常自然例如date.plusDays(1).withMonth(12)读起来就像句子。强大的周期计算Period用于日期间隔年、月、日Duration用于时间间隔纳秒、秒。这对于计算两个日期之间相差多少天、多少个月或者给日期加上一个“1年3个月”这样的模糊周期变得异常简单。对于蓝桥杯竞赛这些特性直接转化为优势代码更简洁、更易读、更不易出错。在时间紧迫的赛场上你不需要花精力去记月份是从0开始还是1开始不需要担心日期格式化的线程安全问题可以更专注于算法逻辑本身。2.2 竞赛场景下的典型需求拆解基于历年真题我们可以将涉及LocalDate的题目需求归纳为以下几类日期计算与推导给定一个日期计算N天前/后的日期、本月最后一天、下一个周一等。这需要熟练使用plus/minus方法和TemporalAdjusters工具类。日期信息提取与判断获取日期是星期几、是当月的第几天、是否是闰年、是否在某个日期范围内。这需要用到getDayOfWeek()、getDayOfMonth()、isLeapYear()、isBefore()/isAfter()等方法。日期间隔计算计算两个日期之间相差的具体天数、工作日数排除周末或节假日。这是2021年国赛题的变种核心在于循环或流处理中的判断与累加。日期序列生成与处理生成一个时间范围内的所有日期并对它们进行过滤、统计等操作。这往往结合循环或Java 8 Stream API来实现。理解这些核心需求我们就能有的放矢地学习接下来的API而不是盲目地记忆所有方法。3.LocalDate核心API全解与实战技巧现在让我们进入实战环节。我会按照“创建 - 获取 - 修改 - 判断 - 计算”的逻辑顺序讲解LocalDate最核心的API并在每个环节穿插竞赛中高频使用的技巧和易错点。3.1 创建与解析如何正确“出生”一个日期创建LocalDate主要有三种方式对应不同的输入场景。// 1. 获取当前日期系统时钟默认时区 LocalDate today LocalDate.now(); // 竞赛注意如果题目有“基于当前日期”的假设就用这个。但通常题目会给定具体日期。 // 2. 指定年、月、日创建 LocalDate date1 LocalDate.of(2021, 5, 1); // 2021年5月1日 // 关键技巧月份用数字1就是1月告别Calendar的0。 // 易错点of方法会进行有效性校验。LocalDate.of(2021, 2, 30) 会直接抛出DateTimeException帮你提前发现错误。 // 3. 从字符串解析最常用因为竞赛输入通常是字符串 DateTimeFormatter formatter DateTimeFormatter.ofPattern(yyyy-MM-dd); LocalDate date2 LocalDate.parse(2021-07-01, formatter); // 更简单的情况如果是标准格式ISO-8601yyyy-MM-dd LocalDate date3 LocalDate.parse(2021-07-01); // 直接parse无需formatter注意字符串解析是竞赛中的主要输入方式。务必确认题目给出的日期格式。如果格式是yyyy/MM/dd或dd-MM-yyyy你必须定义对应的DateTimeFormatter。DateTimeFormatter也是不可变且线程安全的可以放心定义为static final常量。3.2 信息获取读懂日期的“身份证”创建日期后我们经常需要获取其组成部分或衍生信息。LocalDate date LocalDate.of(2021, 8, 25); // 获取基本单元 int year date.getYear(); // 2021 Month month date.getMonth(); // Month.AUGUST (枚举比数字更安全) int monthValue date.getMonthValue(); // 8 int dayOfMonth date.getDayOfMonth(); // 25 int dayOfYear date.getDayOfYear(); // 237 (这一年的第几天) // 获取星期几非常重要用于判断周末 DayOfWeek dayOfWeek date.getDayOfWeek(); // DayOfWeek.WEDNESDAY int dayOfWeekValue dayOfWeek.getValue(); // 3 (1Monday, 7Sunday) boolean isWeekend dayOfWeekValue 6 || dayOfWeekValue 7; // 判断是否为周六或周日 // 判断闰年 boolean isLeap date.isLeapYear(); // false, 2021年不是闰年 // 也可以直接用Year类判断Year.of(2021).isLeap()3.3 日期修改与调整让时间“流动”起来这是LocalDate最强大的能力之一。所有修改操作都返回新对象。LocalDate date LocalDate.of(2021, 1, 31); // 1. 直接加减TemporalAmount LocalDate nextDay date.plusDays(1); // 2021-02-01 (自动处理月末) LocalDate prevMonth date.minusMonths(1); // 2020-12-31 LocalDate nextYear date.plusYears(1); // 2022-01-31 // 2. 使用Period进行复杂加减 Period period Period.of(1, 2, 3); // 1年2个月3天 LocalDate futureDate date.plus(period); // 2022-04-03 // 3. 使用TemporalAdjusters进行常见调整超级实用 LocalDate firstDayOfMonth date.with(TemporalAdjusters.firstDayOfMonth()); // 2021-01-01 LocalDate lastDayOfMonth date.with(TemporalAdjusters.lastDayOfMonth()); // 2021-01-31 LocalDate nextMonday date.with(TemporalAdjusters.next(DayOfWeek.MONDAY)); // 2021-02-01 LocalDate lastInMonth date.with(TemporalAdjusters.lastInMonth(DayOfWeek.FRIDAY)); // 本月最后一个周五 // 4. 直接设置某个字段with LocalDate dateAtYearEnd date.withDayOfYear(365); // 2021-12-31 LocalDate dateAtMonthStart date.withDayOfMonth(1); // 2021-01-01实操心得TemporalAdjusters是竞赛神器。很多题目要求“下一个工作日”、“本季度末”用这个工具类一两行代码就能解决比自己写逻辑判断既快又准。务必熟悉它的常用方法。3.4 日期比较与区间判断谁在前谁在后比较是计算间隔和判断范围的基础。LocalDate start LocalDate.of(2021, 1, 1); LocalDate end LocalDate.of(2021, 12, 31); LocalDate target LocalDate.of(2021, 6, 15); // 基本比较 boolean isBefore target.isBefore(end); // true boolean isAfter target.isAfter(start); // true boolean isEqual target.isEqual(LocalDate.of(2021, 6, 15)); // true // 比较顺序start target end // 判断是否在某个区间内 [start, end] 闭区间 boolean isInRange !target.isBefore(start) !target.isAfter(end); // true // 或者用区间类如果JDK版本支持或引入三方库3.5 日期间隔计算精确到天、月、年计算两个LocalDate的间隔主要使用Period和ChronoUnit。LocalDate date1 LocalDate.of(2021, 1, 1); LocalDate date2 LocalDate.of(2021, 12, 31); // 方法1使用Period获取“人类友好”的间隔年、月、日分别多少 Period period Period.between(date1, date2); System.out.println(period.getYears()); // 0 System.out.println(period.getMonths()); // 11 System.out.println(period.getDays()); // 30 // 注意Period.between(date1, date2) 的结果是 date1 加上这个Period等于date2。它表示的是“差额”不是总天数。 // 方法2使用ChronoUnit计算总的天数、月数等更常用 long daysBetween ChronoUnit.DAYS.between(date1, date2); // 364天 long monthsBetween ChronoUnit.MONTHS.between(date1, date2); // 11个月 // 这是计算总间隔的推荐方式尤其是计算天数。踩坑记录Period.between在计算跨月的天数时逻辑是“忽略年和月只看日字段的差值”。而ChronoUnit.DAYS.between计算的是精确的总日历天数。在需要“工作日计数”的题目中我们几乎总是需要ChronoUnit.DAYS.between得到总天数然后再遍历减去周末。4. 攻克2021国赛真题工作日计算的多种实现有了前面的武器库我们现在正面迎战那道“传说中”的2021年国赛题。假设题目是计算从startDate字符串格式yyyy-MM-dd到endDate包含之间的工作日天数周一至周五。4.1 基础循环法最直观的解法这是最容易被想到的方法适合所有基础水平的选手。思路就是一天一天加判断是不是周末。import java.time.DayOfWeek; import java.time.LocalDate; import java.time.format.DateTimeFormatter; public class WorkingDaysBasic { public static long calculateWorkingDays(String startStr, String endStr) { DateTimeFormatter formatter DateTimeFormatter.ofPattern(yyyy-MM-dd); LocalDate start LocalDate.parse(startStr, formatter); LocalDate end LocalDate.parse(endStr, formatter); long workingDays 0; LocalDate current start; // 注意题目如果说明包含结束日期则循环条件是 !current.isAfter(end) while (!current.isAfter(end)) { DayOfWeek dayOfWeek current.getDayOfWeek(); if (dayOfWeek ! DayOfWeek.SATURDAY dayOfWeek ! DayOfWeek.SUNDAY) { workingDays; } current current.plusDays(1); // 移动到下一天 } return workingDays; } public static void main(String[] args) { System.out.println(calculateWorkingDays(2021-01-01, 2021-01-10)); // 输出6 (1,2,3是周末4,5,6,7,8,9,10中4,5,7,8,9,10是工作日) } }优点逻辑极其清晰易于理解和调试。缺点如果日期跨度非常大比如几十年循环次数会很多效率较低。但在蓝桥杯的输入规模内通常完全够用。4.2 流处理Stream API法函数式风格的优雅解法如果你熟悉Java 8的Stream这是一种非常简洁的写法。import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.util.stream.LongStream; public class WorkingDaysStream { public static long calculateWorkingDays(String startStr, String endStr) { DateTimeFormatter formatter DateTimeFormatter.ofPattern(yyyy-MM-dd); LocalDate start LocalDate.parse(startStr, formatter); LocalDate end LocalDate.parse(endStr, formatter); long totalDays ChronoUnit.DAYS.between(start, end) 1; // 1 因为包含结束日 return LongStream.range(0, totalDays) .mapToObj(start::plusDays) // 生成从start开始的所有日期流 .filter(date - { DayOfWeek dow date.getDayOfWeek(); return dow ! DayOfWeek.SATURDAY dow ! DayOfWeek.SUNDAY; }) .count(); // 统计过滤后的数量 } }优点代码紧凑表达了“生成序列-过滤-计数”的声明式逻辑。缺点需要理解Stream和Lambda。对于极大时间跨度它和循环法一样需要遍历每一天。4.3 数学优化法应对超大规模数据的思路当时间跨度以年为单位且需要频繁计算时我们可以用数学方法减少循环。核心思路是先计算完整的周数再处理剩余的天数。public class WorkingDaysMath { public static long calculateWorkingDays(LocalDate start, LocalDate end) { // 确保start不晚于end if (start.isAfter(end)) { return 0; } long totalDays ChronoUnit.DAYS.between(start, end) 1; // 总天数包含 DayOfWeek startDow start.getDayOfWeek(); int startDowValue startDow.getValue(); // 1Mon, 7Sun // 计算完整周数的工作日每周5个工作日 long fullWeeks totalDays / 7; long workingDays fullWeeks * 5; // 计算剩余天数0到6天 long remainingDays totalDays % 7; // 遍历剩余天数判断是否为工作日 for (long i 0; i remainingDays; i) { int dowValue (int)((startDowValue i - 1) % 7) 1; // 计算当前是星期几 if (dowValue 5) { // 1-5是工作日 workingDays; } } return workingDays; } }优点极大减少了循环次数。无论总天数是100天还是10000天最内层的循环最多只执行6次。性能最优。缺点逻辑稍复杂需要仔细处理星期几的数学计算容易在边界条件如余数计算上出错。考场策略建议在时间有限的竞赛中优先采用基础循环法。它的正确率最高调试最方便。除非题目明确提示日期范围极大比如1 N 10^9天否则不要轻易尝试数学优化法避免因小失大。先把题目做对再考虑优化。5. 性能优化与边界条件处理实录在实际编码和竞赛中除了核心逻辑性能和鲁棒性往往决定成败。这里分享几个关键点。5.1 避免在循环中重复创建对象这是一个常见的性能陷阱。看下面这段代码// 低效写法 while (!current.isAfter(end)) { DateTimeFormatter formatter DateTimeFormatter.ofPattern(yyyy-MM-dd); // 错误每次循环都创建 String str current.format(formatter); // ... 一些操作 current current.plusDays(1); }DateTimeFormatter的创建成本相对较高。正确的做法是将其提取到循环外部甚至定义为静态常量。// 高效写法 private static final DateTimeFormatter FORMATTER DateTimeFormatter.ofPattern(yyyy-MM-dd); // ... 在方法内 while (!current.isAfter(end)) { String str current.format(FORMATTER); // 使用常量formatter // ... }5.2 小心日期范围的边界“包含”还是“不包含”起始/结束日期是日期计算中最容易出错的地方。题目说“从A日到B日”通常包含B日。我们的循环条件while (!current.isAfter(end))就是包含结束日。如果用while (current.isBefore(end))就不包含结束日。务必仔细审题。5.3 处理非法输入和日期溢出竞赛题目的输入通常是规范的但养成防御性编程的习惯有好处。public static LocalDate safeParse(String dateStr, String pattern) { try { DateTimeFormatter formatter DateTimeFormatter.ofPattern(pattern); return LocalDate.parse(dateStr, formatter); } catch (DateTimeParseException e) { // 根据题目要求返回默认值或抛出特定异常 System.err.println(日期格式错误: dateStr); return null; // 或 throw new IllegalArgumentException(...); } }对于加减操作LocalDate会自动处理溢出如2021-01-31加一个月会得到2021-02-28而不会报错。这是符合预期的行为但你需要意识到这一点。6. 常见问题与排查技巧速查即使掌握了API实战中还是会遇到各种问题。这里整理了一个速查表。问题现象可能原因排查与解决解析日期时抛出DateTimeParseException1. 字符串格式与DateTimeFormatter模式不匹配。2. 日期值非法如2021-02-30。1. 仔细核对格式字符串。yyyy代表年MM代表月dd代表日。大小写敏感2. 使用DateTimeFormatter的parse方法它会自动校验。计算出的天数少1天或多1天边界条件处理错误。between方法或循环条件对“包含”理解有误。明确需求是否包含起始日和结束日ChronoUnit.DAYS.between(a, b)计算的是从a到b之间的天数不包含b。通常需要1。用isBefore/isAfter循环时想清楚终止条件。月份计算错误比如2月变成3月混淆了Period的getMonths()和ChronoUnit.MONTHS.between()。Period.getMonths()是“差额”中的月部分0-11。ChronoUnit.MONTHS.between()是忽略日后差异的总月数。根据需求选择。判断周末时结果不符合预期对DayOfWeek.getValue()的返回值理解有误。记住getValue()返回1周一到7周日。周六是6周日是7。判断周末的标准是value 6代码在长时间区间运行很慢使用了逐天遍历的朴素算法且区间跨度极大。考虑使用“数学优化法”先计算完整周数。如果题目允许思考是否存在更直接的数学公式。plusMonths()或minusMonths()在月末日期产生意外结果这是特性不是bug。例如Jan 31加一个月是Feb 28。LocalDate会保证结果日期是有效的。如果原日期是某月最后一天加减月份后也会是目标月的最后一天。如果需要保持“日”不变无效日期则抛异常应使用withDayOfMonth手动调整。最后再分享一个小技巧在竞赛中如果日期计算非常复杂可以在草稿纸上画一个简单的时间轴标出起始日、结束日、以及几个关键节点如周末。视觉化能极大降低逻辑出错的概率。对于LocalDate的操作多写单元测试验证边界情况比如闰年的2月29日、每个月的第一天和最后一天、跨年的计算等。把这些测试用例记下来就是你的“武器库”下次遇到类似题目能快速反应。
返回列表