JEP 406 Pattern Matching for switch (Preview)

Contexte

Faisant suite à la JEP 394 sur le filtrage par motif pour instanceof, nous avons la version du filtrage pour le switch. Pour rappel, le filtrage par motif pour instanceof permet la nouvelle écriture suivante :

// Ancien code
if (o instanceof String) {
    String s = (String)o;
    // s est utilisable
	...
}

// Nouveau code
if (o instanceof String s) {
    // s est utilisable
	...
}

Vous retrouverez plus d’informations dans mon billet Billet sur les nouveautés de Java 16.

Le principe

Si nous avons des conditions imbriquées car il existe plusieurs types possibles. Nous obtenons 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 switch (et même une expression switch plus exactement).

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

astuceLe code est concis et claire. Et c’est cela qui est génial.

Gestion de la valeur nulle

La valeur null n’est normalement pas autorisée pour un switch. Il est nécessaire de le protéger. Cela donne généralement le code suivant :

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

Nous pouvons traiter simplement le cas de la valeur nulle avec le code suivant :

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

Cela fonctionne aussi si nous souhaitons traiter la valeur nulle avec une autre valeur.

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

Pattern 'switch label'

Expressoin de sélection

Nous pouvons une selection sur les types suivants :

  • Classes

  • Enregistrements

  • Tableau

  • Cas par défaut

  • Cas du null cité plus haut.

record Point(int i, int j) {}
enum Color { RED, GREEN, BLUE; }

static void typeTester(Object o) {
    switch (o) {
        case null     -> System.out.println("null");
        case String s -> System.out.println("String");
        case Color c  -> System.out.println("Color with " + c.values().length + " values");
        case Point p  -> System.out.println("Record class: " + p.toString());
        case int[] ia -> System.out.println("Array of ints of length" + ia.length);
        default       -> System.out.println("Something else");
    }
}

La dominance

l’exemple suivant ne fonctionne pas.

static void error(Object o) {
    switch(o) {
        case CharSequence cs ->
            System.out.println("A sequence of length " + cs.length());
        case String s ->    // Erreur -  Le filtrage est dominé par le motif précédent.
            System.out.println("A string: " + s);
        default -> {
            break;
        }
    }
}

En effet, toute chaine de caractère sera par héritage une instance de CharSequance. Donc le premier motif est aussi valable.

La complétude

L’exemple ci-dessous ne fonctionne pas.

static int coverage(Object o) {
    return switch (o) {         // Erreur - incomplete
        case String s  -> s.length();
        case Integer i -> i;
    };
}

En effet, tous les cas ne sont pas traités. Il faut à minima inclure le cas default. Cela donne le code suivant :

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

Classes scéllées

Avec les classes scéllées, nous pouvons tous énumérer. Dans ce cas, le cas default n’est plus nécessaire.

sealed interface S permits A, B, C {}
final class A implements S {}
final class B implements S {}
record C(int i) implements S {}  // Implicitly final

static int testSealedCoverage(S s) {
    return switch (s) {
        case A a -> 1;
        case B b -> 2;
        case C c -> 3;
    };
}

Valeur nulle

Normalement, la valeur nulle n’est pas autorisé dans un switch. Cela lève une exception de type NullPointerException

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

Pour garder le comportement, cela équivaux au code précédent.

Filtrage gardé

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