Calendar类使用getActualMaximum方法天坑

导读:本篇文章讲解 Calendar类使用getActualMaximum方法天坑,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

问题

Calendar使用getActualMaximum获取当月天数最后一天出现了与月份不符合的天数,而且是线上问题,小编焦头烂额,工具类有毒,大家以后调用别人方法之前先看下啊。

下面是工具类的的方法。

public static Integer queryEndDayOfMonth(Integer year, int month) {
        Calendar c = Calendar.getInstance();
   
        c.set(Calendar.YEAR, year);
        c.set(Calendar.MONTH, month);
        return c.getActualMaximum(Calendar.DAY_OF_MONTH);
    }

首先Calendar 的月份是从0到11,所以使用的时候,传入方法得默认减1,或者在方法内修改。
修改第一次:

public static queryEndDayOfMonth(Integer year, int month) {
        Calendar c = Calendar.getInstance();

        c.set(Calendar.YEAR, year);
        c.set(Calendar.MONTH, month-1);
        return c.getActualMaximum(Calendar.DAY_OF_MONTH);
    }

之后发现2月的时候,出现31天的问题。小编做了一个测试,测试日期是今天,3月31日。
测试类:

@Test
    public void testQueryEndDayOfMonth(){
        for (int i = 1; i < 13; i++) {
            System.out.println("2021年"+i+"月最后一天为:"+DateUtil.queryEndDayOfMonth(2021,i));
        }
    }

测试结果

20211月最后一天为:31
20212月最后一天为:31
20213月最后一天为:31
20214月最后一天为:31
20215月最后一天为:31
20216月最后一天为:31
20217月最后一天为:31
20218月最后一天为:31
20219月最后一天为:31
202110月最后一天为:31
202111月最后一天为:31
202112月最后一天为:31

看到这个结果,小编觉得真是坑爹啊,这个calendar还和当前天数有关假如在1-28日基本都是没问题的,但到29-31日的时候,2月首先出现问题,之后就是日期为30号的月份也会出现问题。

解决方案

修改第二次

public static queryEndDayOfMonth(Integer year, int month) {
        Calendar c = Calendar.getInstance();
        //clear一下
		c.clear();
        c.set(Calendar.YEAR, year);
        c.set(Calendar.MONTH, month-1);
        return c.getActualMaximum(Calendar.DAY_OF_MONTH);
    }
//或者是这样的
public static queryEndDayOfMonth(Integer year, int month) {
        Calendar c = Calendar.getInstance();
        //或者是set一下date
		c.set(Calendar.DATE, 1);
        c.set(Calendar.YEAR, year);
        c.set(Calendar.MONTH, month-1);
        return c.getActualMaximum(Calendar.DAY_OF_MONTH);
    }

测试结果

20211月最后一天为:31
20212月最后一天为:28
20213月最后一天为:31
20214月最后一天为:30
20215月最后一天为:31
20216月最后一天为:30
20217月最后一天为:31
20218月最后一天为:31
20219月最后一天为:30
202110月最后一天为:31
202111月最后一天为:30
202112月最后一天为:31

这样正确的,当然小编还写了一个使用JDK1.8的日期工具类来做了。代码如下

public static Integer getEndDayOfMonth(Integer year, int month) {
        LocalDate localDate = LocalDate.of(year,month,1);
        LocalDate last = localDate.with(TemporalAdjusters.lastDayOfMonth());
        return last.getDayOfMonth();

    }

问题是解决了得看下Calendar 的源码为什么出现这个问题:
这个方法一开始就让我们尽量自己使用子类覆盖它。这边filed传的是5也就是天的单位(Calendar.DAY_OF_MONTH),
java.util.Calendar#getActualMaximum

public int getActualMaximum(int field) {
		
        int fieldValue = getLeastMaximum(field);
        int endValue = getMaximum(field);
		
        // if we know that the maximum value is always the same, just return it.
        if (fieldValue == endValue) {
            return fieldValue;
        }

        // clone the calendar so we don't mess with the real one, and set it to
        // accept anything for the field values.
        Calendar work = (Calendar)this.clone();
        work.setLenient(true);

        // if we're counting weeks, set the day of the week to Sunday.  We know the
        // last week of a month or year will contain the first day of the week.
        if (field == WEEK_OF_YEAR || field == WEEK_OF_MONTH) {
            work.set(DAY_OF_WEEK, firstDayOfWeek);
        }

        // now try each value from getLeastMaximum() to getMaximum() one by one until
        // we get a value that normalizes to another value.  The last value that
        // normalizes to itself is the actual maximum for the current date
        int result = fieldValue;

        do {
            work.set(field, fieldValue);
            if (work.get(field) != fieldValue) {
                break;
            } else {
                result = fieldValue;
                fieldValue++;
            }
        } while (fieldValue <= endValue);

        return result;
    }

这边其实真正实现的是
java.util.Calendar.GregorianCalendar#getActualMaximum

public int getActualMaximum(int field) {
        final int fieldsForFixedMax = ERA_MASK|DAY_OF_WEEK_MASK|HOUR_MASK|AM_PM_MASK|
            HOUR_OF_DAY_MASK|MINUTE_MASK|SECOND_MASK|MILLISECOND_MASK|
            ZONE_OFFSET_MASK|DST_OFFSET_MASK;
        if ((fieldsForFixedMax & (1<<field)) != 0) {
            return getMaximum(field);
        }
		//这里31号而2月只有28日直接为3月3日
		//getNormalizedCalendar 
		//这个方法即为重要。小伙伴记得打断点调试,其中有一个compute()方法,
		//如果不清除缓存或不设置日期为1的情况下,里面的updateTime();会自动计算出这个月份加上系统的天数是否会超过这个月来计算实际得到的年月日
        GregorianCalendar gc = getNormalizedCalendar();
        BaseCalendar.Date date = gc.cdate;
        BaseCalendar cal = gc.calsys;
        int normalizedYear = date.getNormalizedYear();

        int value = -1;
        switch (field) {
        case MONTH:
            {
                if (!gc.isCutoverYear(normalizedYear)) {
                    value = DECEMBER;
                    break;
                }

                // January 1 of the next year may or may not exist.
                long nextJan1;
                do {
                    nextJan1 = gcal.getFixedDate(++normalizedYear, BaseCalendar.JANUARY, 1, null);
                } while (nextJan1 < gregorianCutoverDate);
                BaseCalendar.Date d = (BaseCalendar.Date) date.clone();
                cal.getCalendarDateFromFixedDate(d, nextJan1 - 1);
                value = d.getMonth() - 1;
            }
            break;

        case DAY_OF_MONTH:
            {
            	//这里就拿到了3月的最大天数
                value = cal.getMonthLength(date);
                if (!gc.isCutoverYear(normalizedYear) || date.getDayOfMonth() == value) {
                    break;
                }

                // Handle cutover year.
                long fd = gc.getCurrentFixedDate();
                if (fd >= gregorianCutoverDate) {
                    break;
                }
                int monthLength = gc.actualMonthLength();
                long monthEnd = gc.getFixedDateMonth1(gc.cdate, fd) + monthLength - 1;
                // Convert the fixed date to its calendar date.
                BaseCalendar.Date d = gc.getCalendarDate(monthEnd);
                value = d.getDayOfMonth();
            }
            break;

        case DAY_OF_YEAR:
            {
                if (!gc.isCutoverYear(normalizedYear)) {
                    value = cal.getYearLength(date);
                    break;
                }

                // Handle cutover year.
                long jan1;
                if (gregorianCutoverYear == gregorianCutoverYearJulian) {
                    BaseCalendar cocal = gc.getCutoverCalendarSystem();
                    jan1 = cocal.getFixedDate(normalizedYear, 1, 1, null);
                } else if (normalizedYear == gregorianCutoverYearJulian) {
                    jan1 = cal.getFixedDate(normalizedYear, 1, 1, null);
                } else {
                    jan1 = gregorianCutoverDate;
                }
                // January 1 of the next year may or may not exist.
                long nextJan1 = gcal.getFixedDate(++normalizedYear, 1, 1, null);
                if (nextJan1 < gregorianCutoverDate) {
                    nextJan1 = gregorianCutoverDate;
                }
                assert jan1 <= cal.getFixedDate(date.getNormalizedYear(), date.getMonth(),
                                                date.getDayOfMonth(), date);
                assert nextJan1 >= cal.getFixedDate(date.getNormalizedYear(), date.getMonth(),
                                                date.getDayOfMonth(), date);
                value = (int)(nextJan1 - jan1);
            }
            break;

        case WEEK_OF_YEAR:
            {
                if (!gc.isCutoverYear(normalizedYear)) {
                    // Get the day of week of January 1 of the year
                    CalendarDate d = cal.newCalendarDate(TimeZone.NO_TIMEZONE);
                    d.setDate(date.getYear(), BaseCalendar.JANUARY, 1);
                    int dayOfWeek = cal.getDayOfWeek(d);
                    // Normalize the day of week with the firstDayOfWeek value
                    dayOfWeek -= getFirstDayOfWeek();
                    if (dayOfWeek < 0) {
                        dayOfWeek += 7;
                    }
                    value = 52;
                    int magic = dayOfWeek + getMinimalDaysInFirstWeek() - 1;
                    if ((magic == 6) ||
                        (date.isLeapYear() && (magic == 5 || magic == 12))) {
                        value++;
                    }
                    break;
                }

                if (gc == this) {
                    gc = (GregorianCalendar) gc.clone();
                }
                int maxDayOfYear = getActualMaximum(DAY_OF_YEAR);
                gc.set(DAY_OF_YEAR, maxDayOfYear);
                value = gc.get(WEEK_OF_YEAR);
                if (internalGet(YEAR) != gc.getWeekYear()) {
                    gc.set(DAY_OF_YEAR, maxDayOfYear - 7);
                    value = gc.get(WEEK_OF_YEAR);
                }
            }
            break;

        case WEEK_OF_MONTH:
            {
                if (!gc.isCutoverYear(normalizedYear)) {
                    CalendarDate d = cal.newCalendarDate(null);
                    d.setDate(date.getYear(), date.getMonth(), 1);
                    int dayOfWeek = cal.getDayOfWeek(d);
                    int monthLength = cal.getMonthLength(d);
                    dayOfWeek -= getFirstDayOfWeek();
                    if (dayOfWeek < 0) {
                        dayOfWeek += 7;
                    }
                    int nDaysFirstWeek = 7 - dayOfWeek; // # of days in the first week
                    value = 3;
                    if (nDaysFirstWeek >= getMinimalDaysInFirstWeek()) {
                        value++;
                    }
                    monthLength -= nDaysFirstWeek + 7 * 3;
                    if (monthLength > 0) {
                        value++;
                        if (monthLength > 7) {
                            value++;
                        }
                    }
                    break;
                }

                // Cutover year handling
                if (gc == this) {
                    gc = (GregorianCalendar) gc.clone();
                }
                int y = gc.internalGet(YEAR);
                int m = gc.internalGet(MONTH);
                do {
                    value = gc.get(WEEK_OF_MONTH);
                    gc.add(WEEK_OF_MONTH, +1);
                } while (gc.get(YEAR) == y && gc.get(MONTH) == m);
            }
            break;

        case DAY_OF_WEEK_IN_MONTH:
            {
                // may be in the Gregorian cutover month
                int ndays, dow1;
                int dow = date.getDayOfWeek();
                if (!gc.isCutoverYear(normalizedYear)) {
                    BaseCalendar.Date d = (BaseCalendar.Date) date.clone();
                    ndays = cal.getMonthLength(d);
                    d.setDayOfMonth(1);
                    cal.normalize(d);
                    dow1 = d.getDayOfWeek();
                } else {
                    // Let a cloned GregorianCalendar take care of the cutover cases.
                    if (gc == this) {
                        gc = (GregorianCalendar) clone();
                    }
                    ndays = gc.actualMonthLength();
                    gc.set(DAY_OF_MONTH, gc.getActualMinimum(DAY_OF_MONTH));
                    dow1 = gc.get(DAY_OF_WEEK);
                }
                int x = dow - dow1;
                if (x < 0) {
                    x += 7;
                }
                ndays -= x;
                value = (ndays + 6) / 7;
            }
            break;

        case YEAR:
            /* The year computation is no different, in principle, from the
             * others, however, the range of possible maxima is large.  In
             * addition, the way we know we've exceeded the range is different.
             * For these reasons, we use the special case code below to handle
             * this field.
             *
             * The actual maxima for YEAR depend on the type of calendar:
             *
             *     Gregorian = May 17, 292275056 BCE - Aug 17, 292278994 CE
             *     Julian    = Dec  2, 292269055 BCE - Jan  3, 292272993 CE
             *     Hybrid    = Dec  2, 292269055 BCE - Aug 17, 292278994 CE
             *
             * We know we've exceeded the maximum when either the month, date,
             * time, or era changes in response to setting the year.  We don't
             * check for month, date, and time here because the year and era are
             * sufficient to detect an invalid year setting.  NOTE: If code is
             * added to check the month and date in the future for some reason,
             * Feb 29 must be allowed to shift to Mar 1 when setting the year.
             */
            {
                if (gc == this) {
                    gc = (GregorianCalendar) clone();
                }

                // Calculate the millisecond offset from the beginning
                // of the year of this calendar and adjust the max
                // year value if we are beyond the limit in the max
                // year.
                long current = gc.getYearOffsetInMillis();

                if (gc.internalGetEra() == CE) {
                    gc.setTimeInMillis(Long.MAX_VALUE);
                    value = gc.get(YEAR);
                    long maxEnd = gc.getYearOffsetInMillis();
                    if (current > maxEnd) {
                        value--;
                    }
                } else {
                    CalendarSystem mincal = gc.getTimeInMillis() >= gregorianCutover ?
                        gcal : getJulianCalendarSystem();
                    CalendarDate d = mincal.getCalendarDate(Long.MIN_VALUE, getZone());
                    long maxEnd = (cal.getDayOfYear(d) - 1) * 24 + d.getHours();
                    maxEnd *= 60;
                    maxEnd += d.getMinutes();
                    maxEnd *= 60;
                    maxEnd += d.getSeconds();
                    maxEnd *= 1000;
                    maxEnd += d.getMillis();
                    value = d.getYear();
                    if (value <= 0) {
                        assert mincal == gcal;
                        value = 1 - value;
                    }
                    if (current < maxEnd) {
                        value--;
                    }
                }
            }
            break;

        default:
            throw new ArrayIndexOutOfBoundsException(field);
        }
        return value;
    }

先get就会出现问题,当取出31号的时候,如果是2,4,6,9,11,直接跳到下个月去算了,取出的结果就会出现问题。所以我们可以优先设置天数,或者清除一下缓存即可。

总结

大家可以继续使用Calendar 类,在设置年月之前先调用clear()方法来做一次清除缓存或设置日期为1,这样也会没有问题。
不过建议大家使用jdk8中的日期工具类。感觉要被P1的锅了。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/13569.html

(1)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!