📜 ⬆️ ⬇️

Java and time: part two

This article is written in continuation of the first part and is devoted to the new Date Time API , which was introduced in Java 8. I initially wanted to arrange this topic separately, because it is quite large and serious. I myself have not fully started to use this API in projects, so we will understand together along the way. In principle, there is no urgent need to switch to the new API, moreover, many have not yet started Java 8 projects, which means that there is still time to master.

In the article I will try not to slip into the banal translation of regular documentation, I would like to concentrate more on what seemed to me especially important.

Story


As for working with time, there have been complaints about the standard Java library for a long time. The criticized version of the API was developed a very long time ago and when designing it, serious mistakes were made. Alternatively, many have used the third-party Joda-time library. I myself am not a very big fan of Joda-time for several reasons:

Comparison


It’s probably worth starting with the fact that it didn’t suit many in the old API. And right there, in order not to waste time, I will immediately indicate that the new API has changed for the better.

Dividing classes into packages:

')
Class Names:


Immutability and thread safety:


Accuracy:


Store time and date stamps:


Specify time zone:


Testing:


Month numbering:


Tagging:


Duration designation:

Fears


It’s also probably worth telling that I’m definitely not going to call it “impairment,” but I’ll carefully call it “apprehension”:


I will tell you more about these and other cases that alerted me in the examples.

Time zones


Let's start as usual with time zones. The new java.time.ZoneId class denotes a time zone. Its two subclasses java.time.ZoneRegion and java.time.ZoneOffset implement two types of time zones: a time zone according to geographic principle and a time zone according to a simple offset from UTC, UT or GMT. The translation rules for arrows are placed in a separate java.time.zone.ZoneRules class, an instance of which is available through the java.time.ZoneId # getRules method.

In general, besides the specified refactoring, I did not find any special fundamental changes here, except that the rules for translating arrows now provide more methods for requesting information. Therefore, everything written in the old article is also valid for new classes of temporary zones, except that the methods differ somewhat in name.

  @Test public void testZoneId() throws Exception { // case-1 ZoneId zid1 = ZoneId.of("Europe/Moscow"); Assert.assertEquals("ZoneRegion", zid1.getClass().getSimpleName()); // case-2 ZoneId zid2 = ZoneId.of("UTC+4"); Assert.assertEquals("ZoneRegion", zid2.getClass().getSimpleName()); // case-3 ZoneId zid3 = ZoneId.of("+03:00:00"); Assert.assertEquals("ZoneOffset", zid3.getClass().getSimpleName()); // case-4 ZoneId zid4 = ZoneId.ofOffset("UTC", ZoneOffset.of("+03:00:00")); Assert.assertEquals("ZoneRegion", zid4.getClass().getSimpleName()); } 


It is not very clear why case-4, which actually requests the same as case-3, creates java.time.ZoneRegion as a result, and not java.time.ZoneOffset.

  @Test public void testZoneUTC() throws Exception { ZoneId zid1 = ZoneOffset.UTC; Assert.assertEquals("ZoneOffset", zid1.getClass().getSimpleName()); ZoneId zid2 = ZoneId.of("Z"); Assert.assertEquals("ZoneOffset", zid2.getClass().getSimpleName()); Assert.assertSame(ZoneOffset.UTC, zid2); ZoneId zid3 = ZoneId.of("UTC"); Assert.assertEquals("ZoneRegion", zid3.getClass().getSimpleName()); } 


For the UTC time zone, a special constant java.time.ZoneOffset # UTC has been entered, but nevertheless the request for ZoneId.of (“UTC”) in the new API already produces an object of class java.util.ZoneRegion, and not this constant.

Clock


" Time is a clock " - as claimed by some physicists. And this phrase is the key to the new API, where the java.time.Clock class is the cornerstone. And just like some of our watches, time for us can be: constant (non-existent), late, running with varying degrees of accuracy, moving the hands differently in different time zones. In general, in the new API, you can use (or define yourself) practically any course of time, including for testing tests.

A standard java.time.Clock instance can be created only by factory static methods (the class itself is abstract).

A standard java.time.Clock instance always knows about the time zone in which it was created (although this is sometimes unnecessary).

Let's go through the factory methods:


You can override java.time.Clock and write any logic of time output, for example, a clock that gives random time for each request, why not?

The java.time.Clock object has only three working methods:


Now a little criticism:


Instant


java.time.Instant is the new java.util.Date, only immutable, with nanosecond precision and the correct name. Inside it stores Unix-time in the form of two fields: long with the number of seconds, and int with the number of nanoseconds within the current second.

The value of both fields can be requested directly, and you can also ask to calculate the Unix-time representation that is more familiar to the old API in the form of milliseconds:
  @Test public void testInstantFields() throws Exception { Instant instant = Clock.systemDefaultZone().instant(); System.out.println(instant.getEpochSecond()); System.out.println(instant.getNano()); System.out.println(instant.toEpochMilli()); } 


As well as java.util.Date (if used correctly), an object of the java.time.Instant class does not know anything about the time zone.

Separately it is necessary to say about the java.time.Instant.toString () method. If earlier java.util.Date.toString () worked taking into account the current locale and the default time zone, the new java.time.Instant.toString () always forms a textual representation in the UTC time zone and the same ISO-8601 format - this applies and output variables in the IDE when debugging:
  @Test public void testInstantString() throws Exception { Instant instant1 = Clock.system(ZoneId.of("Europe/Paris")).instant(); System.out.println(instant1.toString()); Instant instant2 = Clock.systemUTC().instant(); System.out.println(instant2.toString()); Instant instant3 = Clock.systemDefaultZone().instant(); System.out.println(instant3.toString()); } 

 2016-01-06T15: 22: 53.403Z
 2016-01-06T15: 22: 53.417Z
 2016-01-06T15: 22: 53.423Z


Basic interfaces


Let's look at the basic java.time.temporal.TemporalAccessor interface. The TemporalAccessor interface is a directory for requesting individual partial information on the current point or label and all temporary classes of the new API implement it.

Ask for the Unix-time value of java.time.Instant:
  @Test(expected = DateTimeException.class) public void testTemporalAccessor2() throws Exception { TemporalAccessor ta = Clock.systemUTC().instant(); // java.time.DateTimeException: Invalid value for InstantSeconds \ // (valid values -9223372036854775808 - 9223372036854775807): 1451983908 System.out.println(ta.get(ChronoField.INSTANT_SECONDS)); } 


We get an exception with a completely inexplicable message:
 java.time.DateTimeException: Invalid value for InstantSeconds \
                          (valid values ​​-9223372036854775808 - 9223372036854775807): 1451983908


After a bit of a jogging, the reason for the exception becomes clear: theoretically, the result may not fit into the int range (although it currently fits). The INSTANT_SECONDS field must be requested as long. We will correct the request, along the way we will request additional meta-information:
  @Test public void testTemporalAccessor3() throws Exception { TemporalAccessor ta = Clock.systemUTC().instant(); System.out.println(ta.getLong(ChronoField.INSTANT_SECONDS)); ValueRange vr = ta.range(ChronoField.INSTANT_SECONDS); System.out.println(vr.getMinimum()); System.out.println(vr.getMaximum()); System.out.println(ta.isSupported(ChronoField.INSTANT_SECONDS)); System.out.println(ta.isSupported(ChronoField.CLOCK_HOUR_OF_DAY)); } 

 1452094053
 -9223372036854775808
 9223372036854775807
 true
 false


The CLOCK_HOUR_OF_DAY field is not supported by the Instant type. This is completely expected, since to find out the hour of the day at a time point we need to specify a time zone, which is not in java.time.Instant. Let's try all the same to request this value:
  @Test(expected = UnsupportedTemporalTypeException.class) public void testTemporalAccessor1() throws Exception { TemporalAccessor ta = Clock.systemUTC().instant(); // java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: ClockHourOfDay System.out.println(ta.getLong(ChronoField.CLOCK_HOUR_OF_DAY)); } 


That's right - when we request an hour of the day, we get an exception. It's great that the request method did not use the default time zone (which is not in the new API).

In addition to querying individual fields, you can query values ​​using more complex algorithms-strategies that inherit the java.time.TemporalQuery interface:
  @Test public void testTemporalAccessor4() throws Exception { TemporalAccessor ta = Clock.systemUTC().instant(); ZoneId zoneId1 = ta.query(TemporalQueries.zone()); ZoneId zoneId2 = TemporalQueries.zone().queryFrom(ta); Assert.assertEquals(zoneId1, zoneId2); TemporalUnit unit1 = ta.query(TemporalQueries.precision()); TemporalUnit unit2 = TemporalQueries.precision().queryFrom(ta); Assert.assertEquals(unit1, unit2); } 


java.time.temporal.Temporal - the interface is a successor of the TemporalAccessor interface. Enters forward and backward point / tag shift operations, the operation of replacing a portion of temporal information, as well as the operation to calculate the distance to another time point / mark. It is implemented by almost all the “full-fledged” temporary classes of the new API.

We try to move the label a day ahead and calculate the difference:
  @Test public void testTemporal1() throws Exception { Temporal t1 = Clock.systemUTC().instant(); Temporal t2 = t1.plus(1, ChronoUnit.DAYS); Assert.assertEquals(Duration.ofDays(1).getSeconds(), t2.getLong(ChronoField.INSTANT_SECONDS) - t1.getLong(ChronoField.INSTANT_SECONDS)); Assert.assertEquals(24, t1.until(t2, ChronoUnit.HOURS)); Assert.assertEquals(24, Duration.between(t1, t2).get(ChronoUnit.HOURS)); } 


Since all classes have finally become immutable, the results of the operations must not be forgotten to be assigned to another variable, since the original does not change during the operation - everything is the same as java.lang.String or java.math.BigDecimal.

Let's try to change the hour of the day in java.time.Instant:
  @Test(expected = UnsupportedTemporalTypeException.class) public void testTemporal2() throws Exception { Temporal t = Clock.systemUTC().instant(); // java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: HourOfDay t.with(ChronoField.HOUR_OF_DAY, 2); } 


Expectedly we get our hands, because a time zone is necessary for this operation.

java.time.temporal.TemporalAdjuster - interface of the time point / tag correction strategy, for example, moving on the first day of the current code. Previously, you had to write your own auxiliary classes for working with java.util.Calendar fields - now you can put all the code in the form of a strategy, if you don’t have it yet in the standard delivery:
  @Test public void testTemporalAdjuster() throws Exception { ZonedDateTime zdt = ZonedDateTime.of(2005, 10, 30, 0, 0, 0, 0, ZoneId.of("Europe/Moscow")); ZonedDateTime zdt1 = zdt.with(TemporalAdjusters.firstDayOfYear()); ZonedDateTime zdt2 = (ZonedDateTime) TemporalAdjusters.firstDayOfYear().adjustInto(zdt); Assert.assertEquals(zdt1, zdt2); Assert.assertEquals(2005, zdt1.get(ChronoField.YEAR)); Assert.assertEquals(1, zdt1.get(ChronoField.MONTH_OF_YEAR)); Assert.assertEquals(1, zdt1.get(ChronoField.DAY_OF_MONTH)); } 


Now you can go to the temporary classes.

LocalTime, LocalDate, LocalDateTime


java.time.LocalTime is a tuple (hour, minute, second, nanosecond)
java.time.LocalDate is a tuple (year, month, day of month)
java.time.LocalDateTime - both tuples together

I would also refer specific classes for storing a part of information to the same classes: java.time.MonthDay , java.time.Year , java.time.YearMonth

All these classes are united by the fact that they contain timestamps or parts of them, but they cannot determine the time points on the time axis themselves (even LocalDateTime) - since none of them have a time zone or even an offset.

These classes, like all the others, support the java.lang.Comparable interface, but you need to understand that this is exactly a comparison of timestamps, not time points:
  @Test public void testLocalDateTime() throws Exception { ZonedDateTime zdt1 = ZonedDateTime.of(2015, 1, 10, 15, 0, 0, 0, ZoneId.of("Europe/Moscow")); ZonedDateTime zdt2 = ZonedDateTime.of(2015, 1, 10, 14, 0, 0, 0, ZoneId.of("Europe/London")); Assert.assertEquals(-1, zdt1.compareTo(zdt2)); LocalDateTime ldt1 = zdt1.toLocalDateTime(); LocalDateTime ldt2 = zdt2.toLocalDateTime(); Assert.assertEquals(+1, ldt1.compareTo(ldt2)); } 


It must be said that despite the inevitable parallels in use between java.time.LocalTime and java.sql.Time, as well as between java.time.LocalDate and java.sql.Date are totally different classes. In the old API, the classes java.sql.Time and java.sql.Date are descendants of java.util.Date, which means that their interpretation (getting the hour value for example) depends on the time zone in which the object of this class was created and on the time zone in which this object will be read. In the new API, the java.time.LocalTime and java.time.LocalDate classes are honest tuples of values, and the time zone does not participate in any way when writing and reading the hour value.

However, the time zone is necessary when creating them from a time point, since the interpretation of the days and hours depends on it:
  @Test(expected = DateTimeException.class) public void testLocalDateTimeCreate1() throws Exception { Clock clock = Clock.system(ZoneId.of("Europe/Moscow")); // java.time.DateTimeException: Unable to obtain LocalDateTime \ // from TemporalAccessor: 2016-01-11T15:15:03.180Z of type java.time.Instant LocalDateTime ldt = LocalDateTime.from(clock.instant()); } 


An exception is thrown, due to the fact that the time zone is simply nowhere to take (in Instant it does not exist, and we do not take the zone by default). But it can be obtained either from the java.time.Clock, or you can transfer it additionally:
  @Test public void testLocalDateTimeCreate2() throws Exception { Clock clock = Clock.system(ZoneId.of("Europe/Moscow")); LocalDateTime ldt1 = LocalDateTime.ofInstant(clock.instant(), ZoneId.of("UTC")); System.out.println(ldt1); LocalDateTime ldt2 = LocalDateTime.now(clock); System.out.println(ldt2); } 


Now everything works, but the ease with which you can make a mistake is somewhat alarming.

In the comments to the previous article it was mentioned that the real paranoids should also indicate the calendar during operations with calendar values ​​(which includes the creation of objects of all time classes except Instant). The new API has several calendars, which are called chronologies:
  @Test public void testChronology() throws Exception { Clock clock = Clock.system(ZoneId.of("Europe/Moscow")); ZonedDateTime zdt = ZonedDateTime.now(clock); ChronoLocalDateTime dt1 = IsoChronology.INSTANCE.localDateTime(zdt); System.out.println(dt1); // 2016-01-11T18:48:15.145 ChronoLocalDateTime dt2 = JapaneseChronology.INSTANCE.localDateTime(zdt); System.out.println(dt2); // Japanese Heisei 28-01-11T18:48:15.145 ChronoLocalDateTime dt3 = ThaiBuddhistChronology.INSTANCE.localDateTime(zdt); System.out.println(dt3); // ThaiBuddhist BE 2559-01-11T18:48:15.145 } 


, ISO-8601 IsoChronology ( ), , , API .

ZonedDateTime


java.time.ZonedDateTime — java.util.Calendar. , , .

ZonedDateTime LocalDateTime:
  @Test(expected = DateTimeException.class) public void testZoned1() throws Exception { LocalDateTime ldt = LocalDateTime.of(2015, 1, 10, 0, 0, 0, 0); // java.time.DateTimeException: Unable to obtain ZonedDateTime from TemporalAccessor: 2015-01-10T00:00 of type java.time.LocalDateTime ZonedDateTime zdt = ZonedDateTime.from(ldt); } 


, ( LocalDateTime) , - API ( ).

:
  @Test public void testZoned2() throws Exception { LocalDateTime ldt = LocalDateTime.of(2015, 1, 10, 0, 0, 0, 0); ZonedDateTime zdt = ZonedDateTime.of(ldt, ZoneId.of("Europe/Moscow")); } 


Let's see how strict the ZonedDateTime is with respect to incorrectly specified dates. In java.util.Calendar there is a lenient switch that can be configured for both “strict” and “soft” mode. In the new API, there is no such switch.

February 29 is not in a leap year will not pass:
  @Test(expected = DateTimeException.class) public void testLenient2() throws Exception { // java.time.DateTimeException: Invalid date 'February 29' as '2005' is not a leap year ZonedDateTime.of(2005, 2, 29, 2, 30, 0, 0, ZoneId.of("Europe/Moscow")); } 


60th second can not be specified:
  @Test(expected = DateTimeException.class) public void testLenient3() throws Exception { // java.time.DateTimeException: Invalid value for SecondOfMinute (valid values 0 - 59): 60 ZonedDateTime.of(2005, 2, 20, 2, 30, 60, 0, ZoneId.of("Europe/Moscow")); } 


But the indication of the label at the time of the transfer of the arrows to summer time passes successfully, and the result differs from the expected. In strict mode java.util.Calendar is not missed (see the previous article ).
  @Test public void testLenient1() throws Exception { ZonedDateTime zdt = ZonedDateTime.of(2005, 3, 27, 2, 30, 0, 0, ZoneId.of("Europe/Moscow")); Assert.assertEquals(3, zdt.getLong(ChronoField.HOUR_OF_DAY)); Assert.assertEquals(30, zdt.getLong(ChronoField.MINUTE_OF_HOUR)); } 


I will not write anything about operations in ZonedDateTime - you can see the documentation.

OffsetTime, OffsetDateTime


java.time.OffsetTime — LocalTime + ZoneOffset
java.time.OffsetDateTime — LocalDateTime + ZoneOffset

, ( — , ), OffsetDateTime ZonedDateTime. OffsetDateTime , , .

, ( JavaScript ). , ZonedDateTime — . , OffsetDateTime .


API : java.time.Instant, java.time.ZonedDateTime java.time.OffsetTime.

java.time.ZonedDateTime, .

:
  @Test public void testWinterDay() throws Exception { ZonedDateTime zdt1 = ZonedDateTime.of(2005, 10, 30, 0, 0, 0, 0, ZoneId.of("Europe/Moscow")); // case #1 - ok ZonedDateTime zdt2 = zdt1.plusDays(1); Assert.assertEquals(25, Duration.between(zdt1, zdt2).toHours()); // case #2 - ok ZonedDateTime zdt3 = zdt1.plus(1, ChronoUnit.DAYS); Assert.assertEquals(25, Duration.between(zdt1, zdt3).toHours()); // case #3 - ok OffsetDateTime odt1 = zdt1.toOffsetDateTime(); OffsetDateTime odt2 = zdt2.toOffsetDateTime(); Assert.assertEquals(25, Duration.between(odt1, odt2).toHours()); // case #4 - ??? OffsetDateTime odt3 = zdt1.toOffsetDateTime(); OffsetDateTime odt4 = odt3.plus(1, ChronoUnit.DAYS); Assert.assertEquals(24, Duration.between(odt3, odt4).toHours()); // case #5 - ok Instant instant1 = Instant.from(zdt1); Instant instant2 = Instant.from(zdt2); Assert.assertEquals(25, Duration.between(instant1, instant2).toHours()); // case #6 - ??? Instant instant3 = Instant.from(zdt1); Instant instant4 = instant3.plus(1, ChronoUnit.DAYS); Assert.assertEquals(24, Duration.between(instant3, instant4).toHours()); // case #7 - ??? LocalDateTime localDateTime1 = LocalDateTime.from(zdt1); LocalDateTime localDateTime2 = localDateTime1.plus(1, ChronoUnit.DAYS); Assert.assertEquals(24, Duration.between(localDateTime1, localDateTime2).toHours()); // case #8 - ??? LocalDateTime localDateTime3 = LocalDateTime.from(zdt1); LocalDateTime localDateTime4 = LocalDateTime.from(zdt2); Assert.assertEquals(24, Duration.between(localDateTime3, localDateTime4).toHours()); } 


case#1 case#2 ZonedDateTime , 25 .

case#3 , OffsetDateTime , case#4 , .

case#5 case#6 — , Instant , .

case#7 case#8 — , LocalDateTime , .

, API ( - ). . — Java-. API , java.util.Calendar, , — .

Perhaps it was worth banning most operations with time in all classes except ZonedDateTime, since only he is aware of the arrow translations. It might be worthwhile to prohibit the calculation of Duration using LocalDateTime, since without a time zone it does not define a time point. I am not ready now to somehow seriously discuss the possibility or impossibility of such solutions, but I have a feeling of danger from the new API.

Period, Duration


The new API has two classes for determining duration.

java.time.Period - description of the calendar duration (period) in the form of a tuple (year, month, day).

java.time.Duration - description of the exact duration in the form of an integer number of seconds and fractions of the current second in the form of nanoseconds.

The difference between the two can be shown in the example with the day of the transfer of hands to winter time. Due to the switch back, this calendar day consists of 25 hours.
  @Test public void testDuration() throws Exception { Period period = Period.of(0, 0, 1); Duration duration = Duration.of(1, ChronoUnit.DAYS); ZonedDateTime zdt1 = ZonedDateTime.of(2005, 10, 30, 0, 0, 0, 0, ZoneId.of("Europe/Moscow")); ZonedDateTime ztd2 = zdt1.plus(period); Assert.assertEquals(ZonedDateTime.of(2005, 10, 31, 0, 0, 0, 0, ZoneId.of("Europe/Moscow")), ztd2); ZonedDateTime ztd3 = zdt1.plus(duration); Assert.assertEquals(ZonedDateTime.of(2005, 10, 30, 23, 0, 0, 0, ZoneId.of("Europe/Moscow")), ztd3); } 


When adding Period.of (0, 0, 1), we correctly move to the next calendar day. In the case of adding Duration.of (1, ChronoUnit.DAYS), we actually add 24 hours and do not switch to the next calendar day.

Formatting and parsing


API , java.text.SimpleDateFormat -. - , SimpleDateFormat - .

API .

java.time.format.DateTimeFormatter — .

  @Test public void testFormat() throws Exception { DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:dd z", Locale.ENGLISH); ZonedDateTime zdt1 = ZonedDateTime.of(2005, 10, 30, 0, 0, 0, 0, ZoneId.of("Europe/Moscow")); String text = zdt1.format(formatter); System.out.println(text); TemporalAccessor ta = formatter.parse(text); // java.time.format.Parsed ZonedDateTime zdt2 = ZonedDateTime.from(ta); Assert.assertEquals(zdt1, zdt2); } 


JavaDoc , API . , , java.time.Temporal (java.time.format.Parsed ), , , , .


I will give the class diagram of the new API. Some minor classes are not shown, as well as the implementation of such interfaces as java.util.Serializable and java.lang.Comparable.

Basic interfaces



Era



Time zone



Durations and periods



Chronology




Temporary classes




Compatibility


For the exchange of information between the old and new API implemented several methods. Moreover, it is implemented quite competently: the old API knows about the new API, but the new API does not know anything about the old API at all. Purely theoretically, this will allow one to throw out all the old classes, but I doubt that this will happen in our life.



  @Test public void testTimeZoneCompat() throws Exception { ZoneId zoneId1 = ZoneId.of("Europe/Moscow"); TimeZone timeZone = TimeZone.getTimeZone(zoneId1); ZoneId zoneId2 = timeZone.toZoneId(); Assert.assertEquals(zoneId1, zoneId2); } @Test public void testDateCompat() throws Exception { Instant instant1 = Clock.systemUTC().instant(); Date date = Date.from(instant1); Instant instant2 = date.toInstant(); Assert.assertEquals(instant1, instant2); } 


: java.util.Date , API , . , java.lang.System#currentTimeMillis, , .

findings


API. , : ZonedDateTime, runtime . , API . — .

Source: https://habr.com/ru/post/274905/


All Articles