Du JDK18 au JDK22
Je ne parlerai pas des preview (ni des second preview) ni d'incubator car j'attendrai que les fonctionnalités arrivent dans le langage pour les présenter.
Dans cette article, le JDK21 est la version long term support qui suit le JDK 17. Pour rappel, il n'est pas attendu que les logiciels en production suivent le rythme de sortie du JDK, suivre les LTS est très bien (cfr roadmap oracle).
JDK 18
charset
Le charset par défaut est UTF-8 [JEP 400].
Simple web server
À l'instar d'autres langages, java propose un Simple Web Server qui sert statiquement les pages du répertoires courant [JEP 408]. Ce web serveur est disponible via la commande jwebserver
comme ceci :
jwebserver -p 8082
Binding to loopback by default. For all interfaces use "-b 0.0.0.0" or "-b ::".
Serving /elsewhere and subdirectories on 127.0.0.1 port 8082
URL http://127.0.0.1:8082/
127.0.0.1 - - [19/févr./2025:11:35:35 +0100] "GET / HTTP/1.1" 200 -
Javadoc
Ajout d'un tag : @snippet
. Ce tag facilite l'introduction d'exemple de code dans la javadoc.
Là où il fallait écrire
/**
* The following code shows how to use {@code Optional.isPresent}:
* <pre>{@code
* if (v.isPresent()) {
* System.out.println("v: " + v.get());
* }
* }</pre>
*/
il est maintenant possible d'écrire :
/**
* The following code shows how to use {@code Optional.isPresent}:
* {@snippet :
* if (v.isPresent()) {
* System.out.println("v: " + v.get());
* }
* }
*/
D'après la documentation de [JEP 413], il est également possible de faire référence à un code dans la javadoc et de baliser l'extrait de code grâce aux tags @start region="example"
et @end
.
Finalisation
La finalisation — aka l'utilisation d'un bloc finally
— est proposée pour être retirée. Cette vielle structure est définitivement enterrée :
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream(file));
oos.writeObject(shapes);
oos.flush();
} catch (FileNotFoundException ex) {
// complain to user
} catch (IOException ex) {
// notify user
} finally {
if (oos != null) {
try {
oos.close();
} catch (IOException ex) {
// ignore ... any significant errors should already have been
// reported via an IOException from the final flush.
}
}
}
(Source stackoverflow)
btw, la lecture de la JEP le concernant est intéressante puisqu'elle nous rappelle comment les fichiers — en tout cas leur fermeture — étaient gérés avant le try-ressources
.
Voir JEP 421.
Éléments non abordés : - [JEP 416], - [JEP 417] (Possibilité d'optimisation de calcul en utilisant de la vectorisation), - [JEP 418] (Définit un SPI (service-provider interface) pour la résolution de nom. La résolution de nom par défaut reste celle faites par l'hôte),
JDK 19, JDK 20
Ces versions n'apportent que des previews et des incubators, voir JDK 19 et voir JDK 20.
JDK 21 (LTS long term support)
Introduction de SequencedCollection
, SequencedSet
et SequencedMap
Introduction de nouvelles interface dans la hiérarchie de Collection
pour uniformiser les opérations sur les collections ordonnées (ordonnée pas triée).
L'interface SequencedCollection
ajoute des méthodes pour manipuler les extrémités de la collection :
interface SequencedCollection<E> extends Collection<E> {
// new method
SequencedCollection<E> reversed();
// methods promoted from Deque
void addFirst(E);
void addLast(E);
E getFirst();
E getLast();
E removeFirst();
E removeLast();
}
Pour parcourir une collection ordonnée (sequenced collection), on peut écrire :
SequencedCollection<String> nombrils = List.of("Vicky", "Jenny", "Karine");
/*
for modifiable sequenced collection
nombrils = new ArrayList<>(List.of("Vicky", "Jenny", "Karine"));
*/
System.out.println(nombrils.getFirst());
System.out.println(nombrils.getLast());
for (var s : nombrils) {
System.out.print(s + " ");
}
System.out.println();
for (var s : nombrils.reversed()) {
System.out.print(s + " ");
}
System.out.println();
// or with stream
nombrils.reversed().stream().forEach(System.out::print);
De même pour SequencedSet<E>
(qui étend Set<E>
) et SequencedMap<K, V>
(qui étend Map<K, V
).
Ces nouvelles interfaces répondent à des incohérences dans le framework Collection
:
List
etDeque
ont un ordre implicite, mais leur supertype commun,Collection
, n'en définit pas;Set
n'impose pas d'ordre, bien queLinkedHashSet
etSortedSet
le fassent et;- les collections immuables, comme celles créées avec
Collections.unmodifiableSet()
, perdent les informations d'ordre.
De plus, accéder au premier ou dernier élément varie selon les collections :
Collection | Premier élément | Dernier élément |
---|---|---|
List |
list.get(0) |
list.get(list.size() - 1) |
Deque |
deque.getFirst() |
deque.getLast() |
SortedSet |
sortedSet.first() |
sortedSet.last() |
LinkedHashSet |
linkedHashSet.iterator().next() |
Aucune méthode dédiée |
Itérer en sens inverse n'est pas non plus cohérent :
Deque
(etNavigableSet
) propose(nt)descendingIterator()
:
Deque<String> deque = new LinkedList<>(
List.of("Vicky", "Jenny", "Karine"));
for (var it = deque.descendingIterator(); it.hasNext(); ) {
System.out.println(it.next());
}
NavigableSet
utilise (aussi)descendingSet()
:
NavigableSet<String> navigableSet = new TreeSet<>();
navigableSet.addAll(List.of("Vicky", "Jenny", "Karine"));
for (var s : navigableSet.descendingSet()) {
System.out.println(s);
}
- tandis que
List
emploie unListIterator
:
List list = new ArrayList(List.of("Vicky", "Jenny", "Karine"));
for (var it = list.listIterator(list.size()); it.hasPrevious(); ) {
System.out.println(it.previous());
}
La méthode reversed()
amène une belle cohérence à tout ça.
Voir JEP 431
Record pattern
Les record patterns permettent de « déconstruire » des records. Ces record patterns ont fait l'objet de plusieurs preview depuis JDK19.
Avant les type patterns introduit avec JDK16, l’idiome instanceof-and-cast s'écrivait :
// Prior to JDK16
if (obj instanceof String) {
String s = (String)obj;
// ... use s ...
}
JDK16 et le type pattern à simplifié l'écriture avec,
// As of Java 16
if (obj instanceof String s) {
// ... use s ...
}
Que se passe-t-il si l'objet n'est pas de type String
mais est un record
?
// As of Java 16
record Point(int x, int y) {}
static void printSum(Object obj) {
if (obj instanceof Point p) {
int x = p.x();
int y = p.y();
System.out.println(x+y);
}
}
La pattern variable p
n'est utilisée que pour accéder aux attributs du record. C'est dommage de ne pas pouvoir accéder directement aux attributs. Depuis JDK21, il est possible d'écrire :
// As of Java 21
static void printSum(Object obj) {
if (obj instanceof Point(int x, int y)) {
System.out.println(x+y);
}
}
… et Point(int x, int y)
est un record pattern.
JDK21 permet d'aller plus loin et d'imbriquer (nested) les record patterns. Si l'on prend l'exemple — assez habituel — d'un point qui devient un point coloré et qui intervient dans un rectangle, on peut définir :
// As of Java 16
record Point(int x, int y) {}
enum Color { RED, GREEN, BLUE }
record ColoredPoint(Point p, Color c) {}
record Rectangle(ColoredPoint upperLeft, ColoredPoint lowerRight) {}
Un record pattern simple permet d'écrire :
// As of Java 21
static void printUpperLeftColoredPoint(Rectangle r) {
if (r instanceof Rectangle(ColoredPoint ul, ColoredPoint lr)) {
System.out.println(ul.c());
}
}
mais si l'on imbrique les record patterns, on peut écrire :
// As of Java 21
static void printColorOfUpperLeftPoint(Rectangle r) {
if (r instanceof Rectangle(ColoredPoint(Point p, Color c),
ColoredPoint lr)) {
System.out.println(c);
}
}
Les exemples sont issus de JEP 440 et il est possible d'aller plus loin et de voir leur usage dans le cas des génériques (generics)…
Voir JEP 440
Pattern matching pour le switch
Dans la même idée, JDK21 va également un pas plus loin dans le cas du switch.
Il arrive parfois (souvent) que l'on reçoive une variable de type Object
et que l'action a exécuter va dépendre du type effectif de cet objet. Dans cas, les tests se font à grands coups de instanceof
, un peu comme suit (en utilisant type pattern) :
// Prior to Java 21
static String formatter(Object obj) {
String formatted = "unknown";
if (obj instanceof Integer i) {
formatted = String.format("int %d", i);
} else if (obj instanceof Long l) {
formatted = String.format("long %d", l);
} else if (obj instanceof Double d) {
formatted = String.format("double %f", d);
} else if (obj instanceof String s) {
formatted = String.format("String %s", s);
}
return formatted;
}
Là où le switch
n'autorisait que les types byte
, char
, short
et int
, il autorise maintenant un type référence et l'on peut écrire un switch expression comme suit :
// As of Java 21
static String formatterPatternSwitch(Object obj) {
return switch (obj) {
case Integer i -> String.format("int %d", i);
case Long l -> String.format("long %d", l);
case Double d -> String.format("double %f", d);
case String s -> String.format("String %s", s);
default -> obj.toString();
};
}
Le switch version JDK21 autorise également que la variable soit null
et l'on peut donc ajouter case null -> // do something
à l'exemple précédent. Le label default ne correspond jamais à null
. null
doit être traité à part.
Comme les différents cas ne correspondent plus à une constante mais à un type, le pattern case label peut amener à plusieurs valeurs différentes, comme dans l'exemple (toujours issu de JEP 441) :
// As of Java 21
static void testStringOld(String response) {
switch (response) {
case null -> { }
case String s -> {
if (s.equalsIgnoreCase("YES"))
System.out.println("You got it");
else if (s.equalsIgnoreCase("NO"))
System.out.println("Shame");
else
System.out.println("Sorry?");
}
}
}
Pour écrire plus proprement ce genre de switch, JDK21 propose une clause when dans le case, un peu comme suit :
// As of Java 21
static void testStringNew(String response) {
switch (response) {
case null -> { }
case String s
when s.equalsIgnoreCase("YES") -> {
System.out.println("You got it");
}
case String s
when s.equalsIgnoreCase("NO") -> {
System.out.println("Shame");
}
case String s -> {
System.out.println("Sorry?");
}
}
}
Il est également possible de mélanger null
(null label), case constants et case patterns dans un même switch, comme suit :
// As of Java 21
static void testStringEnhanced(String response) {
switch (response) {
case null -> { }
case "y", "Y" -> {
System.out.println("You got it");
}
case "n", "N" -> {
System.out.println("Shame");
}
case String s
when s.equalsIgnoreCase("YES") -> {
System.out.println("You got it");
}
case String s
when s.equalsIgnoreCase("NO") -> {
System.out.println("Shame");
}
case String s -> {
System.out.println("Sorry?");
}
}
}
Pour aller plus loin (et retrouver ces exemples), consulter JEP 441.
Thread virtuel
Les threads virtuels (virtual thread) ont été en preview dans la JEP 4225 (JDK19) et JEP 436 (JDK20). Les virtual threads sont des threads légers utilisant les threads de la plateforme.
Là où les threads de plateforme sont plus lourds, coûteux, limités en nombre, les threads virtuels sont plus légers et peuvent être plus nombreux. Ils sont également non bloquant ; lorsque un thread virtuel bloque sur une I/O par exemple il se détache du thread OS auquel il est lié, le libérant pour un autre tâche. Il n'est pas nécessaire — c'est même une mauvaise idée — d'utiliser un pool de threads pour les threads virtuels. C'est la jvm (machine virtuelle java, java vritual machine) qui gère les threads. La jvm utilise n threads OS pour y lancer m threads virtuels. Ces threads OS sont également appelés carrier threads puisqu'ils servent à « transporter » les threads virtuels.
Testons…
var n = 10_000;
// virtuals threads
var before = System.currentTimeMillis();
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, n).forEach(i -> {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
return i;
});
});
}
var after = System.currentTimeMillis();
System.out.println("Elapsed time : " + (after - before));
// threads OS (one thread per task)
before = System.currentTimeMillis();
try (var executor = Executors.newThreadPerTaskExecutor(
Executors.defaultThreadFactory())) {
IntStream.range(0, n).forEach(i -> {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
return i;
});
});
}
after = System.currentTimeMillis();
System.out.println("Elapsed time : " + (after - before));
// threads OS (with pool of 100 threads)
before = System.currentTimeMillis();
try (var executor = Executors.newFixedThreadPool(100)) {
IntStream.range(0, n).forEach(i -> {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
return i;
});
});
}
after = System.currentTimeMillis();
System.out.println("Elapsed time : " + (after - before));
Voici les différences en fonction des valeurs de n
(c'est bien un ordre de grandeur) :
n | Virtual thread | Thread OS (1/task) | Thread OS (with pool) |
---|---|---|---|
10 | 1 066 ms | 1 007 ms | 1 004 ms |
100 | 1 005 ms | 1 026 ms | 1 019 ms |
1_000 | 1 048 ms | 2 246 ms | 10 024 ms |
10_000 | 1 353 ms | 7 263 ms | 101 015 ms |
100_000 | 1 568 ms | 35 276 ms | 1 000 426 ms (± 17 min) |
Y a pas photo.
Pour conclure, the new way pour créer un thread est :
var t1 = Thread.ofPlatform()
.start(() -> System.out.println("Platform thread"));
var t2 = Thread.ofVirtual()
.start(() -> System.out.println("Virtual thread"));
var t3 = Thread.startVirtualThread(
() -> System.out.println("Virtual thread (other way)"));
var t4 = Thread.ofPlatform()
.daemon()
.start(() -> System.out.println("Platform thread (dæmon)"));
(Déclarer les variables permet de faire un join()
ensuite)
Pour aller plus loin (et retrouver ces exemples), consulter JEP 444.
Éléments non abordés
- JEP 439 Optimisation du garbage collector,
- JEP 452 Key Encapsulation Mechanism API relatif à la cryptographie
JDK 22
Et l'on revient dans un JDK qui n'est pas long term support.
Variables et pattern anonyme _
Unnamed variables & patterns (variables et patterns anonymes) ; lorsqu'une déclaration de variable ou d'un pattern imbriqué (nested pattern) est obligatoire et que la variable ou le pattern ne sera pas utilisé. On utilise alors _
.
Par exemple :
static int count(Iterable<Order> orders) {
int total = 0;
for (Order order : orders) // order is unused
total++;
return total;
}
(Exemple de JEP)
devient :
static int count(Iterable<Order> orders) {
int total = 0;
for (var _ : orders)
total++;
return total;
}
Une variable anonyme ou un pattern anonyme est déclaré en utilisant le caractère underscore (barre de soulignement, tiret bas,
_
,U+005F
)
Si l'on reprend l'exemple d'un nested record pattern,
// As of Java 21
static void printColorOfUpperLeftPoint(Rectangle r) {
if (r instanceof Rectangle(ColoredPoint(Point p, Color c),
ColoredPoint lr)) {
System.out.println(c);
}
}
cette exemple peut devenir :
// As of Java 22
static void printColorOfUpperLeftPoint(Rectangle r) {
if (r instanceof Rectangle(ColoredPoint(_, Color c), _)) {
System.out.println(c);
}
}
Pour plus d'infos et d'exemples voir JEP 456
Lancer plusieurs fichiers sources
Depuis JDK11, il est possible d'écrire
public class Hello{
public static void main(String[] args) {
System.out.println("Hello");
}
}
et de directement exécuter le code d'un
~:$ java Hello.java
Le fichier est alors compilé, le bytecode stocké en mémoire et exécuté. À chaque exécution, le code source est recompilé. C'est la « méthode à la shebang ».
Il est possible d'écrire deux classes dans un même fichier. Un peu comme ci-dessous et ça fonctionne bien.
class Prog {
public static void main(String[] args) { Helper.run(); }
}
class Helper {
static void run() { System.out.println("Hello!"); }
}
Bien sûr pour un programme conséquent, les fichiers sont compilés et les fichiers .class
sont conservés sur disque. Bien sûr aussi qu'un outil de construction est conseillé (Maven ou autre).
Entre un simple Hello world et un programme complexe, il existe des programmes qui utiliseront plusieurs fichiers mais pas encore le besoin de comprendre son IDE ou d'appréhender un outil de construction (build tool). JDK22 permet de lancer directement un programme se trouvant dans plusieurs classes et réparties dans plusieurs fichiers sous certaines conditions.
Si le code précédent se trouve dans deux fichiers différents, java compilera la première classe dans le premier fichier. Lorsque le loader rencontre la seconde classe, java cherche le fichier et le compile. Toujours en mémoire.
Voir JEP 458 pour les détails dans les cas de librairies (bibliothèques) externes, de packages…
Éléments non abordés
- JEP 423 Region Pinning for G1, optimisation du garbage collector G1
- JEP 454 Changement au niveau des appels de fonctions étrangères (comme JNI java native interface)