你可能不知道的 `Calendar` 之 `DAY_OF_WEEK`


## 1.背景:

百度知道上有人咨询了 `Calendar 设置 Calendar.DAY_OF_WEEK不正确的问题` 参见 http://zhidao.baidu.com/question/748936560786113052

> 他的入职时间是 `"2006-02-14"`,要计算 `20年后的所在周的周六`

理论上, `2026-02-14`所在周的周六,正好就是 `2026-02-14`

![](http://i.imgur.com/GxNW6Yw.png)

他写的代码是

```JAVA

public class Test {
	public static void main(String[] args) throws ParseException {
		SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd");
		Date date=sdf.parse("2006-02-14");
		System.out.println(sdf.format(date));

		//********************************************************************
		Calendar c=Calendar.getInstance();
		c.setTime(date);
		c.add(Calendar.YEAR, 20);
		c.set(Calendar.DAY_OF_WEEK, Calendar.SATURDAY);
		//********************************************************************

		System.out.println("入职20周年纪念日派对日期:"+sdf.format(c.getTime()));
	}
}

```

看上去没毛病,但是结果死活就是 `2026-02-21`


但是,在中间插入一行代码  `System.out.println(c.get(Calendar.DAY_OF_WEEK));`

```JAVA

public class Test {
	public static void main(String[] args) throws ParseException {
		SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd");
		Date date=sdf.parse("2006-02-14");
		System.out.println(sdf.format(date));


		//********************************************************************
		Calendar c=Calendar.getInstance();
		c.setTime(date);
		c.add(Calendar.YEAR, 20);
		System.out.println(c.get(Calendar.DAY_OF_WEEK));//有就正确,没有就错误:输出2026-02-21

		c.set(Calendar.DAY_OF_WEEK, Calendar.SATURDAY);  //正确日期2026-02-14
		//********************************************************************

		System.out.println("入职20周年纪念日派对日期:"+sdf.format(c.getTime()));
	}
}

```

结果就是正确的 `2026-02-14`

是不是很神奇?颠覆世界观,觉得不可思议?


## 2.原因

原因在于,**当你设置 `DAY_OF_WEEK` 的时候, 你需要设置  `WEEK_OF_MONTH` 或者 `DAY_OF_WEEK_IN_MONTH`  或者 `WEEK_OF_YEAR`, 否则会使用老的 `WEEK_OF_MONTH` 字段**

> When computing time (milliseconds), GregorianCalendar leaves some fields inconsistent. 
> Then, after the last set(DAY_OF_WEEK), an invalid (older) WEEK_OF_MONTH value is used in the last getTime() call.
> 
> When you set DAY_OF_WEEK, the calendar expects a week field (`WEEK_OF_MONTH`, `DAY_OF_WEEK_IN_MONTH` or `WEEK_OF_YEAR`) has also been set. 
> So, avoid setting DAY_OF_WEEK without setting one of the week fields.


原先的这段代码 

```JAVA

public class Test {
	public static void main(String[] args) throws ParseException {
		SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd");
		Date date=sdf.parse("2006-02-14");
		System.out.println(sdf.format(date));

		//********************************************************************
		Calendar c=Calendar.getInstance();
		c.setTime(date);
		c.add(Calendar.YEAR, 20);
		c.set(Calendar.DAY_OF_WEEK, Calendar.SATURDAY);
		//********************************************************************

		System.out.println("入职20周年纪念日派对日期:"+sdf.format(c.getTime()));
	}
}

```

由于只设置了  `Calendar.DAY_OF_WEEK` ,那么计算的时间结果是 **20年后的第3周的周六** (因为没有设置`WEEK_OF_MONTH`,那么取的是老的"2006-02-14" 的`WEEK_OF_MONTH` )
而 "2006-02-14" 是所在月的第3周, 那么 **20年后的第3周的周六** 结果就是  `2026-02-21` 了 

 
![](http://i.imgur.com/ogukWEC.png)

## 3. 为毛中间插入了一段 `System.out.println(c.get(Calendar.DAY_OF_WEEK));` 结果就正确了呢?

我在问题里面也回答了, 

- 换成 `System.out.println(c.get(Calendar.ERA));` 结果也会对
- 换成 `System.out.println("啦啦啦啦:" + sdf.format(c.getTime()));` 结果也对
	![](http://i.imgur.com/sGDBDj9.png)

**原因在于**,
调用这些方法的时候 ,`Calendar` 会调用 `java.util.Calendar.complete()` 方法, 
这个方法会依照已经设置的参数,把 Calendar 的 17 个字段(包括 `WEEK_OF_MONTH` ) 都重新计算一下 ,

此时,  "2006-02-14" 20年后的日期是  2026-02-14 , 他的 `WEEK_OF_MONTH` 是当月的第二周, 都算完成了

![](http://i.imgur.com/V2EOT0A.png)

![](http://i.imgur.com/9LeKOP6.png)


## 4.最佳实践 (推荐指南)

真心不建议自己来写 Calendar SimpleDateFormat 来操作 ,坑比较多. 

建议使用 `apache commons-lang3` jar 或者 `joda-time`

**示例: 对我来说就三步**

 
```JAVA
 
public static void main(String[] args) throws ParseException{
    //1.转成date
    Date date = DateUtils.parseDate("2006-02-14", "yyyy-MM-dd");

    //2.20年后的日期
    Date d20 = DateUtils.addYears(date, 20);

    //3.20年后的日期所在周的 周六 ,
    Calendar calendar = DateUtils.toCalendar(d20);
    calendar.set(DAY_OF_WEEK, SATURDAY);
    System.out.println("入职20周年纪念日派对日期:" + DateFormatUtils.format(calendar.getTime(), "yyyy-MM-dd"));
}

```


你也可以使用 [feilong-core jar](https://github.com/venusdrogon/feilong-core) 

**示例: 对我来说就三步**

 
```JAVA
 
    public static void main(String[] args){
        //1.转成date
        Date date2 = DateUtil.toDate("2006-02-14", DatePattern.COMMON_DATE);

        //2.20年后的日期
        Date d20 = DateUtil.addYear(date2, 20);

        //3.20年后的日期所在周的 周六
        System.out.println("入职20周年纪念日派对日期:" + DateUtil.toString(getLastDateOfThisWeek(d20), DatePattern.COMMON_DATE));
    }

```


## 5.参考

- http://zhidao.baidu.com/question/748936560786113052
- http://bugs.java.com/view_bug.do?bug_id=4655637