为什么Java的Calendar日期在我将其转换为Date后会将小时重置为另一个值?

huangapple go评论88阅读模式
英文:

How come Java's Calendar date resets the hour to another value when I convert it to a Date?

问题

我想要做一个简单的脚本,在2020年10月26日每隔10分钟设置一次时间。
我的循环如下所示。
然而,即使当我输出'time'变量时,时间看起来基本正确,然后我将其转换为Date对象,并在那个'd'变量上执行toString(),然后似乎又将其转换为另一个时间。
如果你想在在线Java编译器上看到它运行,可以在这里看到它的运行情况:
https://ideone.com/T8anod
你可以看到它做一些奇怪的事情,比如:
0:00 AM
星期一,2020年10月26日10:00:00 GMT
0:10 AM
星期一,2020年10月26日10:10:00 GMT
...
12:00 PM
星期一,2020年10月26日22:00:00 GMT
12:10 PM
...
星期一,2020年10月26日22:10:00 GMT

一个不寻常的事情是,我会在我的IDE中设置一个在12小时处的断点......如果我慢慢地进行调试,它会设置正确的时间。然后我会移除我的断点并播放脚本的剩余部分,然后时间再次变得不正确。这是我在Java中尚未见过的非常不寻常的行为。
我可以像这样重置小时数(或使用不同的技术):

Date d = cal.getTime();
d.setHours(hour); //已弃用

但我宁愿现在弄清楚为什么Java会以这种方式运行。

for(int hour=0; hour < 24; hour++ ) {
    for(int minute = 0; minute < 6; minute++) {

        String str_min = minute==0 ? "00" : String.valueOf(minute*10);
        String time = String.valueOf( hour > 12 ? hour-12 : hour) +":"+ str_min +" "+ ((hour >= 12 ?  Calendar.PM : Calendar.AM)==1 ? "PM" : "AM");
        //注意:小时没有输出"00"
        System.out.println( time );

        Calendar cal = Calendar.getInstance();
        cal.set(Calendar.MONTH, 9); //Oct=9
        cal.set(Calendar.DAY_OF_MONTH, 26);
        cal.set(Calendar.YEAR, 2020);
        cal.set(Calendar.HOUR_OF_DAY, hour );              
        cal.set(Calendar.MINUTE, minute*10     ); //0=0, 1=10, ... 5=50
        cal.set(Calendar.AM_PM, (hour >= 12 ?  Calendar.PM : Calendar.AM) );              
        cal.set(Calendar.SECOND,      0);
        cal.set(Calendar.MILLISECOND, 0);
        Date d = cal.getTime();

        System.out.println( d.toString() );

    }
}
英文:

I was looking to do a simple script of setting a time every 10minutes on Oct 26th 2020.
My loop looks like below.
However even though the time generally seems right when i output 'time' var, then i convert it to Date object and do a toString() on that 'd' variable then it seems to convert it to another time. If you want to see it run on an online java compiler you can see it in action here:
https://ideone.com/T8anod
You can see it do strange stuff like:
0:00 AM
Mon Oct 26 10:00:00 GMT 2020
0:10 AM
Mon Oct 26 10:10:00 GMT 2020
...
12:00 PM
Mon Oct 26 22:00:00 GMT 2020
12:10 PM
...
Mon Oct 26 22:10:00 GMT 2020

One unusually thing is i'll set a breakpoint in my IDE at 12 hours... and if I debug slowly it will set the correct time. Then i'll remove my breakpoint and play through the rest of the script and the times then end up incorrect again. Very unusual behavior I haven't seen in Java yet.
I could do something like this to reset the hours (or use a different technique):

Date d = cal.getTime();  
d.setHours(hour); //deprecated

But I'd rather just figure out for now why Java is acting this way.

  for(int hour=0; hour &lt; 24; hour++ ) {
	  for(int minute = 0; minute &lt; 6; minute++) {

		  String str_min = minute==0 ? &quot;00&quot; : String.valueOf(minute*10);
		  String time = String.valueOf( hour &gt; 12 ? hour-12 : hour) +&quot;:&quot;+ str_min +&quot; &quot;+ ((hour &gt;= 12 ?  Calendar.PM : Calendar.AM)==1 ? &quot;PM&quot; : &quot;AM&quot;); 
          //note: got lazy and not outputting &quot;00&quot; for hour
		  System.out.println( time );

	      Calendar cal = Calendar.getInstance();
	      cal.set(Calendar.MONTH, 9); //Oct=9
	      cal.set(Calendar.DAY_OF_MONTH, 26);
	      cal.set(Calendar.YEAR, 2020);
	      cal.set(Calendar.HOUR_OF_DAY, hour );		      
	      cal.set(Calendar.MINUTE, minute*10     ); //0=0, 1=10, ... 5=50
	      cal.set(Calendar.AM_PM, (hour &gt;= 12 ?  Calendar.PM : Calendar.AM) );		      
	      cal.set(Calendar.SECOND,      0);
	      cal.set(Calendar.MILLISECOND, 0);
	      Date d = cal.getTime();

	      System.out.println( d.toString() );

	  }
  }

答案1

得分: 6

java.util.Date 是一个谎言。它不代表一个日期;它代表的是时间的瞬间,它对时区一无所知,或者对这个“小时”的概念没有概念。这就是为什么除了基于毫秒的方法和构造函数之外的一切都被标记为已弃用。

日历 API 试图修复这个问题,但是它是一个可怕的、可怕的 API。

请改用 java.time。这个丑陋的 API 将会消失,而且与旧的内容不同,您将拥有实际代表您所需内容的类型。鉴于您在这里没有涉及时区,您需要使用 LocalDateTime

for (int hour = 0; hour < 24; hour++) {
  for (int minute = 0; minute < 60; minute += 10) {
    LocalDateTime ldt = LocalDateTime.of(2020, 10, 26, hour, minute, 0);
    System.out.println(ldt);
    // 或者更好地:
    System.out.println(DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(ldt));
  }
}

请注意,在此 API 中,十月是 '10',因为这个 API 不是疯狂的,例如。您可以使用 DTF 中的许多预定义格式化程序之一,或者使用模式编写自己的格式化程序,以精确控制日期的呈现方式。

如果您想要表示其他内容,您也可以这样做;对于人们通常表述的特定时间(例如:我在罗马有一个牙医约会,在2020年11月2日下午2点15分),可以使用 ZonedDateTime。这个概念的要点在于:如果罗马决定切换时区,那么您的约会发生的实际时间瞬间也应该随之改变。

如果您想要一个时间瞬间,可以使用 Instant。这是一个在过去或未来的特定时间点,它的发生方式不会随着剩余的毫秒数而改变。即使面对国家改变它们的时区。

而 LDT,嗯,就是 LDT:它仅代表年、月、日、小时、分钟和秒 - 没有时区。

英文:

java.util.Date is a lie. It does not represent a date; it represents an instant in time, it has no idea about timezones, or this concept of 'hours'. That's why everything (except the epoch-millis-based methods and constructors) are marked deprecated.

The calendar API tries to fix this, but is a horrible, horrible API.

Use java.time instead. The ugly API goes away, and unlike the old stuff, you have types that actually represent exactly what you want. Given that you aren't messing with timezones here, you want LocalDateTime:

for (int hour = 0; hour &lt; 24; hour++) {
  for (int minute = 0; minute &lt; 60; minute += 10) {
    LocalDateTime ldt = LocalDateTime.of(2020, 10, 26, hour, minute, 0);
    System.out.println(ldt);
    // or better:
    System.out.println(DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(ldt));
  }
}

Note how October is '10' in this API, because this API is not insane, for example. You can use any of the many predefined formatters in DTF, or write your own with a pattern to control precisely how you want to render the date.

If you want to represent other things, you can do so; ZonedDateTime for a specific time the way humans would say it (say: I have an appointment with the dentist, who is in Rome, at a quarter past 2 in the afternoon on november 2nd, 2020). The point of such a concept is this: If Rome decides to switch timezones, then the actual instant in time your appointment occurs should change along with it.

If you want an instant in time, there's Instant. This is a specific moment in time (in the past or future) that will not change in terms of how many milliseconds remain until it occurs. Even in the face of countries changing their zone.

And LDT is, well, LDT: It just represents a year, a month, a day, hours, minutes, and seconds - without a timezone.

答案2

得分: 2

# tl;dr

     LocalDate
     .now( ZoneId.of( "America/Chicago" ) )            
     .atStartOfDay( ZoneId.of( "America/Chicago" ) )   
     .plus( 
        Duration.ofMinutes( 10 )                       
     )                                                 

# Avoid legacy date-time classes

You are using terrible date-time classes that were supplanted years ago by the *java.time* classes. Sun, Oracle, and the [JCP][1] gave up on those classes, and so should you.

Rather than try to understand those awful classes, I suggest you invest your effort in learning *java.time*. 

# Use *java.time*

Much easier to capture an `Instant` object when you want the current date-time moment.

Note that *java.time* uses [immutable objects][2]. Rather than alter an existing object, we generate a fresh one.

To represent a moment for every ten-minute interval of today:

    ZoneId z = ZoneId.of( "Africa/Tunis" ) ;  
    LocalDate today = LocalDate.now( z ) ;

Your code makes incorrect assumptions. Days or not always 24 hours long. They may be 23 hours, 25 hours, or some other length. Also, not every day in every time zone starts at 00:00. Let *java.time* determine the first moment of the day.

    ZonedDateTime start = today.atStartOfDay( z ) ;

Get the start of the next day.

    ZonedDateTime stop = start.plusDays( 1 ) ;

Loop for your 10-minute chunk of time. Represent that chunk as a `Duration`.

    Duration d = Duration.ofMinutes( 10 ) ;

    List< ZonedDateTime > results = new ArrayList<>();
    ZonedDateTime zdt = start ;
    while( zdt.isBefore( stop ) ) 
    {
        results.add( zdt ) ;
        zdt = zdt.plus( d ) ;
    }

See this [code run live at IdeOne.com][3].

## Strings

You can generate strings in any format to represent the content of those `ZonedDateTime` objects. You can even let *java.time* automatically localize.

    Locale locale = Locale.CANADA_FRENCH ; 
    DateTimeFormatter f = DateTimeFormatter.ofLocalizedDateTime( FormatStyle.FULL ).withLocale( locale ) ;
    String output = myZonedDateTime.format( f ) ;

In particular, `Locale.US` and `FormatStyle.MEDIUM` might work for you. Fork [the code at IdeOne.com][3] to experiment.

----------

# About *java.time*

The [*java.time*](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/package-summary.html) framework is built into Java 8 and later. These classes supplant the troublesome old [legacy](https://en.wikipedia.org/wiki/Legacy_system) date-time classes such as `java.util.Date`, `Calendar`, & `SimpleDateFormat`.

To learn more, see the [*Oracle Tutorial*](http://docs.oracle.com/javase/tutorial/datetime/TOC.html). And search Stack Overflow for many examples and explanations. Specification is [JSR 310](https://jcp.org/en/jsr/detail?id=310).

The [*Joda-Time*](http://www.joda.org/joda-time/) project, now in [maintenance mode](https://en.wikipedia.org/wiki/Maintenance_mode), advises migration to the `java.time` classes.

You may exchange *java.time* objects directly with your database. Use a [JDBC driver](https://en.wikipedia.org/wiki/JDBC_driver) compliant with [JDBC 4.2](http://openjdk.java.net/jeps/170) or later. No need for strings, no need for `java.sql.*` classes. Hibernate 5 & JPA 2.2 support *java.time*. 

Where to obtain the java.time classes? 

 - [**Java SE 8**](https://en.wikipedia.org/wiki/Java_version_history#Java_SE_8), [**Java SE 9**](https://en.wikipedia.org/wiki/Java_version_history#Java_SE_9), [**Java SE 10**](https://en.wikipedia.org/wiki/Java_version_history#Java_SE_10), [**Java SE 11**](https://en.wikipedia.org/wiki/Java_version_history#Java_SE_11), and later - Part of the standard Java API with a bundled implementation.
   - [**Java 9**](https://en.wikipedia.org/wiki/Java_version_history#Java_SE_9) brought some minor features and fixes.
 - [**Java SE 6**](https://en.wikipedia.org/wiki/Java_version_history#Java_SE_6) and [**Java SE 7**](https://en.wikipedia.org/wiki/Java_version_history#Java_SE_7)
   - Most of the `java.time` functionality is back-ported to Java 6 & 7 in [**ThreeTen-Backport**](http://www.threeten.org/threetenbp/).
 - [**Android**](https://en.wikipedia.org/wiki/Android_(operating_system))
   - Later versions of Android (26+) bundle implementations of the `java.time` classes.
   - For earlier Android (<26), a process known as [**API desugaring**](https://developer.android.com/studio/write/java8-support#library-desugaring) brings a [subset of the `java.time`](https://developer.android.com/studio/write/java8-support-table) functionality not originally built into Android.
     - If the desugaring does not offer what you need, the [**ThreeTenABP**](https://github.com/JakeWharton/ThreeTenABP) project adapts [**ThreeTen-Backport**](http://www.threeten.org/threetenbp/) (mentioned above) to Android. See [**How to use ThreeTenABP…**](http://stackoverflow.com/q/38922754/642706).

  [1]: https://en.wikipedia.org/wiki/Java_Community_Process
  [2]: https://en.wikipedia.org/wiki/Immutable_object
  [3]: https://ideone.com/LhG5u6
英文:

tl;dr

 LocalDate
.now( ZoneId.of( &quot;America/Chicago&quot; ) )            // Today
.atStartOfDay( ZoneId.of( &quot;America/Chicago&quot; ) )   // First moment of today. Returns a `ZonedDateTime` object.
.plus( 
Duration.ofMinutes( 10 )                       // Ten minutes later.
)                                                 // Returns another `ZonedDateTime` object.

Avoid legacy date-time classes

You are using terrible date-time classes that were supplanted years ago by the java.time classes. Sun, Oracle, and the JCP gave up on those classes, and so should you.

Rather than try to understand those awful classes, I suggest you invest your effort in learning java.time.

Use java.time

Much easier to capture a Instant object when you want the current date-time moment.

Note that java.time uses immutable objects. Rather than alter an existing object, we generate a fresh one.

To represent a moment for every ten-minute interval of today:

ZoneId z = ZoneId.of( &quot;Africa/Tunis&quot; ) ;  // Or &quot;America/New_York&quot;, etc.
LocalDate today = LocalDate.now( z ) ;

Your code makes incorrect assumptions. Days or not always 24 hours long. They may be 23 hours, 25 hours, or some other length. Also not every day in every time zone starts at 00:00. Let java.time determine the first moment of the day.

ZonedDateTime start = today.atStartOfDay( z ) ;

Get the start of the next day.

ZonedDateTime stop = start.plusDays( 1 ) ;

Loop for your 10-minute chunk of time. Represent that chunk as a Duration.

Duration d = Duration.ofMinutes( 10 ) ;
List&lt; ZonedDateTime &gt; results = new ArrayList&lt;&gt;() ;
ZonedDateTime zdt = start ;
while( zdt.isBefore( stop ) ) 
{
results.add( zdt ) ;
zdt = zdt.plus( d ) ;
}

See this code run live at IdeOne.com.

Strings

You can generate strings in any format to represent the content of those ZonedDateTime objects. You can even let java.time automatically localize.

Locale locale = Locale.CANADA_FRENCH ; // Or Locale.US etc.
DateTimeFormatter f = DateTimeFormatter.ofLocalizedDateTime( FormatStyle.FULL ).withLocale( locale ) ;
String output = myZonedDateTime.format( f ) ;

In particular, Locale.US and FormatStyle.MEDIUM might work for you. Fork the code at IdeOne.com to experiment.


About java.time

The java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date, Calendar, & SimpleDateFormat.

To learn more, see the Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310.

The Joda-Time project, now in maintenance mode, advises migration to the java.time classes.

You may exchange java.time objects directly with your database. Use a JDBC driver compliant with JDBC 4.2 or later. No need for strings, no need for java.sql.* classes. Hibernate 5 & JPA 2.2 support java.time.

Where to obtain the java.time classes?

答案3

得分: 2

其他答案告诉你要使用不同的API,即不要使用Calendar,他们是对的,但他们没有告诉你问题代码为什么不起作用。

如果你阅读Calendar的文档,并查找"Calendar Fields Resolution"部分,你会发现(由我加粗强调):

> Calendar Fields Resolution
>
> 在从日历字段计算日期和时间时,可能会缺少计算所需的信息(例如仅有年份和月份没有日期),或者可能存在不一致的信息(例如公历1996年7月15日是星期二,但实际上1996年7月15日是星期一)。日历将解析日历字段的值,以以下方式确定日期和时间。

> 如果日历字段的值存在冲突,日历将优先考虑最近被设置的日历字段。以下是日历字段的默认组合。将使用最近的组合,由最近设置的单个字段确定。

> 对于日期字段:
> lang-none &gt; YEAR + MONTH + DAY_OF_MONTH &gt; YEAR + MONTH + WEEK_OF_MONTH + DAY_OF_WEEK &gt; YEAR + MONTH + DAY_OF_WEEK_IN_MONTH + DAY_OF_WEEK &gt; YEAR + DAY_OF_YEAR &gt; YEAR + DAY_OF_WEEK + WEEK_OF_YEAR &gt;
>
> 对于一天中的时间字段:
> lang-none &gt; HOUR_OF_DAY &gt; AM_PM + HOUR &gt;

问题代码为:

cal.set(Calendar.HOUR_OF_DAY, hour );
cal.set(Calendar.AM_PM, (hour &gt;= 12 ?  Calendar.PM : Calendar.AM) );

由于上述字段中的最后一个是AM_PM,它使用了AM_PM + HOUR来解析小时,由于你从未调用过clear(),也未设置HOUR,该值是由getInstance()调用设置的12小时制时钟值,即当前时间

你有两种选择来修复这个问题:

// 仅设置24小时制的值
cal.set(Calendar.HOUR_OF_DAY, hour);
// 设置12小时制的值
cal.set(Calendar.HOUR, hour % 12);
cal.set(Calendar.AM_PM, (hour &gt;= 12 ? Calendar.PM : Calendar.AM) );

我建议使用第一种方法。

我还强烈建议在设置字段之前调用**clear()**,这样结果就不会被剩余的值污染。这将消除将SECONDMILLISECOND设置为0的需要。

实际上,我建议使用更新的时间API,就像其他答案所做的那样,但至少现在你知道代码失败的原因了。

英文:

The other answers tell you to use a different API, i.e. to not use Calendar, and they are right, but they don't tell you why the question code doesn't work.

If you read the documentation of Calendar, and look for section "Calendar Fields Resolution", you will find (bold highlight by me):

> Calendar Fields Resolution
>
>When computing a date and time from the calendar fields, there may be insufficient information for the computation (such as only year and month with no day of month), or there may be inconsistent information (such as Tuesday, July 15, 1996 (Gregorian) -- July 15, 1996 is actually a Monday). Calendar will resolve calendar field values to determine the date and time in the following way.
>
> If there is any conflict in calendar field values, Calendar gives priorities to calendar fields that have been set more recently. The following are the default combinations of the calendar fields. The most recent combination, as determined by the most recently set single field, will be used.
>
> For the date fields:
> lang-none
&gt; YEAR + MONTH + DAY_OF_MONTH
&gt; YEAR + MONTH + WEEK_OF_MONTH + DAY_OF_WEEK
&gt; YEAR + MONTH + DAY_OF_WEEK_IN_MONTH + DAY_OF_WEEK
&gt; YEAR + DAY_OF_YEAR
&gt; YEAR + DAY_OF_WEEK + WEEK_OF_YEAR
&gt;

>
> For the time of day fields:
> lang-none
&gt; HOUR_OF_DAY
&gt; AM_PM + HOUR
&gt;

The question code has:

cal.set(Calendar.HOUR_OF_DAY, hour );
cal.set(Calendar.AM_PM, (hour &gt;= 12 ?  Calendar.PM : Calendar.AM) );

Since the last of those fields set is AM_PM, it resolved the hour using AM_PM + HOUR, and since you never call clear() and never set HOUR, the value is the 12-hour clock value set by the getInstance() call, i.e. the current time.

You have 2 choices to fix this:

// Only set the 24-hour value
cal.set(Calendar.HOUR_OF_DAY, hour);
// Set the 12-hour value
cal.set(Calendar.HOUR, hour % 12);
cal.set(Calendar.AM_PM, (hour &gt;= 12 ? Calendar.PM : Calendar.AM) );

I would recommend doing the first one.

I would also highly recommend calling clear() before setting fields, so the result is not polluted by left-over values. That would eliminate the need to set SECOND and MILLISECOND to 0.

Actually, I would recommend using the newer Time API, like the other answers do, but at least you now know why the code was failing.

huangapple
  • 本文由 发表于 2020年10月27日 09:10:05
  • 转载请务必保留本文链接:https://go.coder-hub.com/64547033.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定