Calendar類set()方法的“陷阱”
在專案中,需要獲取指定年份和月份的最後一天。我在網上找到了一個用Calendar類獲取的方法,程式碼如下:
import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; public class TestCalendar { public static void main(String[] args) { String s = new SimpleDateFormat("yyyy-MM-dd") .format(getLastDay(2017, 9)); System.out.println(s); } public static Date getLastDay(int year, int month) { Calendar c = Calendar.getInstance(); //獲取Calendar類的例項 c.set(Calendar.YEAR, year); //設定年份 c.set(Calendar.MONTH, month - 1); //設定月份,因為月份從0開始,所以用month - 1 int lastDay = c.getActualMaximum(Calendar.DAY_OF_MONTH); //獲取當前時間下,該月的最大日期的數字 c.set(Calendar.DAY_OF_MONTH, lastDay); //將獲取的最大日期數設定為Calendar例項的日期數 return c.getTime(); //返回日期 } }
剛開始使用這個方法的時候,很正常。後來在10月31號(這個日期很重要)當天測試的時候,傳遞的引數時2017年9月,即上面的程式碼,但是結果卻出現的了問題,結果如下圖:
本來該是2017-09-30,可是結果卻是2017-10-01,我原先測試過,這個方法是沒有問題的,可是出了這樣的問題。後來我斷點測試,在剛獲取到Calendar例項的時候,例項中的欄位值如下圖:
但是發現在執行完
c.set(Calendar.MONTH, month - 1);
這行的程式碼的時候,Calendar的例項中,MONTH欄位的值不是我預想中的8(月份欄位從0開始,因此傳入9應該減去1),而是9,而且DAY_OF_MONTH欄位的值從31變成了1,如下圖所示:因此,可以判斷Calendar例項獲取到的時候,是10月31號,例項中的DAY_OF_MONTH的值是31,當把MONTH欄位的值設定為8後,因為9月份只有30天,那DAY_OF_MONTH的值就多1,會自動向後順延1天,變成了2017-10-01 。
但是,還是有其他的問題,因為下面還執行了
c.set(Calendar.DAY_OF_MONTH, lastDay);
這句程式碼,最後的日期應該是2017-10-31才對,但是run的結果卻是2017-10-01,debug的結果是2017-10-31
。
我第一感覺認為Calendar類是不是存線上程安全問題,可是後來一想就覺得不對,畢竟我只是在主執行緒中執行,沒有多執行緒,並不存在這個問題。
第二天我有嘗試了下,發現了問題的原因,如上面的最後一張圖所示,在debug的過程中,我用IDEA的watches功能查看了Calendar例項的欄位值,用了get()方法,如果我刪除掉這幾個get方法之後,發現run和debug的值是一樣的,都是2017-10-01,說明問題出在get()方法上。
因此,可以做如下修改:
在程式碼中,直接列印變數c的值,可以發現,在呼叫get()方法之前,變數c的各欄位值是set()方法設定的,但是並沒有對其進行驗證計算,在呼叫get()方法的過程中,會對各欄位驗證計算。我查看了部分原始碼,在呼叫get(),add(),getTime()等方法的過程中,底層都會呼叫computeTime()方法,對各欄位的時間驗證計算。
另外,又做了一個demo測試,以佐證上面的結論,如下:
import java.text.SimpleDateFormat;
import java.util.Calendar;
public class TestCalendar2 {
public static void main(String[] args) {
Calendar c = Calendar.getInstance();
c.set(Calendar.MONTH, 8); //將月份設定為9月
c.set(Calendar.DAY_OF_MONTH, 32); //將日期設定為32
System.out.println(c); //直接列印Calendar例項,不使用getTime()方法
c.get(Calendar.MONTH);
System.out.println(c);
}
}
結果如下:
即使設定的DAY_OF_MONTH值是明顯非法的,但是並不會在呼叫get()方法之前進行計算進位。
在查詢問題的過程中,也看到了其他的一些問題,這篇文章對add(),set(),roll()方法的區別做了解釋:
回到最初的問題,獲取指定年份和月份的最大的日期的方法要怎麼辦?
方法可以改為:
public static Date getLastDay(int year, int month) {
Calendar c = Calendar.getInstance(); //獲取Calendar類的例項
c.clear();
c.set(Calendar.YEAR, year); //設定年份
c.set(Calendar.MONTH, month - 1); //設定月份,因為月份從0開始,所以用month - 1
int lastDay = c.getActualMaximum(Calendar.DAY_OF_MONTH); //獲取當前時間下,該月的最大日期的數字
c.set(Calendar.DAY_OF_MONTH, lastDay); //將獲取的最大日期數設定為Calendar例項的日期數
return c.getTime(); //返回日期
}
用clear()方法,將Calendar例項的欄位和時間都設定為未定義,這樣可以解決這個問題。
當然網上也有將月份設定為下個月,然後用add(Calendar.DAY_OF_MONTH, -1)這樣的方法也可以得到結果,不過這裡就不詳細介紹了。