JEP 427, Pattern Matching for switch (Third Preview)

Contexte

Le filtrage par motif a été proposé initialement avec la JEP 406 inclus dans le JDK 17. La seconde version, la JEP 420 a été délivrée dans le JDK 18.

Avec la JEP 427, nous sommes donc sur le troisième opus. Au moment de l’écriture de ce billet, la JEP est proposée pour être inclus dans le JDK 19.

Rappel : Principe

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

astucePour plus d’informations sur le filtrage par motif pour instanceof, je vous conseille cette article JEP 375 - Pattern Matching for instanceof (Second Preview)

Initialement, 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;
}

L’objectif est de pouvoir utiliser 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();
    };
}

astucePour reprendre l’ensemble des fonctionnalités du filtrage par motif, je vous conseille l’article initiale sur cette fonctionnalité JEP 406 - Pattern Matching for switch (Preview)

Nouveautés

Traitement du null

Jusqu’à présent, la déclaration ou 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. 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. Par exemple, nous pouvons écrire aussi

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.

Raffinement des cas

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

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 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");
    }
}
----

Avec cette JEP, un nouveau mot-clé when est introduit. 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");
    }
}

Personnellement, j’adore cette possibilité. et vous ?