Logo Java 8

Java 8 – What’s new ? – 2/3 – Date and Time

Java 8Dans la partie précédente, nous avons vu tout ce qu’apporte le Projet Lambda à Java. Mais Java 8 ce n’est pas seulement les lambdas et un zeste de programmation fonctionnelle. C’est aussi de nombreuses améliorations tout aussi révolutionnaires. Il aura fallu plus de dix ans pour que les développeurs soient entendus, mais le jour du salut est bientôt là ; nous allons enfin avoir une API de manipulation de dates digne de ce nom qui va se substituer à JodaTime.

Note importante : Le JDK 8 n’étant pas encore à l’état finalisé, il est possible qu’il y ait des différences entre les fonctionnalités décrites dans cet article et celles proposées dans la version finale.

Pour cet article, le JDK 8 build b94 a été utilisée. Le dernier build peut être téléchargé depuis jdk8.java.net.

L’intégralité des sources est disponible via Github.

java.time – JSR 310

La nouvelle API Date and Time est l’une des fonctionnalités les plus attendu et ce depuis de nombreuses années. Avec les JDK 7 et antérieures, grâce à la méthode statique System.currentTimeMillis(), il est possible de récupérer le temps écoulé, en millisecondes, depuis le 1er janvier 1970. Si vous préférez utiliser des objets, il y a la classe java.util.Date, dont la plupart des méthodes ont été dépréciées dans la JDK8. Quant à la classe java.util.GregorianCalendar, elle permet d’effectuer – entre autre – des opérations sur les dates, comme par exemple ajouter ou soustraire x heures. Dans l’ensemble, ces classes et ces méthodes ne sont pas pratiques à utiliser. De ce fait, l’ancienne API a été délaissée par les développeurs au profit de JodaTime.

Il est donc indiscutable que nous avions besoin de quelque chose de simple à utiliser, de performant et d’objets immuables, dans la continuité de JodaTime. Mais la JSR 310 n’est pas de un copier/coller de JodaTime, elle en est seulement inspirée, pour les raisons évoquées dans ce post. Stephen Colebourne, le créateur de JodaTime est aussi co-leader de la JSR 310.

La nouvelle API

Cette nouvelle API est basée sur deux différents modèles de conception du temps. Le temps Machine et le temps Humain. Pour une machine, le temps n’est qu’un entier augmentant depuis l’epoch (01 janvier 1970 00h00min00s0ms0ns). Pour un humain en revanche, il s’agit d’une succession de champs ayant une unité (année, mois, jours, heure, etc.).

Les principes architecturaux autour desquels a été conçu la nouvelle API sous les suivants :

  • Immuabilité et thread safety : Toutes les classes centrales de l’API Date and Time sont immuables, ce qui nous assure de ne pas avoir à nous soucier de problèmes de concurrence. De plus, qui dit objets immuables, dit des objets simples à créer, à utiliser et à tester.
  • Chaînage : les méthodes chaînables rendent le code plus lisible et elles sont aussi plus simples à apprendre. Quant aux méthodes de type factory (par exemple: now(), from(), etc.) elles sont utilisées en lieu et place de constructeurs.
  • Clareté : Chaque méthode définie clairement ce qu’elle fait. De plus, hormis dans quelques cas particuliers, passer un paramètre nul à une méthode provoquera la levée d’un NullPointerException. Les méthodes de validation prenant des objets en paramètre et retournant un booléen retournent généralement “false” lorsque null est passé.
  • Extensibilité : Le design pattern Stratégie utilisé à travers l’API permet son extension en évitant toute confusion. Par exemple, bien que les classes de l’ API soient basées sur le système de calendrier ISO-8601, nous pouvons aussi utiliser les calendriers non-ISO – tel que le calendrier Impérial Japonais – qui sont inclus dans l’API, ou même créer votre propre calendrier.

Le temps Machine

Pour commencer, voyons deux classes associées au temps machine, java.time.Instant et java.time.Duration.

java.time.Instant

La classe java.time.Instant représente un point relatif à l’epoch.

/*
 * org.isk.datetime.MachineTimeTest
 */
@Test
public void instant() {
    //---- Instant.EPOCH
    Assert.assertEquals("1970-01-01T00:00:00Z", Instant.EPOCH.toString());
    Assert.assertEquals(Instant.parse("1970-01-01T00:00:00Z"), Instant.EPOCH);
    Assert.assertEquals(Instant.ofEpochSecond(0), Instant.EPOCH);

    //---- Instant.MIN
    Assert.assertEquals(Instant.parse("-1000000000-01-01T00:00:00Z"), Instant.MIN);

    //---- Instant.MAX
    Assert.assertEquals(Instant.parse("+1000000000-12-31T23:59:59.999999999Z"), Instant.MAX);

    //---- Few instance methods
    final Instant instant = Instant.now();

    // prints the current time
    // e.g. 2013-05-26T21:37Z (Coordinated Universal Time)
    System.out.println("Instant.now() : " + instant);

    // print the number of nano seconds
    System.out.println("Instant.now().getNano() : " + instant.getNano());

    //-- Working with 2 instants
    // 2013-05-26T23:10:40Z & 1530-05-26T23:10:40Z
    final Instant instant20130526_231040 = Instant.parse("2013-05-26T23:10:40Z");
    final Instant instant15300526_231040 = Instant.parse("1530-05-26T23:10:40Z");

    // 2013-05-26T23:10:40Z is After 1530-05-26T23:10:40Z
    Assert.assertTrue(instant20130526_231040.isAfter(instant15300526_231040));

    // 2013-05-26T23:10:40Z is NOT Before 1530-05-26T23:10:40Z
    Assert.assertFalse(instant20130526_231040.isBefore(instant15300526_231040));

    // 2013-05-26T23:10:40Z minus 1 hour (3600s)
    Assert.assertEquals(Instant.parse("2013-05-26T22:10:40Z"),
                        instant20130526_231040.minusSeconds(3600));
}

Source

  • Instant.EPOCH : représente l’epoch
  • Instant.MIN : représente la plus petite valeur (pour les dates avant Jésus-Christ)
  • Instant.MAX : représente la plus grande valeur possible (31 décembre un trillion).
  • Instant.parse() : retourne un objet de type Instant à partir d’une chaîne de caractère représentant une date au format ISO-8601. Si la chaîne de caractères ne représente pas une valeur valide, une exception de type java.time.format.DateTimeParseException sera levée (le standard ISO-8601 spécifie que la lettre “T” désigne l’heure qu’elle précède et “Z” une data UTC).
  • Instant.ofEpochSecond()/Instant.ofEpochMilliSecond() : retourne un objet de type Instant à partir d’un offset de x secondes ou millisecondes par rapport à l’epoch.
  • Instant.now() : retourne un objet de type Instant représentant la date UTC actuelle.
  • Instant.now().getNano() : retourne le nombre de nano secondes de la date actuelle retournée par Instant.now().

Les méthodes isAfter(), isBefore(), minusSeconds(), etc. font ce que leur nom indique.

Note : La méthode toString() sur une instance d’un objet Instant retourne la date au format ISO-8601.

java.time.Duration

La classe java.time.Duration représente une durée.

/*
 * org.isk.datetime.MachineTimeTest
 */
@Test
public void duration() {
    //---- Duration.ZERO
    Assert.assertEquals(Duration.parse("PT0S"), Duration.ZERO);
    Assert.assertEquals("PT0S", Duration.ZERO.toString());

    //---- 2h 5min 30s 345ms = 7_530_345ms
    final Duration duration = Duration.ofMillis(7_530_345);
    Assert.assertEquals("PT2H5M30.345S", duration.toString());
    Assert.assertEquals(7530, duration.getSeconds());
    Assert.assertEquals(345000000, duration.getNano());

}

Source

  • Duration.ZERO : représente une durée nulle.
  • Duration.parse() : retourne un objet de type Duration à partir d’une chaîne de caractère représentant une durée au format ISO-8601. Si la chaîne de caractères ne représente pas une valeur valide, une exception de type java.time.format.DateTimeParseException sera levée (selon le standard ISO-8601 une durée commence P – pour Period- et est suivie d’une valeur comme par exemple 4DT11H9M8S – pour Days, Hours, Minutes et Seconds – où la date et l’heure sont séparées par la lettre “T” – pour Time -). Le standard permet aussi de définir un nombre d’années et de mois, ce que ne permet pas la méthode parse().
  • Duration.ofMillis() : retourne un objet de type Duration à partir d’une chaîne de caractères représentant une durée en millisecondes.

La classe Duration, à l’instar de Instant, possède, différents getters et méthodes de manipulation telles que plusX(), minusX(), etc. où X correspond à Days, Hours, Minutes, etc. ainsi que withY() où Y correspond à Seconds ou Nanos (ces méthodes retournent une copie de la durée ajustée avec la valeur passée en paramètre).

Les classes Instant et Duration étant immuables, les méthodes plusX(), minusX(), withY(), etc. retournent toujours de nouvelles instances.

Le temps Humain

LocalDate, LocalTime, et LocalDateTime

Les classes java.time.LocalDate, java.time.LocalTime et java.time.LocalDateTime représentent des dates et heures système sans indication du fuseau horaire.

Les morceaux de code ci-dessous étant suffisamment clairs, je vous invite à les lire ainsi que leurs commentaires associés.

Les constantes

Divers constantes sont disponibles :

/*
 * org.isk.datetime.HumanTimeTest
 */
@Test
public void constants() {
    //---- LocalDate
    // LocalDate.MIN & LocalDate.MAX
    Assert.assertEquals("-999999999-01-01", LocalDate.MIN.toString());
    Assert.assertEquals("+999999999-12-31", LocalDate.MAX.toString());

    //---- LocalTime
    // LocalTime.MIN & LocalTime.MAX
    Assert.assertEquals("00:00", LocalTime.MIN.toString());
    Assert.assertEquals("23:59:59.999999999", LocalTime.MAX.toString());

    // LocalTime.NOON & LocalTime.MIDNIGHT
    // There is no mention of AM and PM
    Assert.assertEquals("12:00", LocalTime.NOON.toString());
    Assert.assertEquals("00:00", LocalTime.MIDNIGHT.toString());

    //---- LocalDateTime
    // LocalDateTime.MIN & LocalDateTime.MAX
    Assert.assertEquals("-999999999-01-01T00:00", LocalDateTime.MIN.toString());
    Assert.assertEquals("+999999999-12-31T23:59:59.999999999", LocalDateTime.MAX.toString());
}

Source

La méthode statique now()

La méthode statique now() retourne la date du système avec le fuseau horaire pris en compte, sauf si indiqué autrement en paramètre.

/*
 * org.isk.datetime.HumanTimeTest
 */
@Test
public void now() {
    //---- LocalDate
    // Current System Date
    // e.g. 2013-05-26
    System.out.println("LocalDate.now(): " + LocalDate.now());

    // Current UTC System Date
    // e.g. 2013-05-26
    System.out.println("LocalDate.now(Clock.systemUTC()): " + LocalDate.now(Clock.systemUTC()));

    //---- LocalTime
    // Current System Time
    //e.g. 21:35:45.977
    System.out.println("LocalTime.now(): " + LocalTime.now());

    // Current UTC System Time
    //e.g. 19:35:45.977
    System.out.println("LocalTime.now(Clock.systemUTC()): " + LocalTime.now(Clock.systemUTC()));

    //---- LocalDateTime
    // Current System Date and Time
    //e.g. 2013-05-26T21:35:45.977
    System.out.println("LocalDateTime.now(): " + LocalDateTime.now());

    // Current UTC System Date and Time
    //e.g. 2013-05-26T19:35:45.977
    System.out.println("LocalDateTime.now(Clock.systemUTC()): " + LocalDateTime.now(Clock.systemUTC()));
}

Source

Les méthodes statiques de construction

Il est possible de construire des dates à partir de différents formats.

/*
 * org.isk.datetime.HumanTimeTest
 */
@Test
public void localDateStaticMethods() {
    // LocalDate from a string
    final LocalDate localDateStr = LocalDate.parse("2013-05-23");
    Assert.assertEquals("2013-05-23", localDateStr.toString());

    // LocalDate from 3 integers (year, month, day)
    final LocalDate localDate = LocalDate.of(2013, 05, 26);
    Assert.assertEquals("2013-05-26", localDate.toString());

    // LocalDate with an offset from epoch
    final LocalDate oneHundredDaysBeforeEpoch = LocalDate.ofEpochDay(-1000);
    Assert.assertEquals("1967-04-07", oneHundredDaysBeforeEpoch.toString());

    // Copy of a LocalDate
    final LocalDate copyLocalDate = LocalDate.from(localDate);
    Assert.assertEquals("2013-05-26", copyLocalDate.toString());
}

Source

/*
 * org.isk.datetime.HumanTimeTest
 */
@Test
public void localTimeStaticMethods() {
    // LocalTime from a string
    final LocalTime timeStr1 = LocalTime.parse("12:35");
    Assert.assertEquals("12:35", timeStr1.toString());

    final LocalTime timeStr2 = LocalTime.parse("12:35:32.978");
    Assert.assertEquals("12:35:32.978", timeStr2.toString());

    // LocalDate from 3 integers (hour, minute, second)
    // But can be 2 (hour, minute)
    // or 4 (hour, minute, second, nanoseconds)
    final LocalTime timeAsInts = LocalTime.of(10, 22, 17);
    Assert.assertEquals("10:22:17", timeAsInts.toString());

    // LocalTime from a number of seconds after midnight
    final LocalTime oneHourAfterMidnight = LocalTime.ofSecondOfDay(3600);
    Assert.assertEquals("01:00", oneHourAfterMidnight.toString());

    // Copy of a LocalTime
    final LocalTime copyLocalTime = LocalTime.from(timeAsInts);
    Assert.assertEquals("10:22:17", copyLocalTime.toString());
}

Source

/*
 * org.isk.datetime.HumanTimeTest
 */
@Test
public void localDateTimeStaticMethods() {
    // LocalDateTime from a string
    final LocalDateTime localDateTimeStr = LocalDateTime.parse("2013-05-26T10:22:17");
    Assert.assertEquals("2013-05-26T10:22:17", localDateTimeStr.toString());

    // LocalDate from 5 parameters (year, month, day, hour, minute, second)
    // But range from 5 to 7.
    // Note : Month is an enum.
    final LocalDateTime localDateTime = LocalDateTime.of(2013, Month.MAY, 26, 12, 05);
    Assert.assertEquals("2013-05-26T12:05", localDateTime.toString());

    // LocalDateTime from a LocalDate and a LocalTime
    final LocalDate localDate = LocalDate.of(2013, 05, 26);
    final LocalTime localTime = LocalTime.of(12, 35);
    final LocalDateTime localDateTimeOfDateAndTime = LocalDateTime.of(localDate, localTime);
    Assert.assertEquals("2013-05-26T12:35", localDateTimeOfDateAndTime.toString());

    // Copy of a LocalDateTime
    final LocalDateTime copyLocalDateTime = LocalDateTime.from(localDateTime);
    Assert.assertEquals("2013-05-26T12:05", copyLocalDateTime.toString());
}

Source

Les méthodes d’instances

Les classes LocalDate, LocalTime et LocalDateTime ont différentes méthodes permettant de récupérer une partie de la date, de la tester et d’effectuer des opérations dessus.

/*
 * org.isk.datetime.HumanTimeTest
 */
@Test
public void localDateInstanceMethods() {
    final LocalDate localDate = LocalDate.of(2013, 05, 26);

    // Year
    Assert.assertEquals(2013, localDate.getYear());

    // Month
    Assert.assertEquals(Month.MAY, localDate.getMonth());
    Assert.assertEquals(5, localDate.getMonthValue());

    // Day
    Assert.assertEquals(26, localDate.getDayOfMonth());
    Assert.assertEquals(DayOfWeek.SUNDAY, localDate.getDayOfWeek());
    Assert.assertEquals(146, localDate.getDayOfYear());

    // Leap Year
    Assert.assertFalse(localDate.isLeapYear());
    Assert.assertTrue(LocalDate.of(2004, 05, 26).isLeapYear());

    //---- Operations
    final LocalDate localDate2 = LocalDate.of(2013, 04, 26);

    // Before, After, Equal, equals
    Assert.assertTrue(localDate.isAfter(localDate2));
    Assert.assertFalse(localDate.isBefore(localDate2));
    Assert.assertTrue(localDate.isEqual(LocalDate.of(2013, 05, 26)));
    Assert.assertTrue(localDate.equals(LocalDate.of(2013, 05, 26)));

    // plus & minus
    Assert.assertEquals("2013-04-26", localDate.minusMonths(1).toString());
    Assert.assertEquals("2013-06-05", localDate.plusDays(10).toString());

    // Adjusters
    Assert.assertEquals("2013-05-01", localDate.with(TemporalAdjuster.firstDayOfMonth()).toString());
    Assert.assertEquals("2013-05-10", localDate.withDayOfMonth(10).toString());
}

Source

/*
 * org.isk.datetime.HumanTimeTest
 */
@Test
public void localTimeInstanceMethods() {
    final LocalTime localTime = LocalTime.of(12, 35, 25, 452_367_943);

    // Hour, Minute, Second, Nanosecond
    Assert.assertEquals(12, localTime.getHour());
    Assert.assertEquals(35, localTime.getMinute());
    Assert.assertEquals(25, localTime.getSecond());
    Assert.assertEquals(452_367_943, localTime.getNano());

    //---- Operations
    // Before, After, Equal, equals
    final LocalTime localTime2 = LocalTime.of(12, 35, 25, 452_367_942);
    Assert.assertTrue(localTime.isAfter(localTime2));
    Assert.assertFalse(localTime.isBefore(localTime2));
    Assert.assertTrue(localTime.equals(LocalTime.of(12, 35, 25, 452_367_943)));

    /// plus & minus
    Assert.assertEquals("12:25:25.452367943", localTime.minusMinutes(10).toString());
    Assert.assertEquals("17:35:25.452367943", localTime.plusHours(5).toString());

    // Adjusters
    Assert.assertEquals("05:35:25.452367943", localTime.withHour(5).toString());
}

Source

/*
 * org.isk.datetime.HumanTimeTest
 */
@Test
public void localDateTimeInstanceMethods() {
    final LocalDate localDate = LocalDate.of(2013, 05, 26);
    final LocalTime localTime = LocalTime.of(12, 35, 25, 452_367_943);
    final LocalDateTime localDateTime = LocalDateTime.of(localDate, localTime);

    // As LocalDate & LocalTime
    Assert.assertEquals("2013-05-26", localDateTime.toLocalDate().toString());
    Assert.assertEquals("12:35:25.452367943", localDateTime.toLocalTime().toString());

    // Other methods are the same as for LocalDate and LocalTime
}

Source

ZoneId, ZoneOffset, ZonedDateTime, et OffsetDateTime

Un fuseau horaire est une zone de la surface de la Terre dans lequel toutes les localités qui ont le même temps standard. Chaque fuseau horaire a un identifiant (par exemple Europe/Paris) et un décalage par rapport à UTC/Greenwich (comme +01:00) qui change lorsque l’heure d’été est en vigueur.

ZoneId

La classe ZoneId représente un identifiant de fuseau horaire et fournit des règles de conversion entre Instant et LocalDateTime.

/*
 * org.isk.datetime.HumanTimeTest
 */
@Test
public void zoneId() {
    final ZoneId zoneId = ZoneId.systemDefault();
    final ZoneRules zoneRules = zoneId.getRules();
    Assert.assertEquals("Europe/Paris", zoneId.toString());
    Assert.assertEquals("ZoneRules[currentStandardOffset=+01:00]", zoneRules.toString());

    // DST in effect
    Assert.assertTrue(zoneRules.isDaylightSavings(Instant.parse("2013-05-26T23:10:40Z")));
    Assert.assertFalse(zoneRules.isDaylightSavings(Instant.parse("2013-01-26T23:10:40Z")));
}

Source

ZoneOffset

ZoneOffset décrit un offset de fuseau horaire, qui est un temps (généralement en heures) par lequel un fuseau horaire diffère de Greenwich.

/*
 * org.isk.datetime.HumanTimeTest
 */
@Test
public void zoneOffset() {
    final ZoneOffset zoneOffset = ZoneOffset.of("+06:00");
    Assert.assertEquals("+06:00", zoneOffset.toString());
    Assert.assertEquals(21600, zoneOffset.getTotalSeconds());
}

Source

ZonedDateTime

La classe ZonedDateTime représente une date avec un fuseau horaire au format ISO-8601 (comme par exemple 2012-05-26T10:15:30+02:00 Europe/Paris).

/*
 * org.isk.datetime.HumanTimeTest
 */
@Test
public void zonedDateTime() {
    final LocalDateTime localDateTime = LocalDateTime.parse("2013-05-26T10:22:17");
    final ZoneId zoneId = ZoneId.of("Europe/Paris");
    final ZonedDateTime zonedDateTime = ZonedDateTime.of(localDateTime, zoneId);
    Assert.assertEquals("2013-05-26T10:22:17+02:00[Europe/Paris]", zonedDateTime.toString());

    final ZoneOffset zoneOffset = ZoneOffset.from(zonedDateTime);
    Assert.assertEquals("+02:00", zoneOffset.toString());
}

Source

OffsetDateTime

La classe OffsetDateTime représente une date avec un offset par rapport à Greenwich, au format ISO-8601 (comme par exemple 2012-05-26T10:15:30+02:00) sans indication de localité.

Les classes ZonedDateTime et OffsetDateTime sont similaires, elles représentent des dates avec des offsets par rapport à Greenwich. Cependant, la classe ZonedDateTime permet d’identifier certaines ambiguïtés. Par exemple, lors d’un changement d’heure, une même heure peut apparaître deux fois à une heure de décalage. La classe OffsetDateTime ne prend pas en compte ce cas, contrairement à ZonedDateTime.

/*
 * org.isk.datetime.HumanTimeTest
 */
@Test
public void offsetDateTime() {
    final LocalDateTime localDateTime = LocalDateTime.parse("2013-05-26T10:22:17");
    final ZoneOffset zoneOffset = ZoneOffset.of("+02:00");
    final OffsetDateTime offsetDateTime = OffsetDateTime.of(localDateTime, zoneOffset);
    Assert.assertEquals("2013-05-26T10:22:17+02:00", offsetDateTime.toString());
}

Source

Note : Les méthodes of(), from() et with() sont comparables à celles que nous avons vu dans la partie précédente.

Avant de passer à la suite

La nouvelle API Date and Time surmonte divers problèmes des anciennes APIs Date and Time. Elle est organisée autour du package principal java.time et de quatre sous-packages. Même si nous utiliserons le plus souvent les classes Instant, Duration, LocalDate, LocalTime, LocalDateTime, ZoneId, ZoneOffset, ZonedDateTime, et OffsetDateTime, il existe d’autres types qui méritent notre attention.

Pour un utilisateur de JodaTime, il n’y a pas vraiment de différence. En revanche, si vous continuez à utiliser l’ancienne API, la nouvelle disponible avec Java 8 vous changera la vie.

What’s next ?

Dans la partie suivante de cet article nous nous intéresserons aux Annotations et à Nashorn, le nouveau moteur JavaScript.

Nombre de vue : 985

COMMENTAIRES 3 commentaires

  1. Nobre dit :

    Il manque un exemple sur les différentes méthodes liée aux Duration et Instant, ex :

    Duration.between(Instant.parse("2011-07-28T00:00:00.000Z"),
    Instant.parse("2014-03-18T00:00:00.000Z")).getSeconds();

  2. fagu dit :

    Enfin une API pour le temps !
    Mais elle ne respecte pas la norme ISO8601 :
    Ne fonctionne pas :

    LocalDate.parse("2011");
    LocalDate.parse("201107");
    LocalDate.parse("2011-07");
    LocalDate.parse("20110728"); // par contre 2011-07-28 fonctionne !
    LocalDate.parse("2011214");
    LocalDate.parse("2011-214");
    LocalDate.parse("2011W316");
    LocalDate.parse("2011-W31-6");

    // le fuseau horaire bloque le parse
    LocalDateTime.parse("2011-07-28T12:34:00.000+0100");
    etc

    De plus, la norme préconise la virgule comme séparateur, or c’est le point qui est utilisé.

  3. David Wursteisen dit :

    Il est possible de fournir son propre DateTimeFormatterBuilder à la méthode parse pour ces cas là.


    LocalDate date = LocalDate.parse("20110728", new DateTimeFormatterBuilder()
    .appendValue(YEAR, 4)
    .appendValue(MONTH_OF_YEAR, 2)
    .appendValue(DAY_OF_MONTH, 2)
    .toFormatter());
    System.out.println("Date => " + date);

AJOUTER UN COMMENTAIRE