简体   繁体   中英

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

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 < 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"); 
          //note: got lazy and not outputting "00" 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 >= 12 ?  Calendar.PM : Calendar.AM) );            
          cal.set(Calendar.SECOND,      0);
          cal.set(Calendar.MILLISECOND, 0);
          Date d = cal.getTime();

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

      }
  }

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 < 24; hour++) {
  for (int minute = 0; minute < 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.

tl;dr

 LocalDate
 .now( ZoneId.of( "America/Chicago" ) )            // Today
 .atStartOfDay( ZoneId.of( "America/Chicago" ) )   // 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( "Africa/Tunis" ) ;  // Or "America/New_York", 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< 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 .

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?

The other answers tell you to use a different API, ie 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:

 YEAR + MONTH + DAY_OF_MONTH YEAR + MONTH + WEEK_OF_MONTH + DAY_OF_WEEK YEAR + MONTH + DAY_OF_WEEK_IN_MONTH + DAY_OF_WEEK YEAR + DAY_OF_YEAR YEAR + DAY_OF_WEEK + WEEK_OF_YEAR

For the time of day fields:

 HOUR_OF_DAY AM_PM + HOUR

The question code has:

cal.set(Calendar.HOUR_OF_DAY, hour );
cal.set(Calendar.AM_PM, (hour >= 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, ie 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 >= 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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM