1. 程式人生 > >Calendar類set()方法的“陷阱”

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)這樣的方法也可以得到結果,不過這裡就不詳細介紹了。