日期時間處理是業務開發高頻需求。本文系統整理 java.util.Calendar 經典用法 (維護舊代碼必備)、java.time 現代 API (新專案首選)、格式化/解析、運算、邊界計算、時區處理等核心場景,並列出避坑清單。
Calendar cal = Calendar.getInstance(); // 預設時區、Locale
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("Now: " + sdf.format(cal.getTime()));
cal.add(Calendar.DAY_OF_MONTH, -1); // 昨天 (會改變月份/年份) cal.add(Calendar.MONTH, 3); // 3個月後 cal.roll(Calendar.DAY_OF_MONTH, 10); // 只滾動天,不改變月/年
int year = cal.get(Calendar.YEAR); int month = cal.get(Calendar.MONTH) + 1; // 0-based! int day = cal.get(Calendar.DAY_OF_MONTH); int hour = cal.get(Calendar.HOUR_OF_DAY); // 24制 int min = cal.get(Calendar.MINUTE); int sec = cal.get(Calendar.SECOND);
// 本月最大天數 int maxDay = cal.getActualMaximum(Calendar.DAY_OF_MONTH); // 本月第一天 00:00:00 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); // 本月最後一天 23:59:59.999 cal.set(Calendar.DAY_OF_MONTH, maxDay); cal.set(Calendar.HOUR_OF_DAY, 23); cal.set(Calendar.MINUTE, 59); cal.set(Calendar.SECOND, 59); cal.set(Calendar.MILLISECOND, 999);
public long daysBetween(Date d1, Date d2) {
Calendar c1 = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
Calendar c2 = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
c1.setTime(d1); c2.setTime(d2);
// 歸零時分秒毫秒,比較自然日
for (Calendar c : Arrays.asList(c1, c2)) {
c.set(Calendar.HOUR_OF_DAY, 0);
c.set(Calendar.MINUTE, 0);
c.set(Calendar.SECOND, 0);
c.set(Calendar.MILLISECOND, 0);
}
return (c2.getTimeInMillis() - c1.getTimeInMillis()) / (1000*60*60*24);
}
| 舊類別 | 新類別 | 說明 |
|---|---|---|
| Date | Instant | 時間戳 (UTC) |
| Calendar | ZonedDateTime | 帶時區完整日期時間 |
| 無 | LocalDateTime | 無時區日期時間 (常用) |
| 無 | LocalDate / LocalTime | 純日期/純時間 |
| SimpleDateFormat | DateTimeFormatter | 線程安全、不可變 |
| Calendar.add | plusDays/minusMonths | 流式 API |
| getTimeInMillis | ChronoUnit.between | 精確差值計算 |
// 1. 當前時間
LocalDateTime now = LocalDateTime.now();
ZonedDateTime zdt = ZonedDateTime.now(ZoneId.of("Asia/Taipei"));
// 2. 格式化/解析
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String str = now.format(fmt);
LocalDateTime parsed = LocalDateTime.parse("2024-01-15 10:30:00", fmt);
// 3. 運算
LocalDate tomorrow = LocalDate.now().plusDays(1);
LocalDate lastMonth = LocalDate.now().minusMonths(1);
LocalDateTime nextHour = LocalDateTime.now().plusHours(1);
// 4. 自然日差 (自動處理夏令時)
long days = ChronoUnit.DAYS.between(LocalDate.now(), LocalDate.now().plusWeeks(2));
// 5. 邊界
LocalDate firstDay = LocalDate.now().withDayOfMonth(1);
LocalDate lastDay = LocalDate.now().with(TemporalAdjusters.lastDayOfMonth());
// 6. 星期判斷
DayOfWeek dow = LocalDate.now().getDayOfWeek(); // MONDAY..SUNDAY
ThreadLocal 或每次新建,或改用 DateTimeFormatter (不可變、線程安全)。JANUARY=0,取值時 +1,設值時 -1。Calendar.getInstance() 用 JVM 時區,跨地域部署必須顯式指定 TimeZone.getTimeZone("UTC") 或 ZoneId.of("Asia/Taipei")。LocalDate/ChronoUnit.DAYS。new Date(year, month, day) 年份為 1900-based、月份 0-based,極易出錯,一律用 Calendar 或 LocalDate.of。ISO-8601 字串 (2024-01-15T10:30:00+08:00) 或 Instant.toEpochMilli() 長整數,避免時區歧義。java.time,拒絕 Date/Calendar/SimpleDateFormat。java.time,內部轉換:Date.from(instant)、date.toInstant()、LocalDateTime.ofInstant(instant, zone)。LocalDateTime、LocalDate、Instant 映射。Calendar 以 add/get/set 完成萬年曆邏輯,但月份 0-based、非線程安全、時區隱性依賴等坑點眾多。Java 8 java.time 以不可變、線程安全、領域驅動設計 (LocalDate/LocalTime/Instant/ZonedDateTime) 徹底解決歷史包袱。**新代碼零成本遷移,舊代碼邊際替換**,是目前最穩健策略。