首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java 日期時間處理工具類 DateUtil 完整實作與最佳實踐

     編輯:Java 相關知識

前言

日期時間處理是業務開發的高頻需求。雖然 JDK 8+ 推出全新 java.time 套件,但大量存量專案仍使用 java.util.Date/Calendar。本文提供一個生產級 DateUtil 工具類,涵蓋格式化、解析、運算、比較、邊界計算等 40+ 實用方法,並對比新舊 API 用法。

核心設計原則

  • 執行緒安全:SimpleDateFormat 非執行緒安全 → 使用 ThreadLocal 或方法區域變數
  • 不可變性:所有方法無副作用,回傳新物件/字串
  • 容錯性:異常捕獲記錄日誌,回傳預設值而非拋出
  • 效能:常用格式化器靜態快取,避免重複建立

DateUtil 完整原始碼

package com.util;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.ParseException;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.*;

public class DateUtil {
    private static final Logger log = LoggerFactory.getLogger(DateUtil.class);
    
    // ===== 常用格式常量 =====
    public static final String DATE_PATTERN = "yyyy-MM-dd";
    public static final String TIME_PATTERN = "HH:mm:ss";
    public static final String DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
    public static final String DATETIME_MS_PATTERN = "yyyy-MM-dd HH:mm:ss.SSS";
    public static final String MONTH_PATTERN = "yyyy-MM";
    public static final String COMPACT_DATETIME = "yyyyMMddHHmmss";
    
    // ===== 執行緒安全的格式化器快取 =====
    private static final ThreadLocal DATE_FMT = 
        ThreadLocal.withInitial(() -> new SimpleDateFormat(DATE_PATTERN));
    private static final ThreadLocal DATETIME_FMT = 
        ThreadLocal.withInitial(() -> new SimpleDateFormat(DATETIME_PATTERN));
    private static final ThreadLocal TIME_FMT = 
        ThreadLocal.withInitial(() -> new SimpleDateFormat(TIME_PATTERN));
    
    // ===== 1. 當前時間獲取 =====
    public static Date now() { return new Date(); }
    
    public static String nowDate() { return DATE_FMT.get().format(now()); }
    public static String nowTime() { return TIME_FMT.get().format(now()); }
    public static String nowDateTime() { return DATETIME_FMT.get().format(now()); }
    public static String nowCompact() { 
        return new SimpleDateFormat(COMPACT_DATETIME).format(now()); 
    }
    
    // ===== 2. 格式化:Date → String =====
    public static String format(Date date, String pattern) {
        if (date == null) return "";
        try {
            return new SimpleDateFormat(pattern).format(date);
        } catch (Exception e) {
            log.error("格式化失敗", e);
            return "";
        }
    }
    
    public static String formatDate(Date date) { return format(date, DATE_PATTERN); }
    public static String formatDateTime(Date date) { return format(date, DATETIME_PATTERN); }
    public static String formatTime(Date date) { return format(date, TIME_PATTERN); }
    
    // ===== 3. 解析:String → Date =====
    public static Date parse(String dateStr, String pattern) {
        if (StringUtils.isBlank(dateStr)) return null;
        try {
            return new SimpleDateFormat(pattern).parse(dateStr);
        } catch (ParseException e) {
            log.error("解析失敗:{}", dateStr, e);
            return null;
        }
    }
    
    public static Date parseDate(String dateStr) { return parse(dateStr, DATE_PATTERN); }
    public static Date parseDateTime(String dateStr) { return parse(dateStr, DATETIME_PATTERN); }
    public static Date parseTime(String dateStr) { return parse(dateStr, TIME_PATTERN); }
    
    // ===== 4. 日期運算 (基於 Calendar) =====
    private static Calendar toCal(Date date) {
        Calendar cal = Calendar.getInstance();
        cal.setTime(date);
        return cal;
    }
    
    public static Date addDays(Date date, int days) {
        Calendar cal = toCal(date);
        cal.add(Calendar.DAY_OF_MONTH, days);
        return cal.getTime();
    }
    
    public static Date addMonths(Date date, int months) {
        Calendar cal = toCal(date);
        cal.add(Calendar.MONTH, months);
        return cal.getTime();
    }
    
    public static Date addYears(Date date, int years) {
        Calendar cal = toCal(date);
        cal.add(Calendar.YEAR, years);
        return cal.getTime();
    }
    
    public static Date addHours(Date date, int hours) {
        Calendar cal = toCal(date);
        cal.add(Calendar.HOUR_OF_DAY, hours);
        return cal.getTime();
    }
    
    public static Date addMinutes(Date date, int minutes) {
        Calendar cal = toCal(date);
        cal.add(Calendar.MINUTE, minutes);
        return cal.getTime();
    }
    
    public static Date addSeconds(Date date, int seconds) {
        Calendar cal = toCal(date);
        cal.add(Calendar.SECOND, seconds);
        return cal.getTime();
    }
    
    // ===== 5. 日期差值計算 =====
    public static long diffDays(Date start, Date end) {
        if (start == null || end == null) return 0;
        long diffMs = end.getTime() - start.getTime();
        return diffMs / (24 * 60 * 60 * 1000);
    }
    
    public static double diffDaysPrecise(Date start, Date end) {
        if (start == null || end == null) return 0;
        return (end.getTime() - start.getTime()) / (24.0 * 60 * 60 * 1000);
    }
    
    public static int diffMonths(Date start, Date end) {
        if (start == null || end == null) return 0;
        Calendar s = toCal(start);
        Calendar e = toCal(end);
        return (e.get(Calendar.YEAR) - s.get(Calendar.YEAR)) * 12 
             + (e.get(Calendar.MONTH) - s.get(Calendar.MONTH));
    }
    
    // ===== 6. 邊界時間 (當天 00:00:00 / 23:59:59) =====
    public static Date startOfDay(Date date) {
        Calendar cal = toCal(date);
        cal.set(Calendar.HOUR_OF_DAY, 0);
        cal.set(Calendar.MINUTE, 0);
        cal.set(Calendar.SECOND, 0);
        cal.set(Calendar.MILLISECOND, 0);
        return cal.getTime();
    }
    
    public static Date endOfDay(Date date) {
        Calendar cal = toCal(date);
        cal.set(Calendar.HOUR_OF_DAY, 23);
        cal.set(Calendar.MINUTE, 59);
        cal.set(Calendar.SECOND, 59);
        cal.set(Calendar.MILLISECOND, 999);
        return cal.getTime();
    }
    
    public static Date startOfMonth(Date date) {
        Calendar cal = toCal(date);
        cal.set(Calendar.DAY_OF_MONTH, 1);
        return startOfDay(cal.getTime());
    }
    
    public static Date endOfMonth(Date date) {
        Calendar cal = toCal(date);
        cal.set(Calendar.DAY_OF_MONTH, cal.getActualMaximum(Calendar.DAY_OF_MONTH));
        return endOfDay(cal.getTime());
    }
    
    // ===== 7. 實用判斷 =====
    public static boolean isSameDay(Date d1, Date d2) {
        if (d1 == null || d2 == null) return false;
        return formatDate(d1).equals(formatDate(d2));
    }
    
    public static boolean isLeapYear(int year) {
        return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
    }
    
    public static int daysInMonth(int year, int month) {
        Calendar cal = Calendar.getInstance();
        cal.set(year, month - 1, 1);
        return cal.getActualMaximum(Calendar.DAY_OF_MONTH);
    }
    
    public static boolean isBefore(Date d1, Date d2) {
        return d1 != null && d2 != null && d1.before(d2);
    }
    
    public static boolean isAfter(Date d1, Date d2) {
        return d1 != null && d2 != null && d1.after(d2);
    }
    
    public static boolean isBetween(Date target, Date start, Date end) {
        return isAfter(target, start) && isBefore(target, end);
    }
    
    // ===== 8. 字串比較 (避免解析開銷) =====
    public static int compareDateStr(String d1, String d2) {
        if (StringUtils.isBlank(d1) || StringUtils.isBlank(d2)) return 0;
        return d1.compareTo(d2); // ISO 格式 yyyy-MM-dd 可直接字串比較
    }
    
    // ===== 9. 時間戳工具 =====
    public static long currentTimestamp() { return System.currentTimeMillis(); }
    public static String timestampToDateTime(long ts) { 
        return format(new Date(ts), DATETIME_PATTERN); 
    }
    
    // ===== 10. 相對時間描述 (如 "3分鐘前", "2天前") =====
    public static String relativeTime(Date date) {
        if (date == null) return "";
        long diff = System.currentTimeMillis() - date.getTime();
        if (diff < 60_000) return "剛剛";
        if (diff < 3_600_000) return (diff / 60_000) + "分鐘前";
        if (diff < 86_400_000) return (diff / 3_600_000) + "小時前";
        if (diff < 604_800_000) return (diff / 86_400_000) + "天前";
        return formatDate(date);
    }
}

JDK 8+ java.time 現代寫法對照

舊 API (DateUtil)新 API (java.time)優勢
DateUtil.nowDateTime()LocalDateTime.now().format(DTF)不可變、執行緒安全、流式 API
DateUtil.addDays(d, 3)localDate.plusDays(3)鏈式呼叫、不修改原物件
DateUtil.diffDays(d1, d2)ChronoUnit.DAYS.between(d1, d2)精確、支援納秒、時區感知
DateUtil.startOfDay(d)localDate.atStartOfDay()語意清晰
DateUtil.parseDateTime(s)LocalDateTime.parse(s, DTF)嚴格解析、異常資訊豐富
Calendar 時區坑ZonedDateTime / OffsetDateTime正確處理夏令時、跨時區

新專案建議遷移範例

// 現代日期工具類 (JDK 8+)
public class ModernDateUtil {
    private static final DateTimeFormatter DT_FMT = 
        DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    private static final DateTimeFormatter D_FMT = 
        DateTimeFormatter.ofPattern("yyyy-MM-dd");
    
    public static String now() { 
        return LocalDateTime.now().format(DT_FMT); 
    }
    
    public static LocalDateTime parse(String str) {
        return LocalDateTime.parse(str, DT_FMT);
    }
    
    public static String format(LocalDateTime ldt) {
        return ldt.format(DT_FMT);
    }
    
    public static long daysBetween(LocalDate start, LocalDate end) {
        return ChronoUnit.DAYS.between(start, end);
    }
    
    public static LocalDateTime startOfDay(LocalDate date) {
        return date.atStartOfDay();
    }
    
    public static String relative(LocalDateTime past) {
        Duration d = Duration.between(past, LocalDateTime.now());
        if (d.toMinutes() < 1) return "剛剛";
        if (d.toHours() < 1) return d.toMinutes() + "分鐘前";
        if (d.toDays() < 1) return d.toHours() + "小時前";
        return d.toDays() + "天前";
    }
}

避坑指南

  1. SimpleDateFormat 非執行緒安全:絕不可作為 static 欄位共用 → 使用 ThreadLocal 或區域變數
  2. Calendar 月份 0-based:JANUARY=0 → 易寫錯,新 API Month enum 解決
  3. 時區預設陷阱:Date 無時區,格式化/解析用 JVM 預設時區 → 明確指定 ZoneId
  4. 夏令時邊界:某天 23:00-25:00 不存在/重複 → 使用 ZonedDateTime 正確計算
  5. 字串比較陷阱:只有 ISO 8601 (yyyy-MM-dd) 格式才能直接字串比大小
  6. 效能熱點:高頻解析/格式化 → 快取 DateTimeFormatter、避免重複建立

測試主程式

public static void main(String[] args) {
    System.out.println("現在:" + DateUtil.nowDateTime());
    System.out.println("3天後:" + DateUtil.formatDate(DateUtil.addDays(new Date(), 3)));
    System.out.println("本月最後一天:" + DateUtil.formatDate(DateUtil.endOfMonth(new Date())));
    System.out.println("相對時間:" + DateUtil.relativeTime(DateUtil.addHours(new Date(), -2)));
    System.out.println("是否閏年:" + DateUtil.isLeapYear(2024));
}
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved