Calendar类set方法坑点

求任意一天的前一个月的月末

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值的变化。

  1. 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”
  2. calendar.setTime(cdate) 执行后 fields 为[1, 2019, 11, 1, 5, 31, 365, 3, 5, 0, 0, 0, 0, 0, 0, 28800000, 0],表示“2019-12-31”
  3. 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号
  4. 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()
比被修改的字段大的字段会根据字段是增大还是减小自动改变大小,比被修改字段小的字段如果是不可变的/不在取值范围内,会自动增大到变化最小的值。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值