JEP 405, Record Patterns (Preview)

Contexte

Le mois dernier, nous avons parlé du filtrage par motif pour le cas de l’instruction switch. La première utilisation de ce principe a été pour le filtrage par motif pour instanceof (JEP 406)

Pour rappel, nous avions avant ce type de code :

if (obj instanceof String) {
    String s = (String) obj;
    // utilisation de s
}

Maintenant, nous pouvons écrire simplement le code suivant :

if (obj instanceof String s) {
    // utilisation de s en tant que chaine de caractere
    ... s.contains(..);
} else {
    // pas possible d'utiliser la variable s
}

astucevous pouvez consulter ce billet pour avoir plus d’informations.

Côté des enregistrements (Record), cela a été introduit dans le JDK14 en mode aperçu.

Pour rappel, cela permet de définir des classes sous une forme simplifiée.

record Point(int x, int y) {}

astucePour plus de précision, vous pouvez consulter ce billet.

Voilà pour le contexte, passons au principe de cette JEP.

Le principe

L’objectif est d’écrire du code utilisant les deux fonctionnalités précédentes : Les enregistrements (Record) et les motifs (Patterns).

Prenons la définition de l’enregistrement Point suivant :

record Point(int x, int y) {}

D’ores et déjà, nous pouvons écrire le code suivant avec le filtrage par motif pour instanceof:

static void afficheSomme(Object o) {
    if (o instanceof Point p) {
        int a = p.x();
        int b = p.y();
        System.out.println(a + b);
    }
}

Les classes classiques sont basées sur le principe de l’encapsulation. C’est à dire que nous connaissons le contrat de services via l’interface mais nous ne connaissons pas le détail de l’implémentation.

A l’inverse, les enregistrements (Record) mettent en avant leur structure interne. L’enregistrement Point est composé d’une valeur x et d’une valeur y.

Par ce principe, nous allons pouvoir écrire l’instruction instanceof comme ceci :

void afficheSomme(Object o) {
    if (o instanceof Point(int a, int b)) {
        System.out.println(a + b);
    }
}

Pour aller plus loin

Nous pouvons manipuler des enregistrements plus complexe. Par exemple, prenons un enregistrement Rectangle qui est composé de deux Point:

record Rectangle(Point hautGauche, Point basDroit) {}

Par conséquent, nous pouvons écrire le code suivant :

static void afficheSommePointHautGauche(Rectangle r) {
    if (r instanceof Rectangle(Point hg, Point bd)) {
         System.out.println(hg.x() + hg.y());
    }
}

Ce qui nous intéresse dans le code est le point haut à gauche, donc nous pouvons ignorer le second point.

static void afficheSommePointHautGauche(Rectangle r) {
    if (r instanceof Rectangle(Point hg, var bd)) {
         System.out.println(hg.x() + hg.y());
    }
}

En revanche, nous savons qu’un Point contient deux valeurs. Par conséquent, nous pouvons appliquer le même principe.

static void afficheSommePointHautGauche(Rectangle r) {
    if (r instanceof Rectangle(Point(int a, int b) hg, var bd)) {
         System.out.println(a + b);
    }
}

Support des génériques

Prenons la définition suivante pour l’enregistrement Box :

record Box<T>(T t) {}

Nous pouvons écrire le code pour le filtrage suivant :

static void test1(Box<Object> bo) {
    if (bo instanceof Box<Object>(String s)) { // (1)
        System.out.println("String " + s);
    }
}
static void test2(Box<Object> bo) {
    if (bo instanceof Box<String>(var s)) { // (2)
        System.out.println("String " + s);
    }
}
  1. Déduction faite avec le type du paramètre s

  2. Déduction faite avec le type dans les chevrons

Cela implique que le code ci-dessous ne compile pas

static void test3(Box<Object> bo) {
    if (bo instanceof Box<Object>(var s)) { // Erreur
        System.out.println("String " + s);
    }
}

Exhaustivité du switch

Prenons les définitions suivantes pour Forme, Triangle, Rectangle et Paire :

sealed interface Forme permits Triangle, Rectangle {}
final class Triangle implements Forme {}
final class Rectangle implements Forme {}
record Paire<T>(T x, T y) {}

Nous pouvons définir une variable comme celle-ci

Paire<Forme> p1 = ...;

L’interface Forme est une interface scellée. Cela peut être soit une classe Triangle, soit une classe de Rectangle. Nous pouvons écrire l’instruction switch suivante :

switch (p1) {
    case Paire<Forme>(Triangle  p, Forme s) -> ...
    case Paire<Forme>(Rectangle p, Forme s) -> ...
}

Ci-dessus, l’ensemble des cas de l’instruction switch est défini. Tous les combinaisons possibles sont traitées.

Maintenant, nous pouvons aussi écrire le code suivant :

switch (p1) {
    case Paire<Forme>(Triangle p, Forme s) -> ...
    case Paire<Forme>(Rectangle p, Triangle s) -> ...
    case Paire<Forme>(Rectangle p, Rectangle s) -> ...
}

Nous avons simplement décomposé le deuxième cas du switch (Rectangle p, Forme s) avec les différents cas possibles de Forme : la classe Triangle et la classe Rectangle.

Maintenant, regardons le code suivant :

switch (p2) { // Erreur !
    case Paire<Forme>(Triangle p, Rectangle s) -> ...
    case Paire<Forme>(Rectangle p, Triangle s) -> ...
    case Paire<Forme>(Forme p, Triangle s) -> ...
}

Ce dernier provoque une erreur de compilation car il manque le second cas avec le premier paramètre de type Forme et le second paramètre de type Rectangle.

Là encore, le compilateur nous aide pour traiter l’exhaustivité des cas pour l’écriture des switch.

astuceCela est très utile pour la mise en place ou si on ajoute une nouvelle classe dérivée à l’interface scellée Forme comme la classe Cercle par exemple. Le compilateur signalera tous les endroits où des cas ne sont pas traités.