【问题标题】:Strange behavior of the java.util.GregorianCalendar class in different android versionsjava.util.GregorianCalendar 类在不同 android 版本中的奇怪行为
【发布时间】:2019-03-17 16:38:50
【问题描述】:

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

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 上的结果都是正确的

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 日和星期日)。从代码中删除日志并检查:

 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());
    }
}

1992 年 4 月的行为也是 1993 年 4 月。为什么?我已经伤透了脑筋。

【问题讨论】:

  • 你应该学习How to create a Minimal, Complete, and Verifiable example。我相信你的问题最多可以用 10 行代码来演示。这将使那些有兴趣帮助您的人更容易,增加他们最终这样做的机会。
  • 感谢您的回答。他看起来像事实。我知道 LokalData 类,它不适合 min SdK。感谢您提供有关 ThreeTenAbP 的建议。我会研究它。但是仍然不清楚为什么在 GregorianCalendar 对象的任何初始化期间测试都是正确的,并且在设备(真实和仿真器)上只有 getInstance() 是正确的?为什么在新版本的 android 上结果无论如何都是正确的?
  • “如何创建一个最小的、完整的和可验证的例子”——我会研究这个。谢谢

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


【解决方案1】:

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 并没有找到任何支持它的东西。

为了解释你的观察,我们还需要几条:

  1. 我怀疑的错误只会出现在某些 Android 版本(4.4、5.0)中,并在更高版本(8.0)中修复(或者您的 Android 8.0 设备将运行不同的时区)。此外,您运行测试的环境要么没有错误,要么具有不同的默认时区(或者可以解释测试通过的原因)。
  2. 您从getInstance 获得的GregorianCalendar 包含一天中的时间。并在您设置日期后保留它。说明设置日期的两种方式之间的区别:假设您在 9:05 运行代码。 new GregorianCalendar(1994, Calendar.APRIL, 1) 会给你 1994 年 4 月 1 日 00:00。 Calendar.getInstance() 后跟 currentCalendar.set(year, month, 1); 为您提供 1994 年 4 月 1 日 09: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 没有夏令时,这回避了这个问题。

您可能会尝试的另一件更骇人听闻的事情是:

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

这会将一天中的小时设置为 6,这意味着当您返回夏令时转换时,您将在早上 5:00,仍然是正确的日期(上面的调用确实 没有设置秒和毫秒;在一次运行中我得到了 1994 年 4 月 1 日 06:00:40.213 UTC)。

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

是的,java.time 在较旧和较新的 Android 设备上运行良好。它只需要至少 Java 6

  • 在 Java 8 及更高版本以及更新的 Android 设备(从 API 级别 26 起)中,现代 API 是内置的。
  • 在 Java 6 和 7 中,获取 ThreeTen Backport,即新类的后向端口(对于 JSR 310,ThreeTen;请参阅底部的链接)。
  • 在(较旧的)Android 上使用 ThreeTen Backport 的 Android 版本。它被称为 ThreeTenABP。并确保从 org.threeten.bp 导入日期和时间类以及子包。

链接

【讨论】:

  • 感谢您的详细回复,以及花在我身上的时间。我会研究你的建议。正如我所写,以下代码工作正常: GregorianCalendar currentCalendar = (GregorianCalendar) Calendar.getInstance(); currentCalendar.set(年, 月, 1);在所有版本的 android 上。
  • 不适用于旧版 Android(未检查所有版本):GregorianCalendar currentCalendar = new GregorianCalendar(year, month,1);
  • 这很奇怪。我以为:new GregorianCalendar(year, month, 1) == (GregorianCalendar) Calendar.getInstance() 和 set(year, month,1)。但是发现了一个难以理解的区别.在相同的条件下,一个代码总是有效,另一个代码不总是。
  • 我已经编辑并尝试更详细地解释我的第 2 项。我希望它有助于理解一方面new GregorianCalendar(year, month, 1) 与另一方面(GregorianCalendar) Calendar.getInstance()set(year, month,1) 之间的区别。
【解决方案2】:

这里没有什么奇怪的。 getInstance 将根据您的语言环境和时区返回数据,这与构造函数不同。不确定是否有较新的 Android 版本,可能这里发生了一些变化,或者您使用不同的时区/语言环境进行了测试?

【讨论】:

  • 在新版本的 android 中行为有所不同。很多时候我在不同版本的android中使用了构造函数,没问题。以及为什么是 1992 年 4 月、1993 年、1994 年。谢谢
  • 该应用程序已经在不同的语言环境下进行了测试,其行为在不同版本的 Android 中是不同的。仅使用新的 GregorianCalendar 并且仅在此代码段上
  • new GregerianCalendar() 和 Calendar.getInstance() 返回带有语言环境的 GregorianCalendar。此外,所有情况下的测试都正确通过,但在 Android 设备上结果不同。
猜你喜欢
  • 2021-02-07
  • 1970-01-01
  • 2013-06-16
  • 1970-01-01
  • 2016-05-08
  • 1970-01-01
  • 1970-01-01
  • 2022-10-23
  • 1970-01-01
相关资源
最近更新 更多