Java 中設計日期工具類 DateTools 和日期工廠類 DateFactory 完善飽受詬病的原生 Date 類

Java 原生的日期時間類 Date 有很多體驗不好的地方,比如裏面的年份字段存的是距離 1900 年的年數,月份字段用 0~11 代表 1~12 月,用裏面的 getYear(), getMonth() 等得到的都不是我們想要的結果,還要再進行額外的處理(setter 同理。這也是爲什麼這些方法在 JDK 1.1 之後馬上就被標註爲廢棄的原因。)後來的 Calendar 類雖然有所改良,但爲什麼唯獨留着那個月份字段不改,仍然是用 0~11 來存儲,我百思不得其解。

好在還有一個 DateFormat 類算是比較友好的,日期字符串裏的各個部分都是自然數,直接就能轉換爲 Date 對象,沒有年 + 1900,月 + 1 這樣的顧慮。那麼我們現在就結合 Date 和 DateFormat 這兩個類做一個日期工具類 DateTools 和日期工廠類 DateFactory,用於完善原生 Date 類。

首先我們來設計 DateFactory 工廠類,提供一些方法用於創建一個 Date 對象。先設計一個簡單的方法:

public class DateFactory {
    private static final DateFormat DATE_FORMAT;
    private static final String STR_FORMAT;

    static {
        DATE_FORMAT = new SimpleDateFormat("yyyy-M-d H:m:s", Locale.CHINA);
        STR_FORMAT = "%d-%d-%d %d:%d:%d";
    } // static

    public static Date create(int y, int m, int d, int h, int min, int sec) {
        String dateString = String.format(STR_FORMAT, y, m, d, h, min, sec);
        try {
            return DATE_FORMAT.parse(dateString);
        } // try
        catch (ParseException e) {
            throw new RuntimeException(e.toString());
        } // catch (ParseException e)
    } // create(int * 6)
} // DateFactory Class

在這個工廠方法裏,我們將傳入的年、月、日、時、分、秒組織起來,並按照【%d-%d-%d %d:%d:%d】的格式轉化成日期字符串,這個字符串格式和【yyyy-M-d H:m:s】的日期格式是完全一致的。然後我們調用 DateFormat 類的 parse 方法,將日期字符串轉換爲 Date 對象並返回。

這個方法要求提供精確到秒的時間。我們還可以再創建精確到分或者小時的工廠方法:

public class DateFactory {
    // 省略已有代碼

    // 精確到分鐘
    public static Date create(int y, int m, int d, int h, int min) {
        return create(y, m, d, h, min, 0);
    } // create(int * 5)

    // 精確到小時
    public static Date create(int y, int m, int d, int h) {
        return create(y, m, d, h, 0, 0);
    } // create(int * 4)
} // DateFactory Class

現在,假如我們要創建一個時間爲 2019 年 3 月 1 日 18 點 30 分的 Date 對象,我們就可以執行下面這條語句來創建了:

Date date = DateFactory.create(2019, 3, 1, 18, 30);

然後,我們再提供一些方法用於創建相對時間,即比一個基準 Date 對象早/晚一定時間的 Date 對象。

public class DateFactory {
    // 省略已有代碼

    // 創建比基準時間 date 要早 minutes 分鐘的 Date 對象
    public static Date backwardMinutes(Date date, int minutes) {
        long millis = date.getTime();
        millis -= 60000 * minutes;
        return new Date(millis);
    } // backwardMinutes()

    // 創建比基準時間 date 要晚 minutes 分鐘的 Date 對象
    public static Date forwardMinutes(Date date, int minutes) {
        long millis = date.getTime();
        millis += 60000 * minutes;
        return new Date(millis);
    } // forwardMinutes()
} // DateFactory Class

Date 對象內部記錄的其實是距離 1970 年 1 月 1 日零時的毫秒數,用 getTime() 方法獲得,同樣 Date 類也有一個傳入毫秒數的構造器。所以,創建比基準時間早/晚若干分鐘的 Date 對象,就是先將基準對象裏記錄的毫秒數提取出來,然後在此基礎上減去/加上 minutes 的值乘以 60000(一分鐘爲 60000 毫秒),再使用 Date 類的有參構造方法傳入新的毫秒數,創建新的 Date 對象返回即可。同理可以寫出創建比基準時間早/晚若干秒、若干小時、若干天的 Date 對象的工廠方法。

但是我們該如何調整 Date 對象的某一個或某些字段的值呢?比如某個 Date 對象,我只想改它的月份,或者日期不動,只改時間。誠然,目前的 DateFactory 類還不夠完善,但也請允許我暫時打個岔,去設計一下 DateTools 的工具類。設計完 DateTools 工具類後,上面的這些問題就能迎刃而解了。

我們在 DateTools 工具類中添加獲得一個 Date 對象中各日期時間字段值的方法:

public class DateTools {
    private static final DateFormat YEAR_SDF;
    private static final DateFormat MON_SDF;
    private static final DateFormat DAY_SDF;
    private static final DateFormat HOUR_SDF;
    private static final DateFormat MIN_SDF;
    private static final DateFormat SEC_SDF;

    static {
        YEAR_SDF = new SimpleDateFormat("yyyy", Locale.CHINA);
        MON_SDF = new SimpleDateFormat("M", Locale.CHINA);
        DAY_SDF = new SimpleDateFormat("d", Locale.CHINA);
        HOUR_SDF = new SimpleDateFormat("H", Locale.CHINA);
        MIN_SDF = new SimpleDateFormat("m", Locale.CHINA);
        SEC_SDF = new SimpleDateFormat("s", Locale.CHINA);
    } // static

    // 獲得年份
    public static int yearOf(Date date) {
        return Integer.parseInt(YEAR_SDF.format(date));
    } // yearOf()

    // 獲得月份
    public static int monthOf(Date date) {
        return Integer.parseInt(MON_SDF.format(date));
    } // monthOf()

    // 獲得日期
    public static int dayOf(Date date) {
        return Integer.parseInt(DAY_SDF.format(date));
    } // dayOf()

    // 獲得小時
    public static int hourOf(Date date) {
        return Integer.parseInt(HOUR_SDF.format(date));
    } // hourOf()

    // 獲得分鐘
    public static int minuteOf(Date date) {
        return Integer.parseInt(MIN_SDF.format(date));
    } // minuteOf()

    // 獲得秒
    public static int secondOf(Date date) {
        return Integer.parseInt(SEC_SDF.format(date));
    } // secondOf()
} // DateTools Class

在 DateTools 類裏,我們給日期時間的每一個字段(年、月、日、時、分、秒)都設置了一個 SimpleDateFormat 對象用於解析,然後解析出來的其實是字符串表示的對應字段的值,接着我們再用 Integer.parseInt() 方法將它們轉換成 int 類型並返回。這裏所有方法返回的字段值都是符合人類直觀的值,不會出現年要 + 1900,月要 +1 這樣的荒唐事。

當然你還可以在 DateTools 類裏添加其他和日期相關的方法,這裏舉一例,計算一個 Date 對象中的時間離當前時間多遠,以字符串形式返回(例如“1 小時前”、“2 天后”等)。更多的方法我不再過多闡述,各位可以自由添加。

public class DateTools {
    // 省略已有代碼

    // 計算一個 Date 對象所記載的時間距今多久,並以字符串形式返回
    public static String distanceToNow(Date date) {
        long millisToNow;
        long days, hours, minutes, seconds;

        if (date != null) {
            millisToNow = date.getTime() - System.currentTimeMillis();
            if (millisToNow < 0) {
                // 早於當前時間
                if (millisToNow <= -86400000) {
                    days = -millisToNow / 86400000;
                    return String.format("%d 天前", days);
                } // if (millisToNow <= -86400000)
                else if (millisToNow <= -3600000) {
                    hours = -millisToNow / 3600000;
                    return String.format("%d 小時前", hours);
                } // else if (millisToNow <= -3600000)
                else if (millisToNow <= -60000) {
                    minutes = -millisToNow / 60000;
                    return String.format("%d 分鐘前", minutes);
                } // else if (millisToNow <= -60000)
                else {
                    seconds = -millisToNow / 1000;
                    return String.format("%d 秒前", seconds);
                } // else
            } // if (millisToNow < 0)
            else {
                // 晚於當前時間
                if (millisToNow >= 86400000) {
                    days = millisToNow / 86400000;
                    return String.format("%d 天后", days);
                } // if (millisToNow >= 86400000)
                else if (millisToNow >= 3600000) {
                    hours = millisToNow / 3600000;
                    return String.format("%d 小時後", hours);
                } // else if (millisToNow >= 3600000)
                else if (millisToNow >= 60000) {
                    minutes = millisToNow / 60000;
                    return String.format("%d 分鐘後", minutes);
                } // else if (millisToNow >= 60000)
                else {
                    seconds = millisToNow / 1000;
                    return String.format("%d 秒後", seconds);
                } // else
            } // else
        } // if (date != null)
        else {
            return "未知";
        } // else
    } // distanceToNow()
} // DateTools Class

現在讓我們回到 DateFactory 工廠類。有了 DateTools 類的獲得各字段值的方法後,我們可以很輕鬆地調整一個 Date 對象中各字段的值了。直接上代碼:

public class DateFactory {
    // 省略已有代碼

    // 定義調校類,用於調整一個 Date 對象中各字段的值
    public static class Adjuster {
        private int year, month, day, hour, minute, second;

        // 構造器中傳入基準 Date 對象
        public Adjuster(Date baseDate) {
            this.year = DateTools.yearOf(baseDate);
            this.month = DateTools.monthOf(baseDate);
            this.day = DateTools.dayOf(baseDate);
            this.hour = DateTools.hourOf(baseDate);
            this.minute = DateTools.minuteOf(baseDate);
            this.second = DateTools.secondOf(baseDate);
        } // Adjuster() (Class Constructor)

        // 提供各時間字段的 setter。返回自身以便鏈式調用。
        public Adjuster setYear(int year) {
            this.year = year;
            return this;
        } // setYear()

        public Adjuster setMonth(int month) {
            this.month = month;
            return this;
        } // setMonth()

        public Adjuster setDay(int day) {
            this.day = day;
            return this;
        } // setDay()

        public Adjuster setHour(int hour) {
            this.hour = hour;
            return this;
        } // setHour()

        public Adjuster setMinute(int minute) {
            this.minute = minute;
            return this;
        } // setMinute()

        public Adjuster setSecond(int second) {
            this.second = second;
            return this;
        } // setSecond()

        // 提交更改,返回一個新的 Date 對象
        public Date commit() {
            return DateFactory.create(year, month, day, hour, minute, second);
        } // commit()
    } // Adjuster Inner Class
} // DateFactory Class

假如需要生成一個 Date2 對象,將原先的 date 的時間修改爲 12:34:56,則執行下面這條指令即可:

Date date2 = new DateFactory.Adjuster(date)
    .setHour(12)
    .setMinute(34)
    .setSecond(56)
    .commit(); // date2 = new DateFactory.Adjuster(date)...

其實你們可能已經看出來了,這其實就是一個 Builder 模式。Builder 模式可以一次提交多項屬性的修改,如果我們用一般的工廠方法,每次只改一個字段,像下面這樣的:

public static Date changeYear(Date baseDate, int year) {
    return create(
        year,
        DateTools.monthOf(baseDate),
        DateTools.dayOf(baseDate),
        DateTools.hourOf(baseDate),
        DateTools.minuteOf(baseDate),
        DateTools.secondOf(baseDate)
    ); // create()
} // changeYear()

那麼每改一個字段,都會新生成一個 Date 對象,除了最終結果外,中間生成的 Date 對象其實最終也都丟棄了,這就造成了資源和效率的浪費。使用 Builder 模式的話,我們可以把中間的臨時狀態緩存起來,到最後統一提交,既節省了資源,又提高了效率。

至此,我們的 DateTools 工具類和 DateFactory 工廠類就創建完成了。各位在 Java 中玩轉 Date 是不是更輕鬆了呢?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章