1. 程式人生 > >java獲取每月最後一天

java獲取每月最後一天

一個小問題,成為了一個坑。

相信大家對這個題目——Java獲取每個月的最後一天——都不陌生吧。其實,不糾結於最後一天啦,也可以是上個月的最後一天,下個月的第一天,等等之類的。我發現網上都是寫好的一些例子,提供給大家解決那些固定要獲取的一個月的最後一天或者第一天,但是程式碼註釋卻又惜字如金,導致使用者在完全不理解的情況下,Ctrl+C和Ctrl+V,一個坑就暗含在了這裡。

先上一組“凌亂”的程式碼,根據方法名,這個函式是要求得每月的最後一天:

public static String getDateLastDay(String year, String month) {

      //year="2018"
month="2" Calendar calendar = Calendar.getInstance(); // 設定時間,當前時間不用設定 calendar.set(Calendar.YEAR, Integer.parseInt(year)); calendar.set(Calendar.MONTH, Integer.parseInt(month)); // System.out.println(calendar.getTime()); calendar.set(Calendar.DAY_OF_MONTH, 1); calendar.set
(Calendar.DATE, calendar.getActualMaximum(Calendar.DATE)); DateFormat format = new SimpleDateFormat("yyyy-MM-dd "); return format.format(calendar.getTime()); }

解析:
1. 對於這兩行程式碼:

calendar.set(Calendar.DAY_OF_MONTH, 1); 
calendar.set(Calendar.DATE, calendar.getActualMaximum(Calendar.DATE
));

產生了一條冗餘程式碼,首先Calendar.DAY_OF_MONTH和Calendar.DATE,本來就是一回事,如果你進入該行的JDK API你就會發現如下的解釋:

 /**
     * Field number for <code>get</code> and <code>set</code> indicating the
     * day of the month. This is a synonym for <code>DATE</code>.
     * The first day of the month has value 1.
     *
     * @see #DATE
     */
    public final static int DAY_OF_MONTH = 5;

這裡明確闡明瞭DAY_OF_MONTH是DATE的同義詞,可以理解成別名。
因此,JDK對於 Calendar.DAY_OF_MONTH和Calendar.DATE的取值都是“5”,所以看到有人一會用Calendar.DAY_OF_MONTH進行calendar例項物件的set,一會又用Calendar.DATE進行calendar例項物件的set,可見沒有理解這兩行程式碼的意思。

  1. 那個老生常談的問題。Calendar的體系中,month是從“0”開始數,Day卻是從“1”開始數。
    對於month來講,這句話calendar.set(Calendar.MONTH, Integer.parseInt(month)); 如果設定的是1,那麼下面的這句System.out.println(calendar.getTime());列印的將是3月的當前日,可見month是從“0”開始計數;同樣的,對於Day,上面的那段JDK程式碼中也說了“The first day of the month has value 1.” 所以,在這裡java8之前的JDK版本對於Calendar的設計確實是存在著凌亂的,也是容易讓我們程式設計師感到困惑的點。

  2. calendar.set和 calendar.add。曾經我也犯過這樣的錯誤,把add和set混用,其實對於set來說,你按照month從0開始,day從1開始這條規則設定的某個月的第幾天就是第幾天,對比add,也確實是在“0和1”的基線上加天數和月數。
    如果改成這樣:

Calendar calendar = Calendar.getInstance();

calendar.set(Calendar.YEAR, 2018);
calendar.set(Calendar.MONTH, 2);
System.out.println(calendar.getTime());

calendar.add(Calendar.MONTH, 3);
System.out.println(calendar.getTime());

先後列印的結果就是:

Tue Mar 06 10:50:38 CST 2018
Wed Jun 06 10:50:38 CST 2018

沒有問題。
我們可以把Calendar的設定簡單想成一個指標,set和add都是以你設定的日期為基準加上之前提到的“0和1”規則,進行指標移動,如果想要後退指標,那麼很簡單,set或者add負數就可以了。

  1. calendar.set(Calendar.DAY_OF_MONTH, 0)的作用。我們之前說了,day是從1開始的,那麼這句話又是什麼鬼呢。其實很容易理解,如果按照第三條貼出的程式碼,此時指標指向了2018-06-06,那麼再執行這句話就相當於將指標的天數歸零,然後後退一個月,也就是會輸出2018-05-31 。記住,如果你這樣執行:
Calendar calendar = Calendar.getInstance();

calendar.set(Calendar.YEAR, 2018);
calendar.set(Calendar.MONTH, 2);
System.out.println(calendar.getTime());

calendar.add(Calendar.MONTH, 3);
System.out.println(calendar.getTime());

calendar.set(Calendar.DAY_OF_MONTH, 12);
calendar.set(Calendar.DAY_OF_MONTH, 0);  

先設定了日子12,這時變成2018-06-12,再執行歸零操作,那麼還是變為2018-05-31,所以這句話就是將當前指標指向的日期回退到上個月的最後一天。

  1. calendar.getActualMaximum(Calendar.DATE))。在最初的凌亂程式碼中貼出的calendar.getActualMaximum(Calendar.DATE))這句話需要跟“歸零”操作區分開,這句話是真真正正的求當前指標指向的月份的最後一天。強烈不建議把calendar.getActualMaximum(Calendar.DATE))放在calendar.set中,因為會讓你對指標走到哪裡了產生困惑,尤其在加入了“歸零”操作的程式碼中,比如:
Calendar calendar = Calendar.getInstance();

calendar.set(Calendar.YEAR, 2018);
calendar.set(Calendar.MONTH, 2);
System.out.println(calendar.getTime()); //此處是三月

calendar.add(Calendar.MONTH, 3);  
System.out.println(calendar.getTime());  //此處是六月

calendar.set(Calendar.DAY_OF_MONTH, 12);
System.out.println(calendar.getTime());  //此處是6.12
calendar.set(Calendar.DAY_OF_MONTH, 0);  //此處是5.31

System.out.println(calendar.getActualMaximum(Calendar.DATE)); 
//此處是5月份的31天
System.out.println(calendar.getTime()); //此處是5.31

calendar.set(Calendar.DATE, calendar.getActualMaximum(Calendar.DATE));

DateFormat format = new SimpleDateFormat("yyyy-MM-dd");
System.out.print(format.format(calendar.getTime()));

我想這個例子看了我如下的操作以後,大家都會有困惑的:以上程式碼最後輸出的是2018-05-31,如果你理解了之前講的,這個結果是沒有問題的。但是,如果我把這行:System.out.println(calendar.getTime()); //此處是5.31 註釋掉,大家可以試一下,最後的輸出結果是2018-07-01。
為什麼?這個留給大家作為思考題,因為需要自己跟程式碼才有印象,總之這裡的結果如同執行了——
calendar.add(Calendar.DATE, calendar.getActualMaximum(Calendar.DATE));—— “add”方法,而不是“set”方法,因為結果就是在5.31的基礎上加上了31天,正好是7.1。

困惑吧,實話說我都沒有資訊繼續深究了,因為太凌亂了,所以,最好就是不要這樣寫。即不夾雜“歸零”操作,也不在set方法中使用這句程式碼calendar.getActualMaximum(Calendar.DATE),calendar.getActualMaximum()方法就是獲得當前月的最大天數,就只是這麼用就好了。

  1. 這裡還要提到的一個Tip就是DateFormat是執行緒不安全的,也就是說如果兩個執行緒同時操作最初程式碼片段中的最後兩句話,結果會出現意想不到的事情。

那麼,既然Calendar有這樣那樣的問題,而且時不時會出現凌亂的程式碼——也可以看出這是JDK最初設計上的一些瑕疵——那麼JDK對這部分有沒有升級呢?答案當然是肯定的。下面我就介紹一下Java8針對Calendar實現的一個完全的修改。

Java8中引入了LocalDate、LocalTime、Instant、Duration和Period,這也是Oracle在原生的Java API中為引入對日期和時間的高質量的支援整合第三方日期和時間庫Joda-Time的基礎上提出的。它們都存在於java.time包下:

LocalDate localDate = LocalDate.of(2018, 2, 6);
int year = localDate.getYear();
Month month = localDate.getMonth();
int day = localDate.getDayOfMonth();
DayOfWeek dow = localDate.getDayOfWeek();
int len = localDate.lengthOfMonth();
boolean bool = localDate.isLeapYear();
LocalDate today = LocalDate.now();

System.out.println(year);    // 2018
System.out.println(month);   // FEBRUARY
System.out.println(day);     // 6
System.out.println(dow);     // TUESDAY
System.out.println(len);     // 28
System.out.println(bool);    // false
System.out.println(today);   // 2018-02-06

是不是一目瞭然呢,或許都不用做過多的解釋,你就能獲取你想要的日期的某一個部分了。而且,最後一個用LocalDate.now()直接獲取的是格式化好的日期格式,而不是看起來有點凌亂的這種格式“Sat Feb 10 06:38:55 CST 2018”,是不是很優雅呢。(最後一個是判斷閏年,提醒大家,不要自己判斷閏年,但是測試的時候一定要考慮到這個規則:能被4和400整除的是閏年,能被100整除的不是閏年)。

同樣的新的日期時間JDK也提供了可讀性強的列舉型別來實現如上的程式碼,比如:

int year_value = localDate.get(ChronoField.YEAR);
int month_value = localDate.get(ChronoField.MONTH_OF_YEAR);
int day_value = localDate.get(ChronoField.DAY_OF_MONTH);

System.out.println(year_value);    // 2018
System.out.println(month_value);    // 2
System.out.println(day_value);    // 6

/*
 * YEAR(
 * "Year",
 *  ChronoUnit.YEARS,
 *  ChronoUnit.FOREVER,
 *  ValueRange
 *          .of(-999999999L,
 *                  999999999L),
 *  "year"
 */

ChronoField實現了TemporalField介面,如果深入到底層程式碼你會發現我在最後貼出的註釋樣的原始碼——列出了取值範圍——等等,大家有興趣可以參考。

LocalTime也是一樣,可以通過getHour()、getMinute()和getSecond()方法獲取小時、分鐘和秒數。

上面提到了DateFormat在多執行緒的環境下會出現意想不到的結果,新的JDK也給我們提供了新的選擇——DateTimeFormatter,所有的DateTimeFormatter例項都是執行緒安全的,因此我們可以用單例模式建立格式器例項,然後共享它。

LocalDate localDate = LocalDate.of(2018, 2, 6);
String str1 = localDate.format(DateTimeFormatter.BASIC_ISO_DATE);
String str2 = localDate.format(DateTimeFormatter.ISO_LOCAL_DATE);

LocalDate date1 = LocalDate.parse("20180206", DateTimeFormatter.BASIC_ISO_DATE);
LocalDate date2 = LocalDate.parse("2018-02-06", DateTimeFormatter.ISO_LOCAL_DATE);

System.out.println(str1);   //20180206
System.out.println(str2);   //2018-02-06

System.out.println(date1);  
//2018-02-06 ?不知道為什麼這句話列印的不是期望的20180206,大家可以幫我看看,我也會在後續的部落格中更新調查結果。
System.out.println(date2);  //2018-02-06

正如程式碼顯示的可以使用format方法按照DateTimeFormatter提供的靜態成員變數取值進行想要的日期格式化,使用parse獲取期望格式的LocalDate物件。

最後,介紹一下對日期和時間的操縱,也是對開篇提到的那段凌亂程式碼的一個交代。新的JDK提供了對日子和月份加減的優化處理,可讀性和簡潔性非常棒。

LocalDate localDate = LocalDate.of(2018, 2, 6);
LocalDate localDate1 = localDate.withYear(2019);                        //年份修改為2019
LocalDate localDate2 = localDate1.withDayOfMonth(25);                   //日改為25
LocalDate localDate3 = localDate2.with(ChronoField.MONTH_OF_YEAR, 9);   //月份改為9

LocalDate localDate4 = localDate3.plusWeeks(1);                         //此時的日期是2019-09-25,在此基礎上增加一週是2019-10-02
LocalDate localDate5 = localDate4.minusYears(3);                        //減去三年 2016-10-02
LocalDate localDate6 = localDate5.plus(6, ChronoUnit.MONTHS);           //加上六個月2017-04-02

/* 使用TemporalAdjuster進行更復雜的日期調整 */
//獲取以2017-04-02為基準,第一個符合指定星期幾要求的日期,2017-04-02就是星期日,程式會直接返回該物件
LocalDate localDate7 = localDate6.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY)); 
//獲取2017-04-02所處月份的最後一天,同步取值還有lastDayOfNextMonth/firstDayOfMonth/firstDayOfNextMonth,等等
LocalDate localDate8 = localDate7.with(TemporalAdjusters.lastDayOfMonth()); 


System.out.println(localDate);  //2018-02-06
System.out.println(localDate1); //2019-02-06
System.out.println(localDate2); //2019-02-25
System.out.println(localDate3); //2019-09-25
System.out.println(localDate4); //2019-10-02
System.out.println(localDate5); //2019-10-02
System.out.println(localDate6); //2017-04-02
System.out.println(localDate7); //2017-04-02
System.out.println(localDate8); //2017-04-30

最後的最後,再嘮叨兩句:
首先,新的JDK處理日期和時間的方式是在JDK8的前提下支援的,如果你的程式碼還是JDK7或者以下,那麼你只能小心的使用Calendar了;
其次,這裡針對新的日期和時間處理方式介紹的還不夠詳細,比如JDK8對日期和時間的處理同時還支援處理不同的時區和曆法、利用和UTC/格林尼治時間的固定偏差計算時區以及使用別的日曆系統,類似伊斯蘭教日曆這種複雜的日曆。我準備再單獨開一篇部落格專門介紹,並且提供幾個成熟且常用的工具類方法,這次就寫到這裡了,還要去加班 :)
最後,真的希望光大的程式設計師小夥伴看看英文,因為再讀原始碼的時候你真的可以省去很多時間,同時也會增加你的耐心,因為對英文頭大的同學會看不下去。同時,對於輸出結果,比如很簡單的Mar和May,英文的熟悉能夠幫你迅速的解除類似的混淆,尤其是在焦急的解決bug的時候,我就曾經遇到過把Mar當成May對著輸出結果發呆的小夥伴。

真心謝謝你能看到這裡,其中的錯漏之處還望海涵,我會繼續努力的!