1. Java Calendar 基础与历史背景
1.1 认识 java.util.Calendar
Java 的旧日历API核心类是java.util.Calendar,它提供了对日期和时间的统一表示与运算能力。通过它可以对年、月、日、时、分、秒等字段进行访问和修改,并支持跨时区的处理。很多经典的日期计算都依赖于这个类,因此掌握它对日常开发仍然有帮助。
月份从0开始的设计是日常使用中最容易踩坑的点之一。Calendar.MONTH 的取值范围是 0~11,其中0表示一月,11表示十二月,因此在编码时经常需要使用Calendar.FEBRUARY等常量来避免直接写数字。
以下示例展示如何获得一个日历实例并读取基础字段:
import java.util.Calendar;Calendar cal = Calendar.getInstance();
System.out.println("今天的年份: " + cal.get(Calendar.YEAR));
System.out.println("今天的月份(0-based): " + cal.get(Calendar.MONTH));
System.out.println("今天的日子: " + cal.get(Calendar.DAY_OF_MONTH));
通过理解这些字段,你可以在不依赖额外库的情况下进行简单的日期计算与时序比较,为后续深入学习打下基础。
1.2 常用方法与注意点
set、get、add 与 roll是日历操作的核心方法。set 用于修改某一字段,get 获取字段值;add 可以对某一字段进行"加/减"并自动调整其他相关字段;而roll 在不跨越更高字段的前提下调整值,常用于日历内部的循环,但要避免误解。
下面的代码演示把某一天往后推 15 天,并注意月份的自动进位与年变更:
import java.util.Calendar;Calendar cal = Calendar.getInstance();
cal.set(2021, Calendar.JANUARY, 31); // 2021-01-31
cal.add(Calendar.DAY_OF_MONTH, 15); // 推后 15 天
int year = cal.get(Calendar.YEAR);
int month = cal.get(Calendar.MONTH); // 0-based
int day = cal.get(Calendar.DAY_OF_MONTH);
System.out.println(year + "-" + (month + 1) + "-" + day); // 2021-02-15
跨月与跨年的处理在日历计算中非常常见,尤其是在按月统计、日历视图、或区间判定时,需要对字段之间的依赖关系有清晰认知。
2. 日期运算的基本操作:加减、比较、差值
2.1 使用 Calendar 进行加减日期
加减日期的核心在于对 DAY_OF_MONTH、MONTH、YEAR 等字段进行操作,Calendar 会根据当前字段的值进行规范化处理。这在实现带有滚动效果的日期运算时尤为重要。
下面的示例演示如何从指定日期开始,连续工作日加上若干天:
import java.util.Calendar;Calendar cal = Calendar.getInstance();
cal.set(2022, Calendar.MARCH, 28); // 2022-03-28
cal.add(Calendar.DAY_OF_MONTH, 3); // 加 3 天
int y = cal.get(Calendar.YEAR);
int m = cal.get(Calendar.MONTH);
int d = cal.get(Calendar.DAY_OF_MONTH);
System.out.println(y + "-" + (m + 1) + "-" + d); // 2022-03-31
在需要忽略时间部分的场景时,可以通过清零时、分、秒来获得整日的比较基准。
2.2 比较日期与计算间隔
日期比较通常使用 compareTo、before、after等方法;计算间隔天数可以借助相同日的时间戳或通过日历字段实现。
下面给出两种常见场景的实现:先比较两个日期的先后顺序,再计算两个日期之间的天数差异。
import java.util.Calendar;Calendar a = Calendar.getInstance();
a.set(2020, Calendar.JULY, 10);Calendar b = Calendar.getInstance();
b.set(2020, Calendar.JULY, 20);// 比较
boolean aBeforeB = a.before(b); // true
boolean aAfterB = a.after(b); // false// 计算天数差(只做示例,注意时区可能影响结果)
long diffDays = (b.getTimeInMillis() - a.getTimeInMillis()) / (1000 * 60 * 60 * 24);
System.out.println("天数差: " + diffDays);
3. 时区与夏令时对日期计算的影响
3.1 时区基础
时区是日期时间计算中的关键维度,它决定了同一时间点在不同地区的表示方式。通过TimeZone(旧 API)或 ZoneId(新 API)可以明确指定时区,从而得到一致的跨区域时间表示。
在日常开发中,常见的做法是使用系统默认时区进行耦合,或显式指定如“UTC”、“Asia/Shanghai”等时区来避免歧义。
3.2 DST(夏令时)对日期的实际影响
夏令时的变动会导致某些日期的小时数发生变化,在跨时区的日历计算、跨国排程、日志时间戳处理等场景中尤需留意。
下面展示如何在指定时区下获取当前日期,并观察 DST 变化带来的时区偏移:
import java.time.*;
import java.time.format.DateTimeFormatter;ZoneId zoneNY = ZoneId.of("America/New_York");
ZonedDateTime zdt = ZonedDateTime.now(zoneNY);
DateTimeFormatter f = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z");
System.out.println(zdt.format(f));// DST 变动示例(在相应日期可能会看到时区偏移)
ZonedDateTime summer = ZonedDateTime.of(LocalDateTime.of(2024, 6, 1, 12, 0), zoneNY);
ZonedDateTime winter = ZonedDateTime.of(LocalDateTime.of(2024, 12, 1, 12, 0), zoneNY);
System.out.println("夏令时: " + summer.toString());
System.out.println("冬令时: " + winter.toString());
4. 现代日期时间 API 的迁移:从 Calendar 到 java.time
4.1 为什么要迁移
Java 8 引入了 java.time 包,提供了不可变、线程安全的日期时间类型,以及更直观的 API、清晰的时区和持续时间处理能力。相比于 Calendar,java.time 更适合现实世界的时间建模与跨系统集成。
常见对比:Calendar 是可变对象、复杂的字段操作容易出错;而 LocalDate、LocalDateTime、ZonedDateTime 等则以不可变对象和链式操作著称,成为新的默认选择。
4.2 关键转换与示例
日历到日期时间 API 的转换通常经过 Instant:Calendar → Instant → ZoneDateTime/LocalDateTime,然后可以使用 DateTimeFormatter 进行格式化。
下面的代码演示如何把一个 Calendar 对象转为 LocalDate,并在系统默认时区下显示:
import java.util.Calendar;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZonedDateTime;Calendar cal = Calendar.getInstance();
Instant instant = cal.toInstant();
ZonedDateTime zdt = instant.atZone(ZoneId.systemDefault());
LocalDate localDate = zdt.toLocalDate();
System.out.println("本地日期: " + localDate);
简单格式化与解析仍然需要 DateTimeFormatter,尽量使用 java.time 的 API 来实现稳定的增删改查与显示。
此外,迁移过程中也可以参考混合用法:在遗留代码中继续使用 Calendar,而在新模块采用 java.time,以实现平滑过渡。
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;LocalDate ld = LocalDate.of(2023, 8, 15);
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy/MM/dd");
String text = ld.format(fmt);
LocalDate parsed = LocalDate.parse(text, fmt);
System.out.println(text + " -> " + parsed);
5. 实战场景:日历与区间、周/月视图和跨年处理
5.1 求日期区间长度与覆盖面
在生成报告、排程或统计区间时,准确计算两个日期之间的天数、周数或月数至关重要。推荐使用 java.time 的 ChronoUnit 来避免时区引入的偏差。
以下示例展示如何计算两个 LocalDate 之间的天数、以及跨月跨年的边界处理:
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;LocalDate start = LocalDate.of(2022, 12, 28);
LocalDate end = LocalDate.of(2023, 1, 4);long daysBetween = ChronoUnit.DAYS.between(start, end);
long weeksBetween = ChronoUnit.WEEKS.between(start, end);
System.out.println("天数差: " + daysBetween);
System.out.println("周数差: " + weeksBetween);
5.2 生成指定月份的日历矩阵
日历矩阵是常见的前端展示需求,通过计算当月的第一天是星期几、以及当月的总天数,可以得到一个二维的日历结构,方便渲染。
下面给出一个简化的 Java 版实现思路,基于 LocalDate 与 TemporalAdjusters:
import java.time.LocalDate;
import java.time.YearMonth;
import java.time.DayOfWeek;
import java.util.ArrayList;
import java.util.List;YearMonth ym = YearMonth.of(2024, 9);
LocalDate firstOfMonth = ym.atDay(1);
int lengthOfMonth = ym.lengthOfMonth();
DayOfWeek firstDow = firstOfMonth.getDayOfWeek(); // 周日到周六List> calendar = new ArrayList<>();
List week = new ArrayList<>();
int emptyDays = firstDow.getValue() % 7; // 1(MONDAY) -> 1, adjust for locale if neededLocalDate iter = firstOfMonth.minusDays(emptyDays);
for (int i = 0; i < 42; i++) { // 6 行 x 7 列week.add(iter);if (week.size() == 7) {calendar.add(week);week = new ArrayList<>();}iter = iter.plusDays(1);
}
上述逻辑可扩展为完整的日历组件,包括高亮当前日、标注节假日、支持多时区等特性。结合前端模板,可以直接渲染出完整的月历视图,以支撑实战中的日程管理需求。



