Java 日期時間 API 經歷了三代演進:java.util.Date/Calendar (1.0/1.1,**缺陷眾多**)、Joda-Time (事實標準)、java.time (JDK 8+,**現代標準**)。本文聚焦**舊 API (Date/Calendar) 核心操作實戰**,並給出**遷移至 java.time 的最佳實踐建議**,滿足維護老項目與新開發雙重需求。
| 類 | 包 | 特點 | 線程安全 |
|---|---|---|---|
Date | java.util | 時間戳毫秒數包裝,大多方法已棄用,僅作數據傳遞 | 可變,非線程安全 |
Calendar | java.util | 抽象基類,GregorianCalendar 實現,字段操作、日期運算 | 可變,非線程安全 |
SimpleDateFormat | java.text | 格式化/解析,**非線程安全**,多線程需 ThreadLocal 或局部變量 | 非線程安全 |
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
public class DateUtils {
// 線程安全的格式化器持有方式:ThreadLocal
private static final ThreadLocal FMT_SECOND = ThreadLocal.withInitial(
() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA)
);
private static final ThreadLocal FMT_DAY = ThreadLocal.withInitial(
() -> new SimpleDateFormat("yyyy-MM-dd", Locale.CHINA)
);
// 1. Date -> String
public static String format(Date date, String pattern) {
if (date == null) return "";
return new SimpleDateFormat(pattern, Locale.CHINA).format(date);
}
public static String formatFull(Date date) { return FMT_SECOND.get().format(date); }
public static String formatDay(Date date) { return FMT_DAY.get().format(date); }
// 2. String -> Date
public static Date parse(String dateStr, String pattern) throws ParseException {
if (dateStr == null || dateStr.isEmpty()) return null;
return new SimpleDateFormat(pattern, Locale.CHINA).parse(dateStr);
}
public static Date parseFull(String dateStr) throws ParseException { return parse(dateStr, "yyyy-MM-dd HH:mm:ss"); }
public static Date parseDay(String dateStr) throws ParseException { return parse(dateStr, "yyyy-MM-dd"); }
// 3. Date <-> Calendar 互轉
public static Calendar toCalendar(Date date) {
Calendar cal = Calendar.getInstance();
cal.setTime(date);
return cal;
}
public static Date toDate(Calendar cal) { return cal.getTime(); }
// 4. 日期運算 (Calendar 強項)
public static Calendar addDays(Calendar cal, int days) {
cal.add(Calendar.DAY_OF_MONTH, days);
return cal;
}
public static Calendar addMonths(Calendar cal, int months) {
cal.add(Calendar.MONTH, months);
return cal;
}
public static Calendar addYears(Calendar cal, int years) {
cal.add(Calendar.YEAR, years);
return cal;
}
// 5. 獲取本月第一天/最後一天
public static Calendar firstDayOfMonth(Calendar cal) {
cal.set(Calendar.DAY_OF_MONTH, 1);
return cal;
}
public static Calendar lastDayOfMonth(Calendar cal) {
cal.set(Calendar.DAY_OF_MONTH, cal.getActualMaximum(Calendar.DAY_OF_MONTH));
return cal;
}
public static int getDaysInMonth(int year, int month) { // month 1-12
Calendar cal = Calendar.getInstance();
cal.set(year, month - 1, 1);
return cal.getActualMaximum(Calendar.DAY_OF_MONTH);
}
// 6. 獲取本周一 (Locale 相關)
public static Calendar mondayOfWeek(Calendar cal) {
cal.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY);
return cal;
}
// 7. 計算兩日期差值 (天/月/年)
public static long diffDays(Date start, Date end) {
return (end.getTime() - start.getTime()) / (24 * 60 * 60 * 1000);
}
public static int[] diffYMD(Date start, Date end) {
Calendar c1 = toCalendar(start);
Calendar c2 = toCalendar(end);
int years = c2.get(Calendar.YEAR) - c1.get(Calendar.YEAR);
int months = years * 12 + c2.get(Calendar.MONTH) - c1.get(Calendar.MONTH);
long days = diffDays(start, end);
return new int[]{years, months, (int)days};
}
// 8. 判斷是否同一天/同月/同年
public static boolean isSameDay(Date d1, Date d2) {
return FMT_DAY.get().format(d1).equals(FMT_DAY.get().format(d2));
}
}
| 雷區 | 後果 | 正確姿勢 |
|---|---|---|
SimpleDateFormat 定為 static 共享 | 多線程下解析/格式化錯誤、拋異常、數據錯亂 | 用 ThreadLocal、局部變量、或 JDK 8 DateTimeFormatter (線程安全) |
| 月份 0-11 陷阱 | 1月=0,12月=11,極易寫錯 | 常量 Calendar.JANUARY... 或 month - 1 寫死註釋 |
Date 可變性 | 被意外修改導致業務邏輯錯誤 | 防禦性拷貝:new Date(date.getTime());或遷移不可變 LocalDate |
Calendar.getInstance() 默認時區 | 跨時區部署/服務器時區變更導致邏輯偏差 | 顯式設置時區 cal.setTimeZone(TimeZone.getTimeZone("UTC")) |
年份 getYear() 返回年份-1900 | 2023 變 123 (已棄用) | 用 cal.get(Calendar.YEAR) |
解析異常 ParseException 未處理 | 運行時崩潰 | try-catch 包裝為非受檢異常或 Optional 返回 |
| 跨夏令時 (DST) 計算 | 一天非 24h,加減天數出錯 | 用 Calendar.add(DAY_OF_MONTH) 自動處理,勿手算毫秒 |
| 毫秒精度丟失 (數據庫 Timestamp) | Date 只有毫秒,DB 可能微秒/納秒 | 用 java.sql.Timestamp 或 JDK 8 LocalDateTime |
| 字符串格式不匹配 | yyyy-MM-dd 解析 2023/01/01 失敗 | 嚴格校驗輸入格式,或多模式嘗試解析 |
新項目強制、老項目漸進式遷移。核心類對應:
| 舊 API | 新 API (java.time) | 特性 |
|---|---|---|
Date | Instant (時間戳)、LocalDateTime (本地日期時間) | 不可變、線程安全、納秒精度 |
Calendar | ZonedDateTime (帶時區)、LocalDate/LocalTime | 領域模型清晰、API 豐富 |
SimpleDateFormat | DateTimeFormatter | 線程安全、預定義格式、解析更嚴格 |
TimeZone | ZoneId/ZoneOffset | IANA 時區庫、歷史規則完整 |
// 當前時間
LocalDateTime now = LocalDateTime.now();
ZonedDateTime nowZ = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
// 格式化 (線程安全!)
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String str = now.format(fmt);
LocalDateTime parsed = LocalDateTime.parse(str, fmt);
// 日期運算 (流式、不可變)
LocalDateTime nextWeek = now.plusWeeks(1);
LocalDate firstDay = now.with(TemporalAdjusters.firstDayOfMonth());
LocalDate lastDay = now.with(TemporalAdjusters.lastDayOfMonth());
// 時區轉換
Instant instant = now.atZone(ZoneId.systemDefault()).toInstant();
ZonedDateTime shanghai = instant.atZone(ZoneId.of("Asia/Shanghai"));
// 期間/持續時間
Period period = Period.between(startDate, endDate); // 年月日
Duration duration = Duration.between(startTime, endTime); // 時分秒納秒
// 舊新互轉
Date oldDate = Date.from(instant);
Instant inst = oldDate.toInstant();
ThreadLocal、防禦性拷貝、注意月份 0 基、時區顯式化。LocalDateTime/Instant (setObject/getObject),無需轉 Date。2023-10-01T12:00:00+08:00) 作為標準格式,Jackson/Gson 默認支持 java.time。日期時間處理看似瑣碎,實則是業務正確性的基石。**拋棄可變舊 API,擁抱不可變 java.time**,是現代 Java 開發者的必修課。