求任意一天的前一个月的月末
set()实现
public static void main(String[] args) throws ParseException {
String param = "2019-12-30";
SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd");
Date cdate = sf.parse(param);
Calendar calendar = Calendar.getInstance();
calendar.setTime(cdate);
int month = calendar.get(Calendar.MONTH);
// calendar.add(Calendar.MONTH, -1);
calendar.set(Calendar.MONTH, month - 1);
int lastDay = calendar.getActualMaximum(Calendar.DAY_OF_MONTH);
System.out.println(lastDay);
calendar.set(Calendar.DAY_OF_MONTH, lastDay);
System.out.println("上个月最后一天:" + sf.format(calendar.getTime()));
}
执行后输出如下
结果好像是对的,接下来换下参数看下结果
问题出现了,为什么“2019-12-31”的上一个月的月末是“2019-12-01”
rt.jar 里的Calendar.java 有个属性fields,是保存“年、月、日、星期、时、分、秒”这些信息,一共是17个字段。
/**
* The calendar field values for the currently set time for this calendar.
* This is an array of <code>FIELD_COUNT</code> integers, with index values
* <code>ERA</code> through <code>DST_OFFSET</code>.
* @serial
*/
@SuppressWarnings("ProtectedField")
protected int fields[];
可以通过debug,来一步步观察fields值的变化。
- Calendar calendar = Calendar.getInstance() 执行后fields 为[1, 2020, 0, 2, 2, 7, 7, 3, 1, 0, 10, 10, 12, 59, 895, 28800000, 0],这是系统时间,表示“2020-01-07”
- calendar.setTime(cdate) 执行后 fields 为[1, 2019, 11, 1, 5, 31, 365, 3, 5, 0, 0, 0, 0, 0, 0, 28800000, 0],表示“2019-12-31”
- calendar.set(Calendar.MONTH, month - 1) 执行后fields 为[1, 2019, 10, 1, 5, 31, 365, 3, 5, 0, 0, 0, 0, 0, 0, 28800000, 0] ,表示“2019-11-31”,实现上2019年11月没有31号
- calendar.getActualMaximum(Calendar.DAY_OF_MONTH) 执行后fields 为[1, 2019, 10, 1, 5, 31, 365, 3, 5, 0, 0, 0, 0, 0, 0, 28800000, 0],和第三步一样。因为2019年11月没有31号,实现上是计算的“2019-12-01”所在月的最大天数为31天
gc 是当前 calendar 示例是克隆,源码如下,经过gc.complete() 后, gc的fields为[1, 2019, 11, 49, 1, 1, 335, 1, 1, 0, 0, 0, 0, 0, 0, 28800000, 0]
private GregorianCalendar getNormalizedCalendar() {
GregorianCalendar gc;
if (isFullyNormalized()) {
gc = this;
} else {
// Create a clone and normalize the calendar fields
gc = (GregorianCalendar) this.clone();
gc.setLenient(true);
gc.complete();
}
return gc;
}
5.calendar.set(Calendar.DAY_OF_MONTH, lastDay) 执行后 fields 为[1, 2019, 10, 1, 5, 31, 365, 3, 5, 0, 0, 0, 0, 0, 0, 28800000, 0]
6.calendar.getTime() 执行后,由于调用了computeTime()方法,fields 变为了[1, 2019, 11, 49, 1, 1, 335, 1, 1, 0, 0, 0, 0, 0, 0, 28800000, 0],所以输出了“2019-12-01”
add()实现
问题点:为什么计算“2019-12-31”上一个月的月末,set()方法算出的是“2019-12-01”,而add()方法算出的是“2019-11-30”
从GregorianCalendar.java的add方法源码可以看出,add方法实际上多次调用set方法实现的。在此示例中,先set 月份,再通过monthLength(int month, int year)方法查询中实际天数,最后set天数
@Override
public void add(int field, int amount) {
// If amount == 0, do nothing even the given field is out of
// range. This is tested by JCK.
if (amount == 0) {
return; // Do nothing!
}
if (field < 0 || field >= ZONE_OFFSET) {
throw new IllegalArgumentException();
}
// Sync the time and calendar fields.
complete();
if (field == YEAR) {
int year = internalGet(YEAR);
if (internalGetEra() == CE) {
year += amount;
if (year > 0) {
set(YEAR, year);
} else { // year <= 0
set(YEAR, 1 - year);
// if year == 0, you get 1 BCE.
set(ERA, BCE);
}
}
else { // era == BCE
year -= amount;
if (year > 0) {
set(YEAR, year);
} else { // year <= 0
set(YEAR, 1 - year);
// if year == 0, you get 1 CE
set(ERA, CE);
}
}
pinDayOfMonth();
} else if (field == MONTH) {
int month = internalGet(MONTH) + amount;
int year = internalGet(YEAR);
int y_amount;
if (month >= 0) {
y_amount = month/12;
} else {
y_amount = (month+1)/12 - 1;
}
if (y_amount != 0) {
if (internalGetEra() == CE) {
year += y_amount;
if (year > 0) {
set(YEAR, year);
} else { // year <= 0
set(YEAR, 1 - year);
// if year == 0, you get 1 BCE
set(ERA, BCE);
}
}
else { // era == BCE
year -= y_amount;
if (year > 0) {
set(YEAR, year);
} else { // year <= 0
set(YEAR, 1 - year);
// if year == 0, you get 1 CE
set(ERA, CE);
}
}
}
if (month >= 0) {
set(MONTH, month % 12);
} else {
// month < 0
month %= 12;
if (month < 0) {
month += 12;
}
set(MONTH, JANUARY + month);
}
pinDayOfMonth();
} else if (field == ERA) {
int era = internalGet(ERA) + amount;
if (era < 0) {
era = 0;
}
if (era > 1) {
era = 1;
}
set(ERA, era);
} else {
long delta = amount;
long timeOfDay = 0;
switch (field) {
// Handle the time fields here. Convert the given
// amount to milliseconds and call setTimeInMillis.
case HOUR:
case HOUR_OF_DAY:
delta *= 60 * 60 * 1000; // hours to minutes
break;
case MINUTE:
delta *= 60 * 1000; // minutes to seconds
break;
case SECOND:
delta *= 1000; // seconds to milliseconds
break;
case MILLISECOND:
break;
// Handle week, day and AM_PM fields which involves
// time zone offset change adjustment. Convert the
// given amount to the number of days.
case WEEK_OF_YEAR:
case WEEK_OF_MONTH:
case DAY_OF_WEEK_IN_MONTH:
delta *= 7;
break;
case DAY_OF_MONTH: // synonym of DATE
case DAY_OF_YEAR:
case DAY_OF_WEEK:
break;
case AM_PM:
// Convert the amount to the number of days (delta)
// and +12 or -12 hours (timeOfDay).
delta = amount / 2;
timeOfDay = 12 * (amount % 2);
break;
}
// The time fields don't require time zone offset change
// adjustment.
if (field >= HOUR) {
setTimeInMillis(time + delta);
return;
}
// The rest of the fields (week, day or AM_PM fields)
// require time zone offset (both GMT and DST) change
// adjustment.
// Translate the current time to the fixed date and time
// of the day.
long fd = getCurrentFixedDate();
timeOfDay += internalGet(HOUR_OF_DAY);
timeOfDay *= 60;
timeOfDay += internalGet(MINUTE);
timeOfDay *= 60;
timeOfDay += internalGet(SECOND);
timeOfDay *= 1000;
timeOfDay += internalGet(MILLISECOND);
if (timeOfDay >= ONE_DAY) {
fd++;
timeOfDay -= ONE_DAY;
} else if (timeOfDay < 0) {
fd--;
timeOfDay += ONE_DAY;
}
fd += delta; // fd is the expected fixed date after the calculation
int zoneOffset = internalGet(ZONE_OFFSET) + internalGet(DST_OFFSET);
setTimeInMillis((fd - EPOCH_OFFSET) * ONE_DAY + timeOfDay - zoneOffset);
zoneOffset -= internalGet(ZONE_OFFSET) + internalGet(DST_OFFSET);
// If the time zone offset has changed, then adjust the difference.
if (zoneOffset != 0) {
setTimeInMillis(time + zoneOffset);
long fd2 = getCurrentFixedDate();
// If the adjustment has changed the date, then take
// the previous one.
if (fd2 != fd) {
setTimeInMillis(time - zoneOffset);
}
}
}
}
总结
1、add() 有两条规则:
a)当被修改的字段超出它的取值范围时,那么比它大的字段会自动修正。
b)如果比它小的字段是不可变的/不在取值范围内(由 Calendar 的实现类决定),那么该小字段会修正到变化最小的值。
2、Set()
比被修改的字段大的字段会根据字段是增大还是减小自动改变大小,比被修改字段小的字段如果是不可变的/不在取值范围内,会自动增大到变化最小的值。