Java Calendar 类解析

前言

最近在处理关于时间格式化的问题时,用到了 Calendar 类,踩了一些比较低级的坑,感慨虽然是一个 Calendar 类,也有很多要留意的地方,而以前一直没有比较规范的了解过这个类,这次刚好对它深入了解一番

概念

是一个抽象类,可以派生出各种日历,除了使用默认的 GregorianCalendar(格里高利历),还可以自己实现各种其他历法

1
2
3
Calendar iso8601 = new Calendar.Builder().setCalendarType("iso8601").build();
Calendar.getAvailableCalendarTypes()
// return [gregory]

gregory 和 iso8601:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
*设置GregorianCalendar更改日期。 这是从朱利安日期到格里高利日期的转换发生的时刻。 默认值是1582年10月15日(格里高利)。在此之前,日期将在儒略历中。
*要获取纯Julian日历,请将更改日期设置为Date(Long.MAX_VALUE)。 要获得纯公历,请将更改日期设置为日期(Long.MIN_VALUE)。
*/
public void setGregorianChange(Date date) {
long cutoverTime = date.getTime();
if (cutoverTime == gregorianCutover) {
return;
}
// Before changing the cutover date, make sure to have the
// time of this calendar.
complete();
setGregorianChange(cutoverTime);
}

// 设定一年中第一周所需的最小天数; 例如,如果第一周定义为包含一年中第一个月的第一天的那一周,则使用值1调用此方法。如果它必须是整周,则使用值7。
calendar.setMinimalDaysInFirstWeek(1);

ISO 8601

基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 通过getInstance()获取实例,并不是单例,而是每次都new一个新的GregorianCalendar对象,可以把这个对象存起来,避免每次都新建
private val calendar = Calendar.getInstance() // 获得默认的时区和语言地区,即从系统中取
private val calendar = Calendar.getInstance(TimeZone zone, Locale aLocale)

/**
* Gets a calendar with the specified time zone and locale.
* The <code>Calendar</code> returned is based on the current time
* in the given time zone with the given locale.
*
* @param zone the time zone to use
* @param aLocale the locale for the week data
* @return a Calendar.
*/
public static Calendar getInstance(TimeZone zone, Locale aLocale) {
return createCalendar(zone, aLocale);
}

private static Calendar createCalendar(TimeZone zone, Locale aLocale) {
// BEGIN Android-changed: only support GregorianCalendar here
return new GregorianCalendar(zone, aLocale);
// END Android-changed: only support GregorianCalendar here
}

/**
* Constructs a <code>GregorianCalendar</code> based on the current time
* in the given time zone with the given locale.
*
* @param zone the given time zone.
* @param aLocale the given locale.
*/
public GregorianCalendar(TimeZone zone, Locale aLocale) {
super(zone, aLocale);
gdate = (BaseCalendar.Date) gcal.newCalendarDate(zone);
setTimeInMillis(System.currentTimeMillis());
}

// 创建一个 Date
val date = Date(1554362004L)

这里的 Date 类还有其他构造方法,不过已经被弃用了,现在通常都是用 Date(long: Long) 这个构造方法,直接传入时间戳,方便相互转化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 传入一个Date类型
calendar.time = date

/**
* Sets the values for the fields <code>YEAR</code>, <code>MONTH</code>,
* <code>DAY_OF_MONTH</code>, <code>HOUR_OF_DAY</code>, <code>MINUTE</code>, and
* <code>SECOND</code>.
* Previous values of other fields are retained. If this is not desired,
* call {@link #clear()} first.
*
* @param year the value used to set the <code>YEAR</code> calendar field.
* @param month the value used to set the <code>MONTH</code> calendar field.
* Month value is 0-based. e.g., 0 for January.
* @param date the value used to set the <code>DAY_OF_MONTH</code> calendar field.
* @param hourOfDay the value used to set the <code>HOUR_OF_DAY</code> calendar field.
* @param minute the value used to set the <code>MINUTE</code> calendar field.
* @param second the value used to set the <code>SECOND</code> calendar field.
* @see #set(int,int)
* @see #set(int,int,int)
* @see #set(int,int,int,int,int)
*/
public final void set(int year, int month, int date, int hourOfDay, int minute, int second) {
set(YEAR, year);
set(MONTH, month);
set(DATE, date);
set(HOUR_OF_DAY, hourOfDay);
set(MINUTE, minute);
set(SECOND, second);
}

calendar.set(2019,4,8);

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public void set(int field, int value) {
// If the fields are partially normalized, calculate all the
// fields before changing any fields.
if (areFieldsSet && !areAllFieldsSet) {
computeFields();
}
internalSet(field, value);
isTimeSet = false;
areFieisSet[field] = true;
ldsSet = false;
stamp[field] = nextStamp++;
if (nextStamp == Integer.MAX_VALUE) {
adjustStamp();
}
}

SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd:hh:mm:ss");
Calendar calendar2 = Calendar.getInstance();
calendar2.set(2003, 7, 31);
Log.d("textqqq", simpleDateFormat.format(calendar2.getTime()));
calendar2.set(Calendar.MONTH, 8);
//Log.d("textqqq", simpleDateFormat.format(calendar2.getTime()));
calendar2.set(Calendar.DATE, 5);
Log.d("textqqq", simpleDateFormat.format(calendar2.getTime()));

// 注释掉的 Log 是
// 2019-04-08 15:39:16.484 24647-24647/com.example.calendartest D/textqqq: 2003-08-31:03:39:16
// 2019-04-08 15:39:16.485 24647-24647/com.example.calendartest D/textqqq: 2003-09-05:03:39:16

// 去掉注释后的 Log 是
// 2019-04-08 15:46:03.286 25362-25362/com.example.calendartest D/textqqq: 2003-08-31:03:46:03
// 2019-04-08 15:46:03.287 25362-25362/com.example.calendartest D/textqqq: 2003-10-01:03:46:03
// 2019-04-08 15:46:03.287 25362-25362/com.example.calendartest D/textqqq: 2003-10-05:03:46:03

获取不同的日期值

1
2
// 取得不同字段对应的的值
val month = calendar.get(Calendar.MONTH)

常用字段值

Field name Minimum Greatest Minimum Least Maximum Maximum
ERA 0 0 1 1
YEAR 1 1 292269054 292278994
MONTH 0 0 11 11
WEEK_OF_YEAR 1 1 52* 53
WEEK_OF_MONTH 0 0 4* 6
DAY_OF_MONTH 1 1 28* 31
DAY_OF_YEAR 1 1 365* 366
DAY_OF_WEEK 1 1 7 7
DAY_OF_WEEK_IN_MONTH -1 -1 4* 6
AM_PM 0 0 1 1
HOUR 0 0 11 11
HOUR_OF_DAY 0 0 23 23
MINUTE 0 0 59 59
SECOND 0 0 59 59
MILLISECOND 0 0 999 999
ZONE_OFFSET -13:00 -13:00 14:00 14:00
DST_OFFSET 0:00 0:00 0:20 2:00
* 根据格里高利历而变化

比较特殊的字段值

1
2
3
4
5
6
7
8
9
10
11
12
13
public final static int ERA = 0; // 时代
public final static int AD = 1; // 公元
public final static int BC = 0; // 公元前

public final static int AM_PM = 9;
public final static int AM = 0;
public final static int PM = 1;

public final static int DAY_OF_MONTH = 5; // 这两个是一样的
public final static int DATE = 5;

public final static int WEEK_OF_MONTH = 4; // 这周在当月里是第几周,以星期为标准
public final static int DAY_OF_WEEK_IN_MONTH = 8; // 这周在当月里是第几周,以日期为标准

DAY_OF_WEEK_IN_MONTH: 指示当前月中的第几个星期。与 DAY_OF_WEEK 字段一起使用时,就可以唯一地指定某月中的某一天。与 WEEK_OF_MONTHWEEK_OF_YEAR 不同,该字段的值并不取决于 getFirstDayOfWeek()getMinimalDaysInFirstWeek()DAY_OF_MONTH 1 到 7 总是对应于 DAY_OF_WEEK_IN_MONTH 1; 8 到 14 总是对应于 DAY_OF_WEEK_IN_MONTH 2,依此类推。 DAY_OF_WEEK_IN_MONTH 0 表示 DAY_OF_WEEK_IN_MONTH 1 之前的那个星期。负值是从一个月的末尾开始逆向计数,因此,一个月的最后一个星期天被指定为 DAY_OF_WEEK = SUNDAY, DAY_OF_WEEK_IN_MONTH = -1。因为负值是逆向计数的,所以它们在月份中的对齐方式通常与正值的不同。例如,如果一个月有 31 天,那么 DAY_OF_WEEK_IN_MONTH -1 将与 DAY_OF_WEEK_IN_MONTH 5 和 DAY_OF_WEEK_IN_MONTH 4 的末尾相重叠。

拿 2019 年一月来测试一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public final static int ZONE_OFFSET = 15; //获取时区,正为东,负为西
calendar.get(Calendar.ZONE_OFFSET) = 28800000 // 比世界协调时间(UTC)/格林尼治时间(GMT)快8小时的时区

public final static int DST_OFFSET = 16; //以毫秒为单位指示夏令时的偏移量,地区相关
Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("America/Los_Angeles"),Locale.US);
Date newYears = new Date(1546272000000L);
calendar.setTime(newYears);
for (;;calendar.add(Calendar.DATE, 1)) {
if (calendar.get(Calendar.DST_OFFSET) > 0) {
tvContent4.setText("" + FULL_FORMAT.format(calendar.getTime()));
break;
}
}
calendar.get(Calendar.DST_OFFSET) = 3600000
图片名称

中国也曾在1986-1991年间实行夏令时,后废除

一些细微的不同

可以留意到,一些字段取得的值是 0 开始的,比如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public final static int WEEK_OF_MONTH = 4;
public final static int MONTH = 2;

// JANUARY 0
// FEBRUARY
// MARCH
// APRIL
// MAY
// JUNE
// JULY
// AUGUST
// SEPTEMBER
// OCTOBER
// NOVEMBER
// DECEMBER 11
// UNDECIMBER 12 第十三个月

第十三个月

但是有些却是以 1 开始的,比如以下这些,使用时要加以区分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public final static int YEAR = 1;
public final static int WEEK_OF_YEAR = 3;
public final static int DAY_OF_MONTH = 5;
public final static int DAY_OF_YEAR = 6;
public final static int DAY_OF_WEEK = 7;

// SUNDAY 1
// MONDAY
// TUESDAY
// WEDNESDAY
// THURSDAY
// FRIDAY
// SATURDAY 7

// SUNDAY in the U.S, MONDAY in France.
calendar.setFirstDayOfWeek(Calendar.MONDAY);

add 和 roll 的区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/**
* Adds or subtracts the specified amount of time to the given calendar field,
* based on the calendar's rules. For example, to subtract 5 days from
* the current time of the calendar, you can achieve it by calling:
* <p><code>add(Calendar.DAY_OF_MONTH, -5)</code>.
*
* @param field the calendar field.
* @param amount the amount of date or time to be added to the field.
* @see #roll(int,int)
* @see #set(int,int)
*/
abstract public void add(int field, int amount);

/**
* Adds or subtracts (up/down) a single unit of time on the given time
* field without changing larger fields. For example, to roll the current
* date up by one day, you can achieve it by calling:
* <p>roll(Calendar.DATE, true).
* When rolling on the year or Calendar.YEAR field, it will roll the year
* value in the range between 1 and the value returned by calling
* <code>getMaximum(Calendar.YEAR)</code>.
* When rolling on the month or Calendar.MONTH field, other fields like
* date might conflict and, need to be changed. For instance,
* rolling the month on the date 01/31/96 will result in 02/29/96.
* When rolling on the hour-in-day or Calendar.HOUR_OF_DAY field, it will
* roll the hour value in the range between 0 and 23, which is zero-based.
*
* @param field the time field.
* @param up indicates if the value of the specified time field is to be
* rolled up or rolled down. Use true if rolling up, false otherwise.
* @see Calendar#add(int,int)
* @see Calendar#set(int,int)
*/
abstract public void roll(int field, boolean up);


Date newYears = new Date(1545408000000L);

calendar.setTime(newYears);
calendar.roll(Calendar.MONTH,1);

calendar.setTime(newYears);
calendar.add(Calendar.MONTH,1);

2019-04-08 16:13:03.103 31697-31697/com.example.calendartest D/textqqq: 2018-12-22:12:00:00
2019-04-08 16:13:03.103 31697-31697/com.example.calendartest D/textqqq: 2018-01-22:12:00:00
2019-04-08 16:13:03.104 31697-31697/com.example.calendartest D/textqqq: 2018-12-22:12:00:00
2019-04-08 16:13:03.104 31697-31697/com.example.calendartest D/textqqq: 2019-01-22:12:00:00

calendar.setLenient 容错性

1
2
3
4
5
6
7
calendar.set(Calendar.MONTH, 13);
System.out.println(simpleDateFormat.format(calendar.getTime()));
// 关闭容错性
// calendar.setLenient(false);

calendar.set(Calendar.MONTH, 13);
System.out.println(simpleDateFormat.format(calendar.getTime()));

当打开 calendar.setLenient(false) 时,① 处代码可以正常运行,因为将 MONTH 字段设置为 13,将会导致 YEAR 字段加 1;② 处将会导致运行异常,因为设置的 MONTH 字段超出了所允许的范围。关键在于程序中粗体字代码行, Calendar 提供一个 setLenient 用于设置它的容错性,Calendar 默认支持比较好的容错性,通过 setLenient(false) 了一关闭 Calendar 的容错性,让它进行严格的参数检查。
Calendar 有两种解释日历字段的模式:lenient 模式和 non-lenient 模式。当处于 lenient 模式时,每个字段都可接受超出范围的值。当处于 non-lenient 模式时,每个字段都进行严格的参数检查,不接收超出字段的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Calendar calendar2 = Calendar.getInstance();
calendar2.setLenient(false);
calendar2.set(2003, 7, 31);
calendar2.set(Calendar.MONTH, 8);
calendar2.set(Calendar.DATE, 5);

java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.calendartest/com.example.calendartest.MainActivity}java.lang.IllegalArgumentException: MONTH: 8 -> 9
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2778)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2856)
at android.app.ActivityThread.-wrap11(Unknown Source:0)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1589)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:164)
at android.app.ActivityThread.main(ActivityThread.java:6494)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807)
Caused by: java.lang.IllegalArgumentException: MONTH: 8 -> 9
at java.util.GregorianCalendar.computeTime(GregorianCalendar.java:2824)
at java.util.Calendar.updateTime(Calendar.java:3397)
at java.util.Calendar.getTimeInMillis(Calendar.java:1761)
at java.util.Calendar.getTime(Calendar.java:1734)
at com.example.calendartest.MainActivity.onCreate(MainActivity.java:67)
at android.app.Activity.performCreate(Activity.java:7009)
at android.app.Activity.performCreate(Activity.java:7000)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1214)

Java Calendar 类解析
https://enderhoshi.github.io/2019/04/04/Java Calendar 类解析/
作者
HoshIlIlI
发布于
2019年4月4日
许可协议