Calendar的getActualMinimum方法返回异常值原因咨询
问题:Calendar设置HOUR_OF_DAY后日期意外回退到前一天
我完全理解你遇到的这个诡异问题——在巴西利亚标准时间(BRT/GMT-3)下,尝试设置当月第一天的0点,结果设置小时后反而跳到了前一年的12月31日23点。咱们来拆解一下原因和解决办法:
首先看你的代码和输出:
你的代码
Calendar cal = Calendar.getInstance(); System.out.println("After instantiation: " + cal.getTime()); cal.set(Calendar.DAY_OF_MONTH, cal.getActualMinimum(Calendar.DAY_OF_MONTH)); System.out.println("After configuring the Day of Month: " + cal.getTime()); cal.set(Calendar.HOUR_OF_DAY, cal.getActualMinimum(Calendar.HOUR_OF_DAY)); System.out.println("After configuring the Hour of day: " + cal.getTime()); cal.set(Calendar.MINUTE, cal.getActualMinimum(Calendar.MINUTE)); System.out.println("After configuring the Minutes: " + cal.getTime()); cal.set(Calendar.SECOND, cal.getActualMinimum(Calendar.SECOND)); System.out.println("After configuring the Seconds: " + cal.getTime()); cal.set(Calendar.MILLISECOND, cal.getActualMinimum(Calendar.MILLISECOND)); System.out.println("After configuring the Millis: " + cal.getTime());
控制台输出
After instantiation: Thu Jan 19 10:40:18 BRT 2023 After configuring the Day of Month: Sun Jan 01 10:40:18 BRT 2023 After configuring the Hour of day: Sat Dec 31 23:40:18 BRT 2022 After configuring the Minutes: Sat Dec 31 23:00:18 BRT 2022 After configuring the Seconds: Sat Dec 31 23:00:00 BRT 2022 After configuring the Millis: Sat Dec 31 23:00:00 BRT 2022
问题根源
这个问题是过时的Calendar类的时区处理缺陷,再加上你使用的JDK版本(1.8.0_241)时区数据老旧共同导致的:
Calendar的分步计算陷阱:每次调用getTime()时,Calendar都会根据当前时区规则重新计算时间戳。当你设置DAY_OF_MONTH为1后,得到的是Jan 01 10:40:18 BRT 2023,这个时间对应的UTC是Jan 01 13:40:18 UTC(因为BRT比UTC晚3小时)。- 时区规则的错误判断:你的JDK 8u241的时区数据是tzdata 2019c,而巴西的夏令时规则在近几年有调整。JVM错误地认为2023年1月1日巴西利亚处于夏令时(BRST,GMT-2),而非你设置的BRT(GMT-3)。当你尝试把小时设为0时,
Calendar会先将Jan 01 00:40:18按照BRST规则转换成UTC(Jan 01 02:40:18 UTC),再转回BRT时区时,就变成了Dec 31 23:40:18 BRT 2022(UTC时间减3小时)。
解决方案
1. 推荐:迁移到Java 8的java.time API(JSR-310)
这是Java官方推荐的替代Calendar/Date的新API,设计更合理,时区处理更准确,完全避免这类诡异问题:
import java.time.ZoneId; import java.time.ZonedDateTime; public class FirstDayOfMonthExample { public static void main(String[] args) { // 使用巴西利亚时区的标准ID(比直接用BRT更可靠) ZoneId brasiliaZone = ZoneId.of("America/Sao_Paulo"); ZonedDateTime now = ZonedDateTime.now(brasiliaZone); // 直接调整到当月第一天的午夜 ZonedDateTime firstDayMidnight = now.withDayOfMonth(1) .withHour(0) .withMinute(0) .withSecond(0) .withNano(0); System.out.println("Instantiation time: " + now); System.out.println("First day of month at midnight: " + firstDayMidnight); } }
这个代码会输出正确的当月第一天午夜时间,自动适配当前时区规则。
2. 兼容旧代码:优化Calendar的使用方式
如果你必须保留Calendar,请一次性设置所有字段,避免分步调用getTime()触发多次计算:
Calendar cal = Calendar.getInstance(); // 一次性设置到当月第一天的0点0分0秒 cal.set(Calendar.DAY_OF_MONTH, 1); cal.set(Calendar.HOUR_OF_DAY, 0); cal.set(Calendar.MINUTE, 0); cal.set(Calendar.SECOND, 0); cal.set(Calendar.MILLISECOND, 0); System.out.println("First day of month at midnight: " + cal.getTime());
另外,也可以考虑更新JDK的时区数据(通过安装tzdata更新包),但这只是临时方案,长远来看还是建议迁移到java.time。
内容的提问来源于stack exchange,提问作者Guilherme Barboza
相关产品推荐
相关产品推荐

