Java Calendar先设时间再改时区 重复调用日期减一天问题
问题:重复调用时间格式化函数时日期递减一天
我电脑默认时区是UTC,写了个把时间格式化为GMT+8时区0点的函数,之前一直正常,最近发现重复调用时日期会少一天。问题出在函数执行顺序——先设时间再设时区就会出问题,先设时区再设时间就正常,想知道原因。
测试代码
import java.util.Calendar; import java.util.Date; import java.util.TimeZone; public class Test { private static final String DEFAULT_TIME_ZONE = "GMT"; private static final String GMT_8_TIME_ZONE = "GMT+8"; /** * 先设置时间,再设置时区 */ public static long formatTimeToGmt8Zero(Long time) { Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone(DEFAULT_TIME_ZONE)); calendar.setTime(new Date(time)); calendar.setTimeZone(TimeZone.getTimeZone(GMT_8_TIME_ZONE)); calendar.set(Calendar.HOUR_OF_DAY, 0); calendar.set(Calendar.MINUTE, 0); calendar.set(Calendar.SECOND, 0); calendar.set(Calendar.MILLISECOND, 0); return calendar.getTimeInMillis(); } /** * 先设置时区,再设置时间 */ public static long formatTimeToGmt8Zero2(Long time) { Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone(DEFAULT_TIME_ZONE)); calendar.setTimeZone(TimeZone.getTimeZone(GMT_8_TIME_ZONE)); calendar.setTime(new Date(time)); calendar.set(Calendar.HOUR_OF_DAY, 0); calendar.set(Calendar.MINUTE, 0); calendar.set(Calendar.SECOND, 0); calendar.set(Calendar.MILLISECOND, 0); return calendar.getTimeInMillis(); } public static void main(String[] args) { Long beginTime = 1695957557093L;//Fri Sep 29 2023 03:19:17 GMT+0000 long time1 = formatTimeToGmt8Zero(beginTime); long time2 = formatTimeToGmt8Zero(time1); System.out.println("formatTimeToGmt8Zero beginTime:" + beginTime + ",time1:" + time1 + ",time2:" + time2); time1 = formatTimeToGmt8Zero2(beginTime); time2 = formatTimeToGmt8Zero2(time1); System.out.println("formatTimeToGmt8Zero2 beginTime:" + beginTime + ",time1:" + time1 + ",time2:" + time2); } }
运行结果
formatTimeToGmt8Zero beginTime:1695957557093,time1:1695916800000,time2:1695830400000 formatTimeToGmt8Zero2 beginTime:1695957557093,time1:1695916800000,time2:1695916800000
问题原因解析
先设时间再改时区的错误逻辑
- 初始解析:创建GMT时区的Calendar实例,调用
setTime传入UTC时间戳1695957557093(对应UTC的2023-09-29 03:19:17),此时Calendar内部的年月日时分秒字段是基于GMT时区解析的——年2023、月8(Calendar月份从0开始)、日29、时3等。 - 时区切换的坑:调用
setTimeZone切换到GMT+8时,Calendar不会重新用新时区解析时间戳,而是直接保留原有的字段值,用新时区去解释这些字段。原字段的“日29、时3”用GMT+8解释,对应的UTC时间是2023-09-28 19:00:00(因为GMT+8的29日3点等于UTC的28日19点)。 - 重置时间到0点:把时分秒毫秒设为0后,Calendar字段变成日29、时0(GMT+8),对应UTC时间2023-09-28 16:00:00,即时间戳
1695916800000。 - 重复调用的递减:第二次调用时,传入的时间戳
1695916800000对应UTC的2023-09-28 16:00:00,第一步解析为GMT时区的字段:日28、时16。切换到GMT+8后,这些字段解释为GMT+8的28日16点(对应UTC的28日8点),再设为0点后,对应UTC的27日16点,时间戳变成1695830400000——比第一次少了一天。
先设时区再设时间的正确逻辑
- 先切换时区:创建GMT时区的Calendar实例后,先切换到GMT+8时区。
- 正确解析时间:调用
setTime传入时间戳时,Calendar会直接用当前的GMT+8时区解析时间戳。比如1695957557093对应GMT+8的2023-09-29 11:19:17,字段为日29、时11。 - 重置到0点:设为0点后,得到GMT+8的29日0点,对应UTC的28日16点(时间戳
1695916800000)。 - 重复调用无变化:第二次调用时,传入的时间戳
1695916800000解析为GMT+8的29日0点,设为0点后还是同一个时间,所以不会递减。
优化建议:使用Java 8+时间API替代Calendar
Calendar是老旧API,设计存在很多反直觉的问题,推荐使用java.time包下的线程安全API,逻辑更清晰:
import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; public class Test { private static final ZoneId GMT_8 = ZoneId.of("GMT+8"); public static long formatTimeToGmt8Zero(long time) { return Instant.ofEpochMilli(time) .atZone(GMT_8) .truncatedTo(ChronoUnit.DAYS) .toInstant() .toEpochMilli(); } public static void main(String[] args) { long beginTime = 1695957557093L; long time1 = formatTimeToGmt8Zero(beginTime); long time2 = formatTimeToGmt8Zero(time1); System.out.println("formatTimeToGmt8Zero beginTime:" + beginTime + ",time1:" + time1 + ",time2:" + time2); // 输出:formatTimeToGmt8Zero beginTime:1695957557093,time1:1695916800000,time2:1695916800000 } }
内容的提问来源于stack exchange,提问作者peter zhang
相关产品推荐
相关产品推荐

