A la découverte de Java 8


Java 8 est maintenant disponible depuis plusieurs semaines, voici un “petit” résumé des fonctionalités de Java 8 que j’ai testé à travers tous les articles sur lesquels je suis tombé.

Default methods sur les Interfaces

Java 8 utilise le mot clef default pour définir une implémentation par défaut au sein d’une interface.
C’est utile pour les développeurs d’API, c’était même indispensable pour la plupart des nouveautés du langage sinon comment enrichir les interfaces comme java.util.Collection sans briser toutes les implémentations existantes ?

public interface A {
    default void printSomething() {
        System.out.println("something");
    }
}

public class Implementation implements A {}

La classe Implementation n’a pas besoin de définir la méthode printSomething.

public class Examples {
    public static void main(String[] args) {
        new Implementation().printSomething();
    }
}

Que ce passe t-il si deux interfaces implémentent la même méthode par défaut et qu’une classe implémente ces deux interfaces ?

public interface B {
    default void printSomething() {
        System.out.println("something else");
    }
}

public class DoesNotCompile implements A,B {}

Et bien le code précédent ne compile pas:

class DoesNotCompile inherits unrelated defaults for printSomething() from types A and B

On est obligé de surcharger la méthode dans l’implémentation pour résoudre le conflit,

public class ItCompiles implements A,B {

    @Override
    public void printSomething() {
        System.out.printf("I can print what I want !");
    }
}

Pour faire référence à une des méthodes par défaut :

public class ItCompilesToo implements A,B {

    @Override
    public void printSomething() {
        B.super.printSomething();
    }
}

Je pense que cette fonctionalité va être très utile pour les développeurs de librairies, on va pouvoir oublier les fameuses classes abstraites dont il fallait hériter pour ne pas risquer de tout casser lors de mises à jour d’interfaces.

Expressions Lambda

Surement une des évolutions les plus attendues, une expression lambda est une sorte de méthode “anonyme”. Jusqu’à aujourd’hui en Java pour simuler ce comportement on utilisait une classe anonyme qui implémentait une interface avec une seule méthode abstraite (SAM).

List<Integer> numbers = asList(10, 1, 1000, 100);

Collections.sort(numbers, new Comparator<Integer>() {
    @Override
    public int compare(Integer a, Integer b) {
        return a.compareTo(b);
    }
});

peut-etre remplacé par :

Collections.sort(numbers, (a, b) -> a.compareTo(b));

Le compilateur est capable de trouver les types des paramètres, pas besoin de les préciser et la méthode tenant sur une ligne, on peut également omettre les {} et le return.

Accès variable locale

Contrairement à une classe anonyme, une expression lambda peut accéder aux variables locales même si celles ci ne sont pas déclarées comme étant final.

Integer n = 4;
Function<Integer, Integer> modulo = (Integer a) -> a % n;

assert modulo.apply(8) == 0;

mais en fait il faut qu’elles le soient, le code suivant ne compile pas :

Integer n = 4;
Function<Integer, Integer> modulo = (Integer a) -> a % n;
n = 8;

avec comme erreur :

local variables referenced from a lambda expression must be final or effectively final

Membre et variable static

Contrairement aux variables locales, les membres de classe sont accessibles en écriture.

public class Lambda {

    private static int staticNumber;
    private int number;

    public void write() {
        Function<Integer, Integer> function = (Integer a) -> staticNumber = a;
        Function<Integer, Integer> function2 = (Integer a) -> number = a;
    }
}

Functional Interfaces

Les expressions lambda ne sont en fait pas si “anonymes” que ça. Elles correspondent en fait à des types spécifiés par des interfaces avec exactement une méthode abstraite. C’est pour ça que les classes anonymes implémentant une interface de type SAM peuvent être remplacées par des expressions lambda.
Java 8 propose l’annotation @FunctionalInterface pour s’assurer qu’une interface ne déclare qu’une seule méthode abstraite.

@FunctionalInterface
public interface Function<T, R> {

    R apply(T t);
}

Si une deuxième méthode abstraite est ajoutée la compilation échoue :

Function is not a functional interface, multiple non-overriding abstract methods found in interface

Lambda

On peut ajouter des méthodes par defaut sur une @FunctionalInterface.

@FunctionalInterface
public interface A {

    default void printSomething() {
        System.out.println("something");
    }

    void print();
}

mais on ne peut pas accéder à ces méthodes depuis une lambda.

A doesNotCompile = () -> printSomething();

Function, Predicate, etc

De nombreuses @FunctionalInterface sont disponibles par défaut. D’anciennes interfaces de l’API ont été migrées, Comparable et Runnable par exemple. Si vous connaissez déjà Guava vous ne serez pas complètement perdu.

Function

Une Function prend un argument et retourne un résultat.

Function<Integer, String> toString = n -> String.valueOf(n);
Function<String, Integer> toInteger = s -> Integer.valueOf(s);

assert "4".equals(toString.apply(4));
assert toInteger.apply("4") == 4;

assert "4".equals(toString.compose(toInteger).apply("4"));
assert toString.andThen(toInteger).apply(4) == 4;

La méthode compose applique la fonction toInteger puis la fonction toString alors que la méthode andThen applique toString puis toInteger.

BiFunction

C’est un spécialisation d’une Function qui prend deux arguments et retourne un résultat.

BiFunction<Integer, String, String> concat = (Integer i, String s) -> s + ": " + i;

assert "un: 1".equals(concat.apply(1, "un"));

UnaryOperator

C’est une Function qui prend un argument et retourne un résultat du même type. Par exemple la fonction identity retourne toujours la valeur passée en argument.

assert UnaryOperator.identity().apply("un").equals("un");

BinaryOperator

Un BinaryOperator est une spécialisation d’une BiFunction dont les paramètres et le résultat partagent le même type.

BinaryOperator<String> concatString = (s1, s2) -> s1.concat(": ").concat(s2);

assert "un: 1".equals(concatString.apply("un", "1"));

Predicate

Un Predicate prend un argument et retourne un booléen.

Predicate<String> isEmpty = s -> s == null || s.isEmpty();
Predicate<String> isTrimmed = s -> s.equals(s.trim());
 
assert isEmpty.test(null) == true;
assert isEmpty.test("") == true;
assert isEmpty.test("not empty") == false;

assert isEmpty.negate().and(isTrimmed).test("not empty") == true;
assert isEmpty.negate().and(isTrimmed).test(" not empty ") == false;

assert isEmpty.or(isTrimmed).test("") == true;
assert isEmpty.or(isTrimmed).test("not empty") == true;

assert Predicate.isEqual("hello").test("hello") == true;
  • negate inverse un prédicat,
  • and et or permettent de chaîner des prédicats selon l’opérateur logique,
  • La méthode statique isEqual teste l’égalité de deux objets selon Object#equals

BiPredicate

Un BiPredicate prend deux arguments et retourne un booléen.

Supplier

Un Supplier ne prend pas d’argument et produit un résultat.

Supplier<String> emptyString = () -> "";

assert "".equals(emptyString.get());

Consumer

Un Consumer prend un argument mais ne retourne pas de résultat.

Consumer<String> print = s -> System.out.println(s);
Consumer<String> hello = s -> System.out.printf("Hello %s !", s);

print.accept("something"); // something
print.andThen(hello).accept("JC"); // JC Hello JC !

La méthode andThen permet de chaîner les consommateurs.

Comparator

Les Comparator sont devenus des FunctionalInterface.

Comparator<Integer> ascending = (a, b) -> a.compareTo(b);

assert ascending.compare(10, 1) > 0;
assert ascending.reversed().compare(10, 1) < 0;

Sans grande surprise reversed inverse la comparaison.
Il existe des méthodes par défaut pour appliquer une Function avant le Comparator dans les différents thenComparating.

A retenir

Ça fait beaucoup d’interfaces fonctionnelles (et je n’ai même pas lister les versions spécifiques pour les types primitifs), ce qu’il faut retenir :

  • s’il n’y a pas de retour, on utilise un Consumer,
  • s’il faut retourner un booléen, on utilise un Predicate,
  • s’il faut produire un numérique primitif (int, double, long), on utilise un (Type)ToIntFunction, (Type)ToDoubleFunction, (Type)ToLongFunction,
  • s’il faut retourner une valeur sans prendre d’argument, c’est un Supplier,
  • si le seul argument de la fonction est un int, double, long on utilise un Int(something), Double(something), Long(Something),
  • si la fonction prend deux arguments, c’est une Bi(something),
  • si la fonction prend deux arguments de même type, c’est un BinaryOperator,
  • si une fonction retourne une valeur de même type que son unique argument, c’est un UnaryOperator,
  • si une fonction prend en argument un type primitif et un autre type sans retourner de valeur, c’est un Obj(Int|Double|Long)Consumer,
  • sinon c’est une Function.

Surcharge de méthode

On peut surcharger une méthode utilisant une FunctionalInterface en paramètre par une autre plus spécifique (la manière classique d’overloading).

Par exemple, un BinaryOperator est une spécialisation d’une BiFunction,

public class A {

    public void is(BiFunction<Boolean, Boolean, Boolean> function) {
        System.out.println("bifunction");
    }

    public void is(BinaryOperator<Boolean> function) {
        System.out.println("binaryoperator");
    }
}

le code suivant affichera "binaryoperator"

new A().is((a, b) -> true);

Par contre le compilateur ne peut pas toujours trouvé “un type le plus spécifique”. Dans le cas où les types en questions n’ont pas de relation, seul le nombre d’arguments de la FunctionalInterface permet de différencier les méthodes surchargées.

@FunctionalInterface
public interface StringPredicate {
    boolean test(String string);
}

public class A {

    public void anotherMethod(Predicate<String> predicate) {
        System.out.println("predicate");
    }

    public void anotherMethod(StringPredicate predicate) {
        System.out.println("string predicate");
    }
}

Le code suivant ne compile pas

new A().anotherMethod(input -> true);
reference to anotherMethod is ambiguous
both method anotherMethod(java.util.function.Predicate<java.lang.String>) in fr.jcgay.example.java.overload.A and method anotherMethod(fr.jcgay.example.java.overload.StringPredicate) in fr.jcgay.example.java.overload.A match

Même si c’est techniquement possible, je ne suis pas sûr que ce soit une super idée de surcharger de telles méthodes, dans le premier cas par exemple la résolution est bien moins claire lors de la manipulation de l’expression lambda que lors de cas classiques de surcharge où l’on manipule directement des types.

Références de méthodes

Java 8 introduit le mot clef :: pour extraire des références de méthodes. C’est utile pour remplacer des expressions lambda qui se contentent de faire appel à une méthode qui existe déjà.

On peut remplacer :

Function<Integer, String> toString = n -> String.valueOf(n);

par :

Function<Integer, String> toString = String::valueOf;

On peut faire référence à des méthodes statiques mais aussi directement depuis une instance,

String hello = new String("hello");
Predicate<String> startsWith = hello::startsWith;

assert startsWith.test("he") == true;

et aux constructeurs,

Supplier<String> newString = String::new;

et aussi aux méthodes d’instance sans référence d’objet en particulier :

List<String> names = Arrays.asList("Barbara", "James", "Mary", "John");
Collections.sort(names, String::compareToIgnoreCase);

Fonctionnement

C’est bien magique mais comment on détermine qu’une méthode existante est compatible avec une FunctionalInterface ?

statique

La plus simple à se représenter, il suffit que la dite méthode ait bien la même signature que la méthode unique de l’interface désirée.

Par exemple, on peut écrire notre Function toString précédente,

Function<Integer, String> toString = new Function<Integer, String>() {
    @Override
    public String apply(Integer integer) {
        return String.valueOf(integer);
    }
};

String#valueOf(Integer) est bien utilisable pour implémenter Function<Integer, String>#apply(Integer), et remplacer une expression lambda par une référence de méthode statique.

instance

Ce type de référence permet de capturer une instance qui sera utilisée lors de l’évaluation de l’expression lambda.

Ainsi notre exemple du début revient à écrire :

String hello = new String("hello");
Predicate<String> startsWith = new Predicate<String>() {
    @Override
    public boolean test(String s) {
        return hello.startsWith(s);
    }
};

instance arbitraire

Ce sont les plus difficiles à se représenter au début. Ces références permettent de pointer des méthodes sur des instances qui seront découvertes à l’exécution. En fait celà veut dire que l’instance en question sera le premier argument de l’expression lambda et le reste des paramètres seront ceux passés à la méthode en référence.

Reprenons l’exemple de la comparaison de chaînes de caractères,

BiFunction<String, String, Integer> concat = new BiFunction<String, String, Integer>() {
    @Override
    public Integer apply(String self, String argument) {
        return self.compareToIgnoreCase(argument);
    }
};

Optional

C’est un conteneur pour une valeur qui peut être null. Ce conteneur possède deux états, contenant une valeur ou ne contenant rien.
Retourner un Optional à la place d’un null permet d’obliger le traitement du cas “pas de valeur de retour”.

Optional<String> hello = Optional.of("hello");

assert hello.isPresent() == true;
assert "hello".equals(hello.get());

Si la valeur contenu est null,

Optional<Object> absent = Optional.ofNullable(null);

assert absent.isPresent() == false;
absent.get() // throws java.util.NoSuchElementException: No value present

La classe Optional regorge de méthodes utilitaires pour transformer/filtrer la valeur qu’il contient, pour retourner une valeur par défaut et également un orElseThrow qui pour le coup n’existe pas dans Guava.

Stream

Un Stream est une séquence d’éléments sur laquelle on peut effectuer des opérations. Un Stream se compose d’une source (un tableau, une collection, etc), de zéro ou plusieurs opérations intermédiaires (transformation du Stream en un autre via filter par exemple) et d’une opération terminale (qui produit le résultat).
Les calculs ne sont effectués qu’à l’initialisation de l’opération finale et la source est consommée que si c’est nécessaire.

On peut créer un Stream à partir d’une collection en utilisant la méthode stream(),

List<Person> persons = asList(
            new Person("John", "Doe", 30),
            new Person("Jane", "Doe", 20),
            new Person("Jim", "Smith", 15)
    );

assert persons.stream().count() == 3;

En utilisant IntStream, DoubleStream, LongStream pour créer des Stream numériques,

assert IntStream.range(0, 10).sum() == 45;

Ou encore avec différentes méthodes utilitaires sur la classe Stream,

Stream.of("a", "b", "c").forEach(System.out::println);

Stream.builder().add("a").add("b").add("c")
        .build()
        .forEach(System.out::println);

Un Stream peut-être infini, dans ce cas il faut avoir une opération stoppante,

Random random = new Random();
Stream.generate(() -> random.nextInt())
        .limit(10)
        .forEach(System.out::println);

new Random().ints()
            .limit(10)
            .forEach(System.out::println);

En gros il y a des Stream un peu partout :)

forEach

Effectue une opération sur chacun des éléments en utilisant un Consumer. C’est une opération terminale qui consomme le Stream. On ne peut pas appeler d’autres opérations après un forEach.

persons.stream()
        .forEach(p -> System.out.println(p.getLastName() + " " + p.getFirstName()));

// Doe John
// Doe Jane
// Smith Jim

filter

Accepte un Predicate pour filtrer les éléments. C’est une opération intermédiaire qui nous permet donc de chaîner d’autres opérations à sa suite.

persons.stream()
        .filter(p -> p.getLastName().startsWith("D"))
        .forEach(System.out::println);

// Person{firstName='John', lastName='Doe', age=30}
// Person{firstName='Jane', lastName='Doe', age=20}

sorted

Une opération intermédiaire qui permet de trier les éléments à l’aide d’un Comparable.

persons.stream()
        .sorted((p1, p2) -> p1.getFirstName().compareTo(p2.getFirstName()))
        .forEach(System.out::println);

// Person{firstName='Jane', lastName='Doe', age=20}
// Person{firstName='Jim', lastName='Smith', age=15}
// Person{firstName='John', lastName='Doe', age=30}

map

Applique une Function sur les éléments, c’est également une opération intermédiaire.

persons.stream()
        .map(Person::getAge)
        .sorted()
        .forEach(System.out::println);
        
// 15
// 20
// 30

allMatch, anyMatch, noneMatch

Plusieurs méthodes permettant de vérifier que zéro/un/des éléments vérifient un Predicate. Toutes ces opérations sont terminales.

assert persons.stream()
        .allMatch(p -> p.getFirstName().startsWith("J"))
    == true;

assert persons.stream()
        .noneMatch(p -> p.getAge() == 35)
    == true;
    
assert persons.stream()
        .anyMatch(p -> "Doe".equals(p.getLastName()))
    == true;

count

Une opération terminale qui retourne le nombre d’élément dans un Stream.

assert persons.stream()
        .filter(p -> p.getAge() >= 20)
        .count()
    == 2;

sum

Retourne la somme de tous les éléments.

assert IntStream.rangeClosed(1, 10).sum() == 55;

reduce

C’est une opération terminale qui réduit les éléments du Stream avec un BinaryOperator.
On obtient au final le résultat sous la forme d’un Optional,

On va créer un monstre en fusionnant nos personnes,

persons.stream()
        .reduce((p1, p2) -> new Person(p1.getFirstName(), p2.getLastName(), p1.getAge() + p2.getAge()))
        .ifPresent(System.out::println);

// Person{firstName='John', lastName='Smith', age=65}

On peut additionner les âges de nos personnes,

assert persons.stream()
        .map(Person::getAge)
        .reduce(0, Integer::sum)
    == 65;

L’argument idendity est l’élément identité de la réduction, il ne doit avoir aucun effet sur la fonction d’accumulation (accumulator dans la signature de reduce). C’est le cas ici puisque x + 0 = x.

Et on peut encore écrire le reduce sous la forme

assert persons.stream()
        .reduce(0,
                (result, person) -> result + person.getAge(),
                (a, b) -> a + b
        )
    == 65;

L’accumulator définit l’opération effectué sur chacun des éléments et retient ce résultat tandis que le combiner décrit l’opération effectuée pour combiner deux résultats de la fonction accumulator.
Cette fois ci identity ne doit pas avoir d’effet sur le combiner.
La fonction combiner doit être associative pour assurer la cohérence du résultat lors du traitement du Stream en parallèle.

collect

Permet de réunir tous les éléments en utilisant un Collector. C’est bien entendu une opération terminale.

persons.stream()
        .map(Person::getLastName)
        .collect(Collectors.toList());

// [Doe, Doe, Smith]

La classe Collectors regorge d’implémentations de Collector pour faciliter l’opération.
On peut en avoir un très bon aperçu dans la présentation de José Paumard : Java 8 Streams & Collectors : patterns, performances, parallélisation

concat

On peut concaténer des Stream.

IntStream.concat(
        IntStream.range(0, 4),
        IntStream.range(4, 6)
).forEach(System.out::print);

// 012345

findFirst, findAny

Des opérations terminales qui permettent de retourner le premier élément du Stream s’il existe en tant qu’Optional,

persons.stream()
        .findFirst()
        .ifPresent(System.out::print);

// Person{firstName='John', lastName='Doe', age=30}

ou encore n’importe quel élément,

persons.stream()
        .parallel()
        .findAny()
        .ifPresent(System.out::print);

Le résultat de findAny n’est pas constant, le résultat peut varier.

flatMap

C’est une opération intermédiraire qui permet de mettre à plat un Stream.
On peut par exemple transformer un Stream<List<Person>> en Stream<Person> via une Function<List<Person>, Stream<Person>> :

Stream.<List<Person>>builder()
        .add(asList(new Person("John", "Doe", 30), new Person("Jane", "Doe", 20)))
        .add(asList(new Person("Jim", "Smith", 15)))
        .build()
        .flatMap(persons -> persons.stream())
        .filter(person -> "Doe".equals(person.getLastName()))
        .forEach(System.out::println);
        
// Person{firstName='John', lastName='Doe', age=30}
// Person{firstName='Jane', lastName='Doe', age=20}

limit, skip

Permet de se déplacer ou de bloquer le nombre d’éléments d’un Stream. Ce sont des opérations intermédiaires.

persons.stream()
        .limit(2)
        .skip(1)
        .findFirst()
        .ifPresent(System.out::print);

// 	Person{firstName='Jane', lastName='Doe', age=20}

min, max

Des opérations terminales pour trouver le minimum ou le maximum (en tant qu’Optional) d’un Stream s’ils existent à l’aide d’un Comparator.

assert IntStream.rangeClosed(1, 10)
        .min()
        .getAsInt()
    == 1;

assert IntStream.rangeClosed(1, 10)
        .max()
        .getAsInt()
    == 10;

peek

Une méthode utile pour débugger entre les opérations efféctuées sur un Stream. C’est une opération intermédiaire qui exécute un Consumer sur chacun des éléments.

persons.stream()
        .filter(p -> "Doe".equals(p.getLastName()))
        .peek(System.out::println)
        .filter(p -> p.getAge() < 25)
        .peek(System.out::println)
        .collect(Collectors.toSet());

// Person{firstName='John', lastName='Doe', age=30}
// Person{firstName='Jane', lastName='Doe', age=20}

// Person{firstName='Jane', lastName='Doe', age=20}

Parallel Stream

Par défaut les opérations effectuées sur un Stream sont séquentielles. Pour paralléliser les processus il suffit d’utiliser la méthode parallel.
On peut changer l’état d’un Stream au cours de son utilisation en utilisant sequential et parallel.
Bien ententu il ne s’agit pas d’utiliser parallel sur chaque Stream en espèrant un temps de traitement moins long (ça peut même être le contraire). Il conviendra donc de s’assurer de la pertinence de son utilisation.

Map

Les Map ne sont pas compatibles avec les Stream. Elles ont quand même le droit à leur lot de nouvelles fonctionalités.

putIfAbsent

Associe une valeur avec une clef seulement si la clef n’existe pas encore ou si la valeur associée à la clef vaut null.

Map<String, String> map = new HashMap<>();

assert map.putIfAbsent("key", null) == null;
assert map.get("key") == null;

assert map.putIfAbsent("key", "value") == null;
assert map.get("key").equals("value");

assert map.putIfAbsent("key", "new-value").equals("value");
assert map.get("key").equals("value");

forEach

Utilise un BiConsumer pour consommer tous les couples d’une Map.

Map<String, Integer> map = new HashMap<>();
map.put("un", 1);
map.put("deux", 2);
map.put("trois", 3);

map.forEach((key, value) -> System.out.printf("%s(%d) ", key, value));

// trois(3) un(1) deux(2)

computeIf

Permet d’appliquer une BiFunction sur les valeurs de la map.

compute

Si la Function retourne null, le couple est supprimé de la map.

assert map.compute("un", (key, value) -> null) == null;
assert map.compute("deux", (key, value) -> value == null ? null : value + 2) == 4;

// trois(3) deux(4) 

computeIfAbsent

Tente la création d’une valeur pour une clef si celle-ci n’est associée à aucune valeur (ou si elle est null) en utilisant une Function.

assert map.computeIfAbsent("un", key -> null) == 1;
assert map.computeIfAbsent("quatre", key -> 4) == 4;

// trois(3) quatre(4) un(1) deux(2)

computeIfPresent

Tente de mettre à jour la valeur associée à une clef si la valeur en question est non-null avec une BiFunction. Si la Function retourne null, le couple est supprimé de la Map.

Map<String, Integer> map = new HashMap<>();
map.put("un", 1);
map.put("deux", 2);
map.put("trois", 3);
map.put("quatre", null);

assert map.computeIfPresent("trois", (key, value) -> value + 5) == 8;
assert map.computeIfPresent("un", (key, value) -> null) == null;
assert map.computeIfPresent("quatre", (key, value) -> value + 5) == null;

// trois(8) quatre(null) deux(2)

remove

Supprime l’Entry pour la clef en argument si et seulement si la valeur associée à cette clef est égale à celle en argument.

assert map.remove("un", 1) == true;
assert map.remove("deux", 3) == false;

// trois(3) deux(2) 

getOrDefault

Retourne une valeur par défaut si aucune valeur n’est associée à la clef en argument.

assert map.getOrDefault("un", -1) == 1;
assert map.getOrDefault("six", -1) == -1;

merge

Associe la valeur non-null en argument à une clef si cette clef n’est pas déjà associée à une valeur (ou si la valeur est null), sinon applique la BiFunction sur la valeur existante.

Map<Integer, String> map = new HashMap<>();
map.put(1, "un");
map.put(2, "deux");

assert map.merge(1, " exemple", String::concat).equals("un exemple");
assert map.merge(2, "exemples", (key, value) -> null) == null;

// 1(un exemple) 

replace, replaceAll

Remplace une valeur associée à une clef si cette valeur est égale à celle en argument.

Map<String, Integer> map = new HashMap<>();
map.put("un", 1);
map.put("deux", 2);

assert map.replace("un", 1, 5) == true;
assert map.replace("deux", 3, 5) == false;

// un(5) deux(2)

Remplace toutes les valeurs en appliquant la BiFunction en paramètre

Map<String, Integer> map = new HashMap<>();
map.put("un", 1);
map.put("deux", 2);

map.replaceAll((key, value) -> "un".equals(key) ? value + 1 : value + 2);

assert map.get("un") == 2;
assert map.get("deux") == 4;

Date

Une toute nouvelle API pour les dates est disponible dans le package java.time. Elle s’inspire de Joda-Time et toutes les classes de cette API sont enfin immuables et threadsafe !

LocalTime

Cette classe représente une heure au sein d’une journée sans information de timezone, c’est l’heure qu’on affiche sur une horloge.

LocalTime time1 = LocalTime.of(13, 37, 26);
LocalTime time2 = LocalTime.of(15, 47, 54);

assert time1.isBefore(time2) == true;

LocalTime time3 = time1.plusMinutes(5);

assert ChronoUnit.HOURS.between(time1, time2) == 2;
assert ChronoUnit.MINUTES.between(time1, time3) == 5;

time1.format(DateTimeFormatter.ISO_LOCAL_TIME); // 13:37:26
time3.format(DateTimeFormatter.ofPattern("Ha")); // 13PM

DateTimeFormatter formatter = DateTimeFormatter
    .ofLocalizedTime(FormatStyle.SHORT)
    .withLocale(Locale.FRANCE);
LocalTime time4 = LocalTime.parse("17:17", formatter);

assert time4.getHour() == 17;
assert time4.getMinute() == 17;

LocalDate

Représente une date sans information d’heures, c’est ce qu’on utilise pour une date de naissance.

LocalDate today = LocalDate.now();
LocalDate tomorrow = today.plus(1, ChronoUnit.DAYS);
LocalDate yesterday = today.minusDays(1);

LocalDate birthday = LocalDate.of(2014, Month.DECEMBER, 18);
assert birthday.getDayOfWeek() == DayOfWeek.THURSDAY;

DateTimeFormatter formatter = DateTimeFormatter
    .ofLocalizedDate(FormatStyle.LONG)
    .withLocale(Locale.FRANCE);
LocalDate date = LocalDate.parse("14 juillet 2014", formatter);

assert date.getDayOfMonth() == 14;
assert date.getMonth() == Month.JULY;
assert date.getYear() == 2014;

LocalDateTime

Et voilà une date avec des heures mais toujours sans timezone. L’API reste toujours très similaire aux Local* précédents.

LocalDateTime time = LocalDateTime.of(2014, Month.DECEMBER, 25, 12, 10);
assert time.get(ChronoField.YEAR) == 2014;

time.format(DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm")); // 25/12/2014 12:10

ZonedDateTime

La version ultime avec une timezone en plus.

ZonedDateTime today = ZonedDateTime.now(ZoneId.of("Europe/Paris"));
System.out.println(today.format(DateTimeFormatter.ISO_ZONED_DATE_TIME)); 

// 2014-04-21T20:26:53.275+02:00[Europe/Paris]

ZoneId

Cette classe représente les timezone manipulables dans l’API.

ZoneId.getAvailableZoneIds();
// [Asia/Aden, America/Cuiaba, Etc/GMT+9, Etc/GMT+8, Africa/Nairobi, America/Marigot...

ZoneId paris = ZoneId.of("Europe/Paris");
paris.getDisplayName(TextStyle.FULL, Locale.FRENCH); // Heure d'Europe centrale
paris.getDisplayName(TextStyle.SHORT, Locale.FRENCH); // CET

Instant

Représente un instant au cours du temps.

Instant.parse("2007-12-03T10:15:30.00Z");

On peut récupérer cet objet un peu partout depuis l’API, par exemple

Instant now = LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant();
// 2014-04-21T19:27:26.759Z

Et il permet de créer un bon vieux java.util.Date pour la compatibilité.

Date.from(now)

Clock

Permet l’accès à l’Instant courant.

Instant now = Clock.system(ZoneId.of("Europe/Paris")).instant();
// 2014-04-21T19:36:37.786Z

long millis = Clock.systemDefaultZone().millis();
// 1398109144935

Duration

Représente une quantité de temps comme 42 secondes.

Duration twentySeconds = Duration.parse("PT20S");
assert twentySeconds.getSeconds() == 20;

Duration oneMinuteTwentySeconds = Duration.ofMinutes(1).plus(twentySeconds);
assert oneMinuteTwentySeconds.getSeconds() == 80;

LocalTime time1 = LocalTime.of(21, 30, 17);
LocalTime time2 = LocalTime.of(22, 40, 37);
Duration duration = Duration.between(time1, time2); // PT1H10M20S
assert duration.get(ChronoUnit.SECONDS) == 3600 + 600 + 20; 

Du sucre

StringJoiner

On peut enfin concaténer des String séparées par un délimiteur sans utiliser de librairies.

String join = new StringJoiner(",")
        .add("a")
        .add("b")
        .add("c")
        .toString();
assert join.equals("a,b,c");

String join = String.join(",", asList("a", "b", "c"));
assert join.equals("a,b,c");

Calendar.Builder

La mise en place du pattern Builder pour la classe Calendar.
Des exemples sont disponibles dans l’article http://marxsoftware.blogspot.fr/2013/05/jdk-8-calendar-builder.html.

Base64

Plus besoin non plus d’utiliser une librairie pour encoder des données en Base64.

Base64.getEncoder().encode("JC".getBytes()); //SkM=

Annotations

Les annotations peuvent être déclarées sur les utilisations des types.

Instantiation d’une classe :

new @Interned MyObject();

Cast :

myString = (@NonNull String) str;

Implémentation d’une interface :

class UnmodifiableList<T> implements
    @Readonly List<@Readonly T> { ... }

Déclaration des Exceptions:

void monitorTemperature() throws
    @Critical TemperatureException { ... }

Java 8 introduit la possibilité de rendre une annotation @Repeatable.

Vous avez déjà certainement vu le schéma d’utilisation suivant :

  • une annotation d’un certain type, au hasard @JoinColumn,
  • une seconde prenant comme paramètre un tableau d’éléments du premier type, @JoinColumns.

Et on écrivait ensuite

@JoinColumns({
    @JoinColumn(name="ADDR_ID", referencedColumnName="ID"),
    @JoinColumn(name="ADDR_ZIP", referencedColumnName="ZIP")
})
public Address getAddress() { return address; }

On peut maintenant se contenter de

@JoinColumn(name="ADDR_ID", referencedColumnName="ID"),
@JoinColumn(name="ADDR_ZIP", referencedColumnName="ZIP")
public Address getAddress() { return address; }


si l'annotation `@JoinColumn` est annotée avec `@Repeatable(JoinColumns.class)`.

Création

Il faut continuer de créer deux types.

public @interface Descriptions {
    Description[] value();
}

@Repeatable(Descriptions.class)
public @interface Description {
    String value();
}

@Description pourra être répétée dans utiliser @Descriptions.

@Description("toto")
@Description("tata")
public class Repeat {}

En fait c’est le compilateur qui utilisera @Descriptions de manière transparente pour l’utilisateur.

Lecture

Description[] byType = Repeat.class.getAnnotationsByType(Description.class);
assert byType.length == 2;

Description withoutType = Repeat.class.getAnnotation(Description.class);
assert withoutType == null;

Descriptions byContainer = Repeat.class.getAnnotation(Descriptions.class);
assert byContainer != null;
assert byContainer.value().length == 2;

On voit qu’il faut utiliser Class#getAnnotationsByType pour lire une annotation @Repeatable et que le compilateur a bien ajouté une annotation @Descriptions pour englober les @Description.

Nashorn

C’est un moteur Javascript executé au sein de la JVM ! Jusque là était disponible Rhino mais Nashorn est une nouvelle implémentation basée sur invokedynamic.

Il est possible d’interpréter du js en ligne de commande avec l’utilitaire jjs disponible dans le jdk.

jjs                                                                                                                                                                         
jjs> print("hello");
hello

Mais on peut surtout embarquer le moteur dans ses programmes Java.

ScriptEngineManager engineManager = new ScriptEngineManager();
ScriptEngine engine = engineManager.getEngineByName("nashorn");
engine.eval("function sum(a, b) { return a + b; }");

assert (int) engine.eval("sum(1, 2);") == 3;

Il existe de nombreuses possibilités d’intéraction entre votre code Java et le js évalué. L’article Oracle Nashorn: A next-generation JavaScript engine for the JVM présente toutes ces fonctionalités.

jdeps

Un analyseur de dépendances fait son apparition dans les outils du JDK.

jdeps -v Examples.class                                                                            
Examples.class -> /Library/Java/JavaVirtualMachines/jdk1.8.0_05.jdk/Contents/Home/jre/lib/rt.jar
   fr.jcgay.example.java.Strings.Examples             -> java.lang.AssertionError
   fr.jcgay.example.java.Strings.Examples             -> java.lang.CharSequence
   fr.jcgay.example.java.Strings.Examples             -> java.lang.Class
   fr.jcgay.example.java.Strings.Examples             -> java.lang.Iterable
   fr.jcgay.example.java.Strings.Examples             -> java.lang.Object
   fr.jcgay.example.java.Strings.Examples             -> java.lang.String
   fr.jcgay.example.java.Strings.Examples             -> java.util.Arrays
   fr.jcgay.example.java.Strings.Examples             -> java.util.List
   fr.jcgay.example.java.Strings.Examples             -> java.util.StringJoiner

Direction l’article http://marxsoftware.blogspot.fr/2014/03/jdeps.html pour en savoir plus.

Profils

Des sous-ensembles de l’API ont été définis et permettent de réduire la taille de la JVM : guide.

Javadoc

La section “Method Summary” a évoluée légèrement en regroupant maintenant les méthodes par type (Exemple).

Javadoc method summary

Un outil de validation a fait son apparition, doclint, malheureusement activé par défaut. Il y a peu de chance que votre documentation soit valide… Tout est très bien expliqué ici

Metaspace

La suppression de la Permanent Generation (PermGen) laisse place au Metaspace.
Java 8 from PermGen to Metaspace
Metaspace in Java 8

Sources, pour aller plus loin

Understanding Method References
Java 8 tutorial
10 features in Java 8 you haven’t heard of
Annotations Basics
50 nouvelles choses que l’on peut faire avec Java 8
Java time
Java 8 features

java 

comments powered by Disqus