21

我正在检索一年中第一周的日期,我发现了非常奇怪的行为。

我在 Java 控制台应用程序和 Android 模拟器中测试了以下代码片段,它产生了不同的输出。

    Calendar cal = Calendar.getInstance();
    cal.set(Calendar.WEEK_OF_YEAR, 1);
    cal.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY);
    System.out.println(sdf.format(cal.getTime()));

产生了以下输出

Android 日志猫:2012/09/17(不正确)

Java 控制台:2012/01/01(正确)

奇怪的是,如果我在 Android 和 Java 中都使用以下代码,它会产生相同的正确输出。唯一的区别是我从上面的代码中交换了第二行和第三行。

    Calendar cal = Calendar.getInstance();
    cal.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY);
    cal.set(Calendar.WEEK_OF_YEAR, 1);
    System.out.println(sdf.format(cal.getTime()));

Android日志猫:2012/01/01(正确)

Java 控制台:2012/01/01(正确)

我很想知道这件事。

提前致谢。

4

1 回答 1

14

似乎 Calendar 类内部有两个数据容器。

protected long time

protected int[] fields

因此,当您调用 时cal.set(Calendar.WEEK_OF_YEAR, 1),您会更改 中的值fields,而不是time类中的值。

在 Java API 中

受保护的抽象 void computeFields()

将当前毫秒时间值 time 转换为 fields[] 中的日历字段值。这允许您将日历字段值与为日历设置的新时间同步。时间不先重新计算;要重新计算时间,然后是字段,请调用 complete() 方法。

我认为在 Android 的第一种情况下computeFields()不会在内部调用。

为了检查我的理论,我测试了以下代码:

SimpleDateFormat sdf = new SimpleDateFormat("yyyy.MM.dd");
Calendar cal = Calendar.getInstance();
System.out.println(cal);
System.out.println(sdf.format(cal.getTime()));
cal.set(Calendar.WEEK_OF_YEAR, 1);
System.out.println(cal);
System.out.println(sdf.format(cal.getTime()));
cal.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY);
System.out.println(cal);
System.out.println(sdf.format(cal.getTime()));

日志猫:

java.util.GregorianCalendar[ time=1348010308802,areFieldsSet=true ,lenient=true,zone=org.apache.harmony.luni.internal.util.ZoneInfo["null",mRawOffset=0,mUseDst=false],firstDayOfWeek=1 ,minimalDaysInFirstWeek=4,ERA==1,YEAR==2012,MONTH==8, WEEK_OF_YEAR==38 ,WEEK_OF_MONTH==4,DAY_OF_MONTH==18,DAY_OF_YEAR==262, DAY_OF_WEEK==3 ,DAY_OF_WEEK_IN_MONTH==3, AM_PM==1,HOUR==11,HOUR_OF_DAY=23,MINUTE==18,SECOND==28,MILLISECOND==802,ZONE_OFFSET==0,DST_OFFSET==0]

2012.09.18

java.util.GregorianCalendar[ time=?,areFieldsSet=false ,lenient=true,zone=org.apache.harmony.luni.internal.util.ZoneInfo["null",mRawOffset=0,mUseDst=false],firstDayOfWeek=1 ,minimalDaysInFirstWeek=4,ERA==1,YEAR==2012,MONTH==8, WEEK_OF_YEAR==1 ,WEEK_OF_MONTH==4,DAY_OF_MONTH==18,DAY_OF_YEAR==262, DAY_OF_WEEK==3 ,DAY_OF_WEEK_IN_MONTH==3, AM_PM==1,HOUR==11,HOUR_OF_DAY=23,MINUTE==18,SECOND==28,MILLISECOND==802,ZONE_OFFSET==0,DST_OFFSET==0]

2012.01.03

java.util.GregorianCalendar[ time=?,areFieldsSet=false ,lenient=true,zone=org.apache.harmony.luni.internal.util.ZoneInfo["null",mRawOffset=0,mUseDst=false],firstDayOfWeek=1 ,minimalDaysInFirstWeek=4,ERA==1,YEAR==2012,MONTH==8, WEEK_OF_YEAR==1 ,WEEK_OF_MONTH==4,DAY_OF_MONTH==18,DAY_OF_YEAR==262, DAY_OF_WEEK==1 ,DAY_OF_WEEK_IN_MONTH==3, AM_PM==1,HOUR==11,HOUR_OF_DAY=23,MINUTE==18,SECOND==28,MILLISECOND==802,ZONE_OFFSET==0,DST_OFFSET==0]

2012.09.16

正如我们在上面看到的,字段中的值发生了变化,但是内部时间被表示为?,说明它time没有与 同步fields

time您使用方法未同步getTime()并将其打印出来。

我认为 Android 中的日历旨在延迟同步,直到真正需要它。

添加

我在 Java API 中发现了以下内容:

可以使用三种方法更改日历字段:set()、add() 和 roll()。

set(f, value) 将字段 f 更改为 value。此外,它还设置了一个内部成员变量来指示字段 f 已更改。尽管字段 f 立即更改,但在下一次调用 get()、getTime() 或 getTimeInMillis() 之前,不会重新计算日历的毫秒数。因此,多次调用 set() 不会触发多次不必要的计算。作为使用 set() 更改字段的结果,其他字段也可能更改,具体取决于字段、字段值和日历系统。此外,get(f) 不一定会在重新计算字段后返回值。具体由具体的日历类决定。

添加

为了检查“细节由具体的日历类决定”是否属实,我检查了 Dalvik 和 JDK 6 的实际代码。

Dalvik's Calendar中的set方法

来自https://www.codeaurora.org/git/projects/qrd-gb-dsds-7225/repository/revisions/cc99b832a941dc8cbb86f1607d04eb87935ddbfd/entry/android/dalvik/libcore/luni/src/main/java/java/util/Calendar .java

public void set(int field, int value) {
    fields[field] = value;
    isSet[field] = true;
    areFieldsSet = isTimeSet = false;
    if (field > MONTH && field < AM_PM) {
        lastDateFieldSet = field;
    }
    if (field == HOUR || field == HOUR_OF_DAY) {
        lastTimeFieldSet = field;
    }
    if (field == AM_PM) {
        lastTimeFieldSet = HOUR;
    }
}

JDK 6 日历中的设置方法

public void set(int field, int value) {
    if (isLenient() && areFieldsSet && !areAllFieldsSet) {
        computeFields();
    }
    internalSet(field, value);
    isTimeSet = false;
    areFieldsSet = false;
    isSet[field] = true;
    stamp[field] = nextStamp++;
    if (nextStamp == Integer.MAX_VALUE) {
        adjustStamp();
    }
}

具体实现方式大相径庭。要找出问题的确切原因,您应该详细查看这两种实现。

于 2012-09-18T23:29:44.030 回答