JEP 441, Pattern Matching for switch

Contexte

Le principe est comme le filtrage par motif pour instanceof mais en l’appliquant à l’instruction switch.

astucePour plus d’informations sur le filtrage par motif pour instanceof, je vous conseille cette article JEP 394 - Pattern Matching for instanceof

Cette fonctionnalité a connu trois aperçu

Maintenant, c’est bien une fonctionnalité standard qui sera inclus dans le JDK 21.

Principe

Reprenons, le principe. Avant cette fonctionnalité, nous avons le code suivant :

static String formatter(Object o) {
    String formatted = "unknown";
    if (o instanceof Integer i) {
        formatted = String.format("int %d", i);
    } else if (o instanceof Long l) {
        formatted = String.format("long %d", l);
    } else if (o instanceof Double d) {
        formatted = String.format("double %f", d);
    } else if (o instanceof String s) {
        formatted = String.format("String %s", s);
    }
    return formatted;
}

Nous allons pouvoir simplifier l’écriture du code en utilisant un filtrage par motif au niveau du switch (et même une expression switch).

static String formatterPatternSwitch(Object o) {
    return switch (o) {
        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        -> o.toString();
    };
}

Dominance des étiquettes de cas

Il se peut que plusieurs étiquettes soient possibles. Il a été défini que c’est l’ordre apparition qui serait pris en compte. Cela fonctionne comme pour les exceptions.

Prenons un exemple avec les classes suivantes : Forme, Rectangle et Triangle :

class Forme {}
class Rectangle extends Forme {}
class Triangle  extends Forme { int calculerAire() { ... } }

Nous pouvons écrire le code suivant en respectez bien l’ordre.

static void first(Object obj) {
    switch (obj) {
        case Triangle t ->
            System.out.println("C'est un triangle : " + t.calculerAire());
        case Forme f ->
            System.out.println("C'est une forme");
        default -> {
            break;
        }
    }
}

Sinon, cela ne compile pas comme le code suivant :

static void first(Object obj) {
    switch (obj) {
        case Forme f ->
            System.out.println("C'est une forme");
        case Triangle t ->                            (1)
            System.out.println("C'est un triangle : " + c.calculerAire());
        default -> {
            break;
        }
    }
}
  1. ERREUR, un motif est dominé par un motif précédent (Triangle est une Forme)

Dans les exemples, nous manipulatons beaucoup les types mais les étiquettes constantes fonctionnent aussi bien. Voici un exemple:

Integer i = ...
switch (i) {
    case -1, 1 -> ...                   (1)
    case Integer j -> ...               (2)
}
  1. Cas spécifique

  2. Tous les autres cas

Raffinement des cas

Au delà du motif avec le type correspondant, nous pourrions avoir des conditions supplémentaires qui permet de raffiner le cas.

Nous pouvons traiter par type, entre Rectangle et Triangle. Cependant, nous aimerions définir des conditions supplémentaires. Par exemple, nous voulons traiter les triangles dont l’aire est supérieur à 100. Voici le code que nous devons écrire :

static void testTriangle(Forme f) {
    switch (f) {
        case null:
            break;
        case Triangle t:
            if (t.calculerAire() > 100) {
                System.out.println("Grand triangle");
                break;
            }
        default:
            System.out.println("Une forme, qui peut être un petit triangle");
    }
}

Pour cela, un nouveau mot-clé when existe. Il permet de raffiner le cas. Dans notre exemple, nous avons t.calculateArea() > 100

static void testTriangle(Forme f) {
    switch (f) {
        case Triangle t
        when t.calculerAire() > 100 ->
            System.out.println("Grand triangle");
        default ->
            System.out.println("Une forme, qui peut être un petit triangle");
    }
}

Là encore, il est possible de mixer les composants :

static void testTriangle(Forme f) {
    switch (f) {
        case Triangle t
        when t.calculerAire() > 100 ->
            System.out.println("Grand triangle");
        case Triangle t ->
            // Soit les triangles inférieur ou égale à 100
            System.out.println("Petit triangle");
        default ->
            System.out.println("Forme autre que triangle");
    }
}

Gestion des enum comme étiquette des cas

Prenons le code suivant qui défini une énumération PILE et FACE

sealed interface Monnaie permits Piece {}
enum Piece implements Monnaie { PILE, FACE }

Nous pouvons écrire la fonction suivante :

static void bonEnumSwitch1(Monnaie m) {
    switch (m) {
        case Piece.FACE -> {    (1)
            System.out.println("Face");
        }
        case Piece.PILE -> {
            System.out.println("Pile");
        }
    }
}
  1. Nom qualifié de la constante est utilisé comme étiquette

static void bonEnumSwitch2(Piece p) {
    switch (p) {
        case FACE -> {
            System.out.println("Face");
        }
        case Piece.PILE -> {    (1)
            System.out.println("Pile");
        }
    }
}
  1. Nom qualifié n’est pas nécessaire mais cela reste autorisé.

En revanche, le code suivant ne compile pas

static void mauvaisEnumSwitch(Monnaie m) {
    switch (m) {
        case Piece.FACE -> {
            System.out.println("Face");
        }
        case PILE -> {         (1)
            System.out.println("Pile");
        }
        default -> {
            System.out.println("Some currency");
        }
    }
}
  1. ERREUR. Le nom qualifié doit être utilisé pour PILE car le type est Monnaie et non Piece.

Gestion du null

Jusqu’à présent, la déclaration où l’expression switch avec une valeur null lève une exception NullPointerException. C’est pourquoi, généralement, le switch est protégé par l’instruction if.

static void testFooBar(String s) {
    if (s == null) {
        System.out.println("Oops!");
        return;
    }
    switch (s) {
        case "Foo", "Bar" -> System.out.println("Great");
        default           -> System.out.println("Ok");
    }
}

Maintenant, nous pourrons écrire un label null pour traiter ce cas. Ainsi, nous pouvons écrire :

static void testFooBar(String s) {
    switch (s) {
        case null         -> System.out.println("Oops");
        case "Foo", "Bar" -> System.out.println("Great");
        default           -> System.out.println("Ok");
    }
}

Le label peut être mélangé avec un autre. Ainsi, nous pouvons écrire par exemple :

static void testStringOrNull(Object o) {
    switch (o) {
        case null, String s -> System.out.println("String: " + s);
        default -> System.out.println("Something else");
    }
}

Pour de raisons de compatibilité, si la valeur du sélecteur est nulle, le switch lève une exception NullPointerException.

C’est à dire que le code suivant :

static void test(Object o) {
    switch (o) {
        case String s  -> System.out.println("String: " + s);
        case Integer i -> System.out.println("Integer");
        default        -> System.out.println("default");
    }
}

est équivalent à celui-ci

static void test(Object o) {
    switch (o) {
        case null      -> throw new NullPointerException();
        case String s  -> System.out.println("String: "+s);
        case Integer i -> System.out.println("Integer");
        default        -> System.out.println("default");
    }
}

astuce Là encore, c’est le compilateur qui fait le travail.

Cela signifie aussi que le label default ne peut pas correspondre à la valeur nulle. En revanche, nous pouvons le traiter comme tel si nous le souhaitons

static void test(Object o) {
    switch (o) {
        case String s  -> System.out.println("String: "+s);
        case Integer i -> System.out.println("Integer");
        case null, default        -> System.out.println("default ou nulle");
    }
}

Exhaustivité des expressions et des instructions

L’expression switch requiert que toutes les valeurs possibles soient prises en compte dans un bloc du switch.

Par conséquent, le code suivant est erroné :

static int coverage(Object obj) {
    return switch (obj) {           (1)
        case String s -> s.length();
    };
}
  1. Erreur car non exhaustif (String est seulement une possibilité pour un Object)

Nous poursuivons en complétons l’expression switch

static int coverage(Object obj) {
    return switch (obj) {           (1)
        case String s -> s.length();
        case Integer i -> i;
    };
}
  1. Erreur car toujours pas exhaustif (String et Integer ne couvrent pas tous les cas)

Pour couvrir tous les cas, nous allons utiliser l’étiquette default.

static int coverage(Object obj) {
    return switch (obj) {
        case String s -> s.length();
        case Integer i -> i;
        default -> 0;
    };
}

De ce fait, nous pouvons utiliser le compilateur pour nous aider.

Partons sur le code suivant avec la définition de l’énumération Couleur (Rouge et Bleu) et une expression switch

enum Couleur { ROUGE, BLEU }

int nombreLettres = switch (couleur) {
    case ROUGE -> 5;
    case BLEU -> 4;
}

Maintenant, si on ajoute une nouvelle Couleur. Nous avons une erreur de compilation pour les expressions switch qui ne sont pas à jour

enum Couleur { ROUGE, BLEU, ORANGE }

int nombreLettres = switch (couleur) { (1)
    case ROUGE -> 5;
    case BLEU -> 4;
}
  1. Erreur cas tous les cas ne sont pas couverts.

Donc, il sera nécessaire de corriger afin que la compilation fonctionne

enum Couleur { ROUGE, BLEU, ORANGE }

int nombreLettres = switch (couleur) { (1)
    case ROUGE -> 5;
    case BLEU -> 4;
    case ORANGE -> 6;
}

astuceLà encore, le compilateur est notre allié.

avertissementD’où l’intérêt d’éviter l’usage de l’étiquette default car sinon nous perdons l’avantage de ces contrôles par le compilateur.

Ce principe fonctionne aussi avec les classes scellées. Le compilateur provoque des erreurs si nous modifions la hiérarchie.

Exhaustivité et compatibilité

Le principe d’exhaustivité s’appliquent sur l’expression switch et l’instruction switch. Cependant, pour des raisons de compatibilité, les instructions switch existant dans le code continuent à compiler même si l’exhaustivité n’est pas respecté, à condition qu’aucune nouvelle fonctionnalité soit utilisée.

ealed interface S permits A, B, C {}
final class A implements S {}
final class B implements S {}
record C(int i) implements S {}    (1)

static void switchStatementExhaustive(S s) {
    switch (s) {                   (2)
        case A a :
            System.out.println("A");
            break;
        case C c :
            System.out.println("C");
            break;
    };
}
  1. Implicitement final car c’est un enregistrement.

  2. Normalement il devrait avoir une erreur car le switch n’est pas exhaustive. Cependant, l’absence de la classe B est permise pour des raisons de compatibilité.

Portée des variables des motifs

La portée des variables est étendu dans trois cas.

  • Portée d’une déclaration de variable de motif, incluant la condition (mot réservé when).

Cela donne le code suivant :

static void testPortee1(Object obj) {
    switch (obj) {
        case Character c
        when c.charValue() == 7:
            System.out.println("Ding!");
            break;
        default:
            break;
    }
}
  • Portée d’une déclaration de variable de motif dans une étiquette incluant l’expression, le bloc ou l’instruction throw qui apparaît à droite de la flèche.

Voici l’illustration avec le code suivant :

static void testPortee2(Object obj) {
    switch (obj) {
        case Character c -> {
            if (c.charValue() == 7) {
                System.out.println("Ding!");
            }
            System.out.println("Character");
        }
        case Integer i ->
            throw new IllegalStateException("Argument invalide: "
                                            + i.intValue());
        default -> {
            break;
        }
    }
}
  • Portée d’une déclaration de variable de motif incluant un groupe d’instructions avec étiquette. La déclaration d’une variable de motif est interdit

Nous avons le code valide

static void testPorteValide(Object obj) {
    switch (obj) {
        case Character c:
            if (c.charValue() == 7) {
                System.out.print("Ding ");
            }
            if (c.charValue() == 9) {
                System.out.print("Tab ");
            }
            System.out.println("Character");
        default:
            System.out.println();
    }
}

En revanche, voici le code invalide

tatic void testScopeError(Object obj) {
    switch (obj) {
        case Character c:
            if (c.charValue() == 7) {
                System.out.print("Ding ");
            }
            if (c.charValue() == 9) {
                System.out.print("Tab ");
            }
            System.out.println("character");
        case Integer i:                 (1)
            System.out.println("An integer " + i);
        default:
            break;
    }
}
  1. Erreur pendant la compilation