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
etor
permettent de chaîner des prédicats selon l’opérateur logique,- La méthode statique
isEqual
teste l’égalité de deux objets selonObject#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 unInt(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).
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