7.4.2 Calendar类

7.4.2 Calendar类

因为Date类在设计上存在一些缺陷,所以Java提供了Calendar类来更好地处理日期和时间Calendar是一个抽象类,它用于表示日历。

推荐使用公历

历史上有着许多种纪年方法,它们的差异实在太大了,为了统一计时,全世界通常选择最普及、最通用的日历:Gregorian Calendar,也就是日常介绍年份时常用的”公元几几年”。

公历GregorianCalendar类

Calendar类本身是一个抽象类,它是所有日历类的模板,并提供了一些所有日历通用的方法;但它本身不能直接实例化,程序只能创建Calendar子类的实例,Java本身提供了一个GregorianCalendar类,个代表格里高利日历的子类,它代表了通常所说的公历.

可以知己扩展Calendar类

当然,也可以创建自己的Calendar子类,然后将它作为Calendar对象使用(这就是多态)。在Internet上,也有对中国农历的实现。本章不会详细介绍如何扩展Calendar子类,读者可以查看上述Calendar的源码来学习。

通过getInstance方法创建Calendar实例

Calendar类是一个抽象类,所以不能使用构造器来创建Calendar对象。但它提供了几个静态getInstance()方法来获取Calendar对象,这些方法根据TimeZone, Locale类来获取特定的Calendar,如果不指定TimeZoneLocale,则使用默认的TimeZoneLocale来创建Calendar

Calendar和Date的相互转换

CalendarDate都是表示日期的工具类,它们直接可以自由转换,如下代码所示:

通过Calendar的getTime方法直接取出Date

通过Calendar实例的getTime方法即可取得Date对象:

1
2
3
4
//创建一个默认的 Calendar对象
Calendar calendar=Calendar.getInstance();
/从 Calendar对象中取出Date对象
Date date=calendar.getTime();

先创建Calendar实例,再通过setTime方法将Date设置到Calendar实例中

因为Calendar/GregorianCalendar没有构造函数可以接收Date对象,所以必须先获得一个calendar实例,然后调用其setTime(Date date)方法将Date对象中的时间传给Calendar实例.

1
2
3
//通过Date对象获得对应的Calendar对象
Calendar calendar=Calendar.getInstance();
calendar. setTime(new Date());

Calendar类常用方法

Calendar类提供了大量访问、修改日期时间的方法,常用方法如下:

方法 描述
void add(int field, int amount) 根据日历的规则,为给定的日历字段添加或减去指定的时间量。
int get(int field) 返回指定日历字段的值。
int getActualMaximum(int field) 返回指定日历字段可能拥有的最大值。例如月,最大值为11
int getActualMinimum(int field) 返回指定日历字段可能拥有的最小值。例如月,最小值为0。
void roll( int field, int amount) add()方法类似,区别在于加上amount后超过了该字段所能表示的最大范围时,**也不会向上一个字段进位**。
void set(int field, int value) 将给定的日历字段设置为给定值。
roid set(int year, int month, int date) 设置Calendar对象的年、月、日三个字段的值。
void set(int year, int month, int date, int hourOfDay, int minute, int second) 设置Calendar对象的年、月、日、时、分、秒6个字段的值。

上面的很多方法都需要一个int类型的field参数,fieldCalendar类的类变量,如:Calendar.YEARCalendar.MONTH等分别代表了年、月、日、小时、分钟、秒等时间字段。
需要指出的是, Calendar.MONTH字段代表月份,月份的起始值不是1,而是0,所以要设置8月时,用7而不是8

如下程序示范了Calendar类的常规用法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 获取今天的日历
Calendar c = Calendar.getInstance();
// 取出年
System.out.println(c.get(YEAR));
// 取出月份
System.out.println(c.get(MONTH));
// 取出日
System.out.println(c.get(DATE));
System.out.println("-------------------------");
// 分别设置年、月、日、小时、分钟、秒
c.set(2003, 10, 23, 12, 32, 23); // 2003-11-23 12:32:23
System.out.println(c.getTime());
// 将Calendar的年前推1年
c.add(YEAR, -1); // 2002-11-23 12:32:23
System.out.println(c.getTime());
System.out.println("-------------------------");
// 将Calendar的月前推8个月
c.roll(MONTH, -8); // 2002-03-23 12:32:23
System.out.println(c.getTime());

Calendar类还有如下几个注意点。

1. add与roll的区别

add方法

add(int field, int amount)的功能非常强大,add主要用于改变Calendar的特定字段的值。

  • 如果需要增加某字段的值,则让amount为正数;
  • 如果需要减少某字段的值,则让amount为负数即可

add方法的规则

add(int field, int amount)有如下两条规则。

  1. 当被修改的字段超出它允许的范围时,会发生进位,即上一级字段也会增大.
    1
    2
    3
    4
    5
    6
    System.out.println("-------------------------");
    Calendar cal1 = Calendar.getInstance();
    cal1.set(2003, 7, 23, 0, 0, 0); // 2003-8-23
    System.out.println(cal1.getTime());
    cal1.add(MONTH, 6); // 2003-8-23 => 2004-2-23
    System.out.println(cal1.getTime());
  2. 如果下一级字段也需要改变,那么该字段会修正到变化最小的值.
    1
    2
    3
    4
    5
    6
    System.out.println("-------------------------");
    Calendar cal2 = Calendar.getInstance();
    cal2.set(2003, 7, 31, 0, 0, 0); // 2003-8-31
    // 因为进位到后月份改为2月,2月没有31日,自动变成29日
    cal2.add(MONTH, 6); // 2003-8-31 => 2004-2-29
    System.out.println(cal2.getTime());

对于上面的例子,8-31就会变成2-29。因为MONTH的下一级字段是DATE,从31到29改变最小。所以上面2003-8-31的MONTH字段增加6后,不是变成2004-3-2,而是变成2004-2-29。

roll方法的规则

roll()的规则与add()的处理规则不同:当被修改的字段超出它允许的范围时,上一级字段不会增大

1
2
3
4
5
6
System.out.println("-------------------------");
Calendar cal3 = Calendar.getInstance();
cal3.set(2003, 7, 23, 0, 0, 0); // 2003-8-23
// MONTH字段“进位”,但YEAR字段并不增加
cal3.roll(MONTH, 6); // 2003-8-23 => 2003-2-23
System.out.println(cal3.getTime());

下一级字段的处理规则与add()方法相似:

1
2
3
4
5
6
7
System.out.println("-------------------------");
Calendar cal4 = Calendar.getInstance();
cal4.set(2003, 7, 31, 0, 0, 0); // 2003-8-31
// MONTH字段“进位”后变成2,2月没有31日,
// YEAR字段不会改变,2003年2月只有28天
cal4.roll(MONTH, 6); // 2003-8-31 => 2003-2-28
System.out.println(cal4.getTime());

2. 设置Calendar的容错性

调用Calendar对象的set()方法来改变指定时间字段的值时,有可能传入一个不合法的参数

setLenient方法

Calendar提供了一个setLeniente(false)用于设置它的容错性, Calendar默认支持较好的容错性,通过setLenient(false)可以关闭Calendar的容错性,让它进行严格的参数检查。

两种解释日历的模式

Calendar有两种解释日历字段的模式:lenient模式和non-lenient模式。
Calendar处于lenient模式时,每个时间字段可接受超出它允许范围的值;
Calendar处于non-lenient模式时,如果为某个时间字段设置的值超出了它允许的取值范围,程序将会抛出异常。

例如为MONTH字段设置13,这将会导致怎样的后果呢?看如下程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.util.*;
import static java.util.Calendar.*;

public class LenientTest {
public static void main(String[] args) {
Calendar cal = Calendar.getInstance();
// 结果是YEAR字段加1,MONTH字段为1(二月)
cal.set(MONTH, 13); // ①
System.out.println(cal.getTime());

// 关闭容错性
cal.setLenient(false);
// 导致运行时异常
cal.set(MONTH, 13); // ②
System.out.println(cal.getTime());
}
}

上面程序①②两处的代码完全相似,但它们运行的结果不一样:
①处代码可以正常运行,因为设置MONTH字段的值为13,将会导致YEAR字段加1;
②处代码将会导致运行时异常,因为设置的MONTH字段值超出了MONTH字段允许的范围。

3. set()方法延迟修改

set(field, value)方法将日历字段field更改为value,此外它还设置了一个内部成员变量,以指示日历字段field已经被更改。尽管日历字段field是立即更改的,但Calendar所代表的时间却不会立即修改,直到下次调用get()getTime()getTimeInMillis()add()roll()时才会重新计算日历的时间。这被称为set方法的延迟修改,采用延迟修改的优势是多次调用set()不会触发多次不必要的计算。

下面程序演示了set()方法延迟修改的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.*;
import static java.util.Calendar.*;

public class LazyTest {
public static void main(String[] args) {
Calendar cal = Calendar.getInstance();
cal.set(2003, 7, 31); // 2003-8-31
// 将月份设为9,但9月31日不存在。
// 如果立即修改,系统将会把cal自动调整到10月1日。
cal.set(MONTH, 8);

// 下面代码输出10月1日
System.out.println(cal.getTime()); //①

// 设置DATE字段为5
cal.set(DATE, 5); // ②
System.out.println(cal.getTime()); // ③
}
}

上面程序中创建了代表2003-8-31的Calendar对象,当把这个对象的MONTH字段加1后应该得到2003-10-1(因为9月没有31日),如果程序在①号代码处输出当前Calendar里的日期,也会看到输出2003-10-1,③号代码处将输出2003-10-5

如果程序将①处代码注释起来,因为Calendarset()方法具有延迟修改的特性,即调用set()方法后Calendar实际上并未计算真实的日期,它只是使用内部成员变量表记录MONTH字段被修改为8,接着程序设置DATE字段值为5,程序内部再次记录DATE字段为5—就是9月5日,因此看到③处输出2003-9-5。