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 是不是更輕鬆了呢?