java - 不同Android版本中java.util.GregorianCalendar类的奇怪行为

标签 java android date gregorian-calendar java.util.calendar

我正在Android应用程序中创建日历。日历的第一天是星期日或星期一。这取决于语言环境。不同Android版本中java.util.GregorianCalendar类的奇怪行为:

public class CurrentMonth extends AbstractCurrentMonth implements InterfaceCurrentMonth {

    public CurrentMonth(GregorianCalendar calendar, int firstDayOfWeek) {
        super(calendar, firstDayOfWeek);
    }

    @Override
    public List<ContentAbstract> getListContent() {
        int year = calendar.get(Calendar.YEAR);
        int month = calendar.get(Calendar.MONTH);

        GregorianCalendar currentCalendar = new GregorianCalendar(year, month, 1);

        List<ContentAbstract> list = new ArrayList<>();
        int weekDay = getDayOfWeek(currentCalendar);
        currentCalendar.add(Calendar.DAY_OF_WEEK, - (weekDay - 1));

        while (currentCalendar.get(Calendar.MONTH) != month) {
            list.add(getContent(currentCalendar));
            currentCalendar.add(Calendar.DAY_OF_MONTH, 1);
        }

        while (currentCalendar.get(Calendar.MONTH) == month) {
            list.add(getContent(currentCalendar));
            currentCalendar.add(Calendar.DAY_OF_MONTH, 1);
        }
        currentCalendar.add(Calendar.DAY_OF_MONTH, - 1);

        while (getDayOfWeek(currentCalendar) != 7) {
            currentCalendar.add(Calendar.DAY_OF_MONTH, 1);
            list.add(getContent(currentCalendar));
        }

        Log.i("text", "yaer: " + list.get(0).getYear());
        Log.i("text", "month: " + list.get(0).getMonth());
        Log.i("text", "day of month: " + list.get(0).getDay());
        Log.i("text", "day of week: " + list.get(0).getDayOfWeek());

        return list;
    }

    private int getDayOfWeek(GregorianCalendar currentCalendar) {
        int weekDay;
        if (firstDayOfWeek == Calendar.MONDAY) {
            weekDay = 7 - (8 - currentCalendar.get(Calendar.DAY_OF_WEEK)) % 7;
        }
        else weekDay = currentCalendar.get(Calendar.DAY_OF_WEEK);
        return weekDay;
    }

    private GraphicContent getContent(GregorianCalendar cal) {
        GraphicContent content = new GraphicContent();
        content.setYear(cal.get(Calendar.YEAR));
        content.setMonth(cal.get(Calendar.MONTH));
        content.setDay(cal.get(Calendar.DAY_OF_MONTH));
        content.setDayOfWeek(cal.get(Calendar.DAY_OF_WEEK));
        return content;
    }
}

public class GraphicContent extends ContentAbstract {
    private int year;
    private int month;
    private int day;
    private int dayOfWeek;

    @Override
    public int getYear() {
        return year;
    }

    @Override
    public void setYear(int year) {
        this.year = year;
    }

    @Override
    public int getMonth() {
        return month;
    }

    @Override
    public void setMonth(int month) {
        this.month = month;
    }

    @Override
    public int getDay() {
        return day;
    }

    @Override
    public void setDay(int day) {
        this.day = day;
    }

    @Override
    public int getDayOfWeek() {
        return dayOfWeek;
    }

    @Override
    public void setDayOfWeek(int dayOfWeek) {
        this.dayOfWeek = dayOfWeek;
    }
}


设置类构造函数(new GregorianCalendar(1994,3,1),Calendar.SUNDAY)。在Android 4.4、5.0 Logcat结果中:

10-12 14:32:28.332 27739-27739/*** I/text: yaer: 1994
10-12 14:32:28.332 27739-27739/*** I/text: month: 2
10-12 14:32:28.332 27739-27739/*** I/text: day of month: 26
10-12 14:32:28.332 27739-27739/*** I/text: day of week: 7


在android 8.0 logcat结果中:

2018-10-12 11:50:59.549 6565-6565/*** I/text: yaer: 1994
2018-10-12 11:50:59.549 6565-6565/*** I/text: month: 2
2018-10-12 11:50:59.549 6565-6565/*** I/text: day of month: 27
2018-10-12 11:50:59.549 6565-6565/*** I/text: day of week: 1


如您所见,结果-不同的日期(26和27),对应于一周中的不同天。但是,如果您更改日历对象的初始化:

@Override
    public List<ContentAbstract> getListContent() {
        int year = calendar.get(Calendar.YEAR);
        int month = calendar.get(Calendar.MONTH);

        GregorianCalendar currentCalendar = (GregorianCalendar) Calendar.getInstance();
        currentCalendar.set(year, month, 1);


结果将在所有版本的android上为TRUE:

10-12 15:12:56.400 28914-28914/*** I/text: yaer: 1994
10-12 15:12:56.400 28914-28914/*** I/text: month: 2
10-12 15:12:56.400 28914-28914/*** I/text: day of month: 27
10-12 15:12:56.400 28914-28914/*** I/text: week day: 1


在junit测试中,在所有情况下(27和SUNDAY),结果都是正确的。从代码中删除日志并检查:

 public class TestCurrentMonth {

    @Test
    public void testGetListContent() {
        GregorianCalendar calendar = new GregorianCalendar(1994, 3, 1);
        int firstDay = Calendar.SUNDAY;
        CurrentMonth currentMonth = new CurrentMonth(calendar, firstDay);
        List<ContentAbstract> list = currentMonth.getListContent();
        Assert.assertEquals(27, list.get(0).getDay());
        Assert.assertEquals(Calendar.SUNDAY, list.get(0).getDayOfWeek());
    }
}


同样表现为1993年4月,1992年。为什么?我已经不知所措了。

最佳答案

java.time

好的解决方案是跳过CalendarGregorianCalendar类,而是使用java.time(现代Java日期和时间API)中的LocalDateCalendarGregorianCalendar早已过时且设计不良。现代的API更好用。而且LocalDate是一个没有时间和时区的日期,因此,如果我在下面播出的怀疑是正确的,它将保证将您的时区/夏令时问题抛在后面。要在较旧的Android上使用它,请参阅下一节。

什么地方出了错?投机解释

下面的解释纯粹是理论上的,但我已经想到了最好的解释。它基于一些我无法证实的假设:


您(或其中一个设备)所在的时区是1994年3月的最后几天开始的夏令时(DST)。
Android 4.4和5.0中的GregorianCalendar中可能存在一个错误,因此currentCalendar.add(Calendar.DAY_OF_WEEK, - (weekDay - 1));只会在24小时内添加多次。


纯粹是推测,但如果存在此类错误,您的GregorianCalendar最终将在目标日期的前一天23:00结束,这将解释您的结果。例如,欧盟国家/地区在3月的最后一个星期日开始夏季时间。 1994年也是如此。这非常适合您的目标日期1994年3月27日,星期日,并且也可以解释您在1992年和1993年的错误结果。 Android GregorianCalendar,找不到任何支持它的东西。

我怀疑是要解释您的观察,我们还需要几段:


我怀疑该错误仅在某些Android版本(4.4、5.0)中得到解决,而在更高版本(8.0)中得到修复(或者您的Android 8.0设备将在不同的时区运行)。另外,您运行测试的环境没有错误或具有不同的默认时区(可以说明测试通过的原因)。
GregorianCalendar获得的getInstance中有一天中的时间。并在设置日期后保留它。要说明两种设置日期的方式之间的区别:假设您在9:05运行代码。 new GregorianCalendar(1994, Calendar.APRIL, 1)会给您1994年4月1日00:00。 Calendar.getInstance()后跟currentCalendar.set(year, month, 1);会给您1994年4月1日上午9:05。两者之间相差9个小时多一点。在后一种情况下,可疑的错误会导致您在3月27日8:05仍然是正确的日期,因此您看不到该错误。如果您在晚上0:35运行代码,那么您将在3月26日达到23:35,那么在这种情况下您也会看到该错误。


就像我已经说过的那样,LocalDate,java.time和ThreeTenABP将构成很好的解决方案。如果您选择不依赖外部库,而是通过过时的类来解决问题,我相信以下内容会有所帮助:

    GregorianCalendar currentCalendar = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
    currentCalendar.set(year, month, 1);


TimeZone是又一类较旧且设计欠佳的类,尤其是我正在使用的getTimeZone方法令人有些吃惊,但我相信以上工作(手指交叉)。这个想法是告诉Calendar使用UTC时间。 UTC没有夏季时间,因此可以避免此问题。

您可以尝试的另一种更hacky的方法是:

    currentCalendar.set(year, month, 1, 6, 0);


这会将一天中的小时设置为6,这意味着当您返回夏季转换时,您将在早上5:00播放,这仍将是正确的日期(上面的调用未设置秒和毫秒) ;一次运行,我在1994年4月1日世界标准时间06:00:40.213)。

问题:我可以在Android上使用java.time吗?

是的,java.time在较新和较旧的Android设备上均可正常运行。它至少需要Java 6。


在Java 8和更高版本以及更新的Android设备(来自API级别26)中,内置了现代API。
在Java 6和7中,获取ThreeTen Backport,即新类的Backport(JSR 310的ThreeTen;请参见底部的链接)。
在较旧的Android上,请使用Android版本的ThreeTen Backport。叫做ThreeTenABP。并确保使用子包从org.threeten.bp导入日期和时间类。


链接


Oracle tutorial: Date Time解释如何使用java.time
Java Specification Request (JSR) 310,其中首先描述java.time
ThreeTen Backport project,是java.time到Java 6和7(JSR-310的ThreeTen)的反向端口。
ThreeTenABP,ThreeTen Backport的Android版
Question: How to use ThreeTenABP in Android Project,具有非常详尽的说明。

关于java - 不同Android版本中java.util.GregorianCalendar类的奇怪行为,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/52779671/

相关文章:

date - Hive 日期/时间戳列

java - 为什么 jframe 最大化时会隐藏任务栏?

java - 使用 Schedulers 时,System.out.println 在 RxJava 中不打印任何内容

java - 桌面应用视频加密

java - 卡夫卡流 RoundRobinPartitioner

java - Android 监听触摸释放

java 日历格式

android - 从广播接收器关闭应用程序

java - 如何在 Android 的 AlertDialog 中设置主题

MySql 选择相对相似的日期