Ca y est, Java 17 est là !

Cette nouvelle version apporte quelques nouveautés que nous avons suivi de près chez Liksi. Elle est d’autant plus importante qu’elle a le statut LTS, ce qui veut dire qu’elle servira de version de base pour les projets à venir. Nous avons souhaité vous partager dans cet article une des nouvelles features qu’elle embarque : la notion de pattern matching dans les instructions switch, qui arrive flambant neuve en version preview.

[JEP-406] Du pattern matching dans vos switchs

Cette feature est, du point de vue d’un développeur, certainement la plus importante de cette version. Cependant, elle arrive seulement en preview en Java 17, il faudra donc suivre attentivement les prochaines versions pour pouvoir utiliser pleinement cette fonctionnalité… 
L’instruction switch en Java souffre du fait qu’il est impossible de tester le type d’un objet directement dans un case. Nous allons voir par l’exemple cette nouvelle fonctionnalité, en essayant d’afficher un message différent selon le type d’un statut HTTP, qui peut être soit un succès, soit un échec. Voici comment nous souhaitons modéliser ces données :

public class AbstractStatus { }

public final class ErrorStatus extends AbstractStatus {
    private final Exception ex;

    private ErrorStatus(final Exception ex) {
        this.ex = ex;
    }

    public static ErrorStatus of(Exception ex) {
        return new ErrorStatus(ex);
    }

    public Exception getEx() {
        return ex;
    }
}

public class SuccessStatus extends AbstractStatus{
    private final String fetchedData;

    private SuccessStatus(final String fetchedData) {
        this.fetchedData = fetchedData;
    }

    public static SuccessStatus of(String fetchedData) {
        return new SuccessStatus(fetchedData);
    }

    public String getFetchedData() {
        return fetchedData;
    }
}

Nous avons un petit service chargé de récupérer les données, puis un second service qui appelle le premier et qui affiche le statut de la requête : 

@Service
public class RegexService {

    private final Logger LOG = LoggerFactory.getLogger(RegexService.class);

    private final Caller caller;

    public RegexService(final Caller caller) {
        this.caller = caller;
    }

    public void fetch() {
        try {
            printStatus(SuccessStatus.of(caller.fetch()));
        } catch (Exception e) {
            printStatus(ErrorStatus.of(e));
        }
    }
}

@Service
public class Caller {
    public String fetch() throws Exception {
        return "data fetched";
    }
}

Maintenant que le décor est posé, voyons l’évolution de cette méthode printStatus() selon les versions de Java : 

  • Avant Java 16, il était nécessaire, en plus de devoir présenter tous les cas possibles dans une grosse instruction if/elseif/else, de caster l’objet source en l’objet cible pour pouvoir accéder à ses propriétés. Voici ce que cela donnait :
public void printStatus(AbstractStatus status) {
	if (status instanceof ErrorStatus) {
		final var error = (ErrorStatus) status;
		LOG.error("An error occured - {}", error.getEx().getMessage());
	} else if (status instanceof SuccessStatus) {
		final var success = (SuccessStatus) status;
		LOG.info("Data fetched with success : {}", success.getFetchedData());
	} else {
		throw new RuntimeException("Unable to handle status");
	}
}

@Test
void should_print_before_java_16() throws Exception {
	when(caller.fetch()).thenReturn("fetched data");
	regexService.fetch();
	when(caller.fetch()).thenThrow(new Exception("ERROR"));
	regexService.fetch();
}

// [main] INFO org.example.java17.regex.RegexService - Data fetched with success : fetched data
// [main] ERROR org.example.java17.regex.RegexService - An error occured - ERROR
  • A partir de Java 16, la JEP-394 a permis de rajouter du pattern matching à l’intérieur des instructions instanceof. Ce n’est clairement pas la révolution de l’année, mais ça permet de gagner quelques lignes de code :
public void printStatus(AbstractStatus status) {
	if (status instanceof ErrorStatus error) {
		LOG.error("An error occured - {}", error.getEx().getMessage());
	} else if (status instanceof SuccessStatus success) {
		LOG.info("Data fetched with success : {}", success.getFetchedData());
	} else {
		throw new RuntimeException("Unable to handle status");
	}
}
  • Et depuis Java 17, on peut faire tout ça directement dans une instruction switch, de manière bien plus fonctionnelle :
public void printStatus(AbstractStatus status) {
	switch (status) {
		case ErrorStatus errorStatus -> LOG.error("An error occured - {}", errorStatus.getEx().getMessage());
		case SuccessStatus successStatus -> LOG.info("Data fetched with success : {}", successStatus.getFetchedData());
		default -> throw new RuntimeException("Unable to handle status");
	}
}

Il est également important de noter qu’il est désormais également possible de gérer le cas null (cette délicieuse invention) comme un case à part entière. En effet, avant Java 17, si la variable testée dans l’instruction switch avait pour valeur null, une exception de type NullPointer était immédiatement levée, ce qui nécessitait de tester cette valeur null avant de faire un switch. Désormais, cette valeur peut être testée directement comme n’importe quelle autre valeur :  

public void printStatusNull(AbstractStatus status) {
	switch (status) {
		case null -> LOG.error("Valeur nulle");
		default -> LOG.info("Another value");
	}
}	

@Test
void should_test_null_case() {
	regexService.printStatusNull(null);
}

// [main] ERROR org.example.java17.regex.RegexService - Valeur nulle

Note : depuis Java 8 et l’apparition des Optional, il est possible (et même fortement recommandé) de s’affranchir de cette notion de null. Cette dernière fonctionnalité ne devrait donc pas vous changer la vie !

Un peu plus loin

On va voir que cette feature ne s’arrête pas là : il est désormais possible de retourner une valeur directement depuis une instruction switch, ce qui est vraiment un plus et qui facilite la programmation fonctionnelle en Java :

public String getStatusLog(AbstractStatus status) {
	return switch (status) {
		case ErrorStatus errorStatus -> "An error occured : " + errorStatus.getEx().getMessage();
		case SuccessStatus successStatus -> "Data fetched with success : " + successStatus.getFetchedData();
		default -> throw new RuntimeException("Unable to handle status");
	};
}

Enfin, un dernier apport vraiment sympa permet d’étendre le test de type avec un prédicat permettant de raffiner nos pattern matchings. Imaginons le cas suivant : on souhaite récupérer les logs du statut reçu en paramètre, sauf dans le cas où l’erreur est un NullPointerException, auquel cas on souhaite retourner une RuntimeException comme si le type de statut n’était pas reconnu. On peut maintenant le faire de façon très simple et surtout très intuitive :

public String getStatusWithoutNullPointer(AbstractStatus status) {
	return switch (status) {
		case ErrorStatus errorStatus &&
			(!errorStatus.getEx().getMessage().contains("NullPointerException")) -> "An error occured : " + errorStatus.getEx().getMessage();
		case SuccessStatus successStatus -> "Data fetched with success : " + successStatus.getFetchedData();
		default -> "another case";
	};
}

Testons son efficacité :

@Test
void should_get_with_guarded_pattern() throws Exception {
	when(caller.fetch()).thenReturn("fetched data");
	regexService.fetch();
	when(caller.fetch()).thenThrow(new Exception("ERROR")).thenThrow(new Exception("Testing with some NullPointerException message"));
	regexService.fetch();
	regexService.fetch();
}

// [main] INFO org.example.java17.regex.RegexService - Data fetched with success : fetched data
// [main] INFO org.example.java17.regex.RegexService - An error occured : ERROR
// [main] INFO org.example.java17.regex.RegexService - another case

C’est à peu près tout ce qu’il y a d’important à noter pour cette feature, mais si vous souhaitez pousser plus loin, vous pouvez trouver de plus amples informations sur la description officielle de la JEP ici.

Conclusion

Cette feature est très intéressante et se rapproche d’ailleurs beaucoup de la façon de faire en Kotlin. Cependant, un point reste vraiment problématique dans cette version 17 : le fait qu’elle ne soit disponible qu’en preview. En effet, la combinaison de cette feature avec les sealed classes est particulièrement pertinente mais il faudra pour l’instant se contenter de la syntaxe if/elseif/else avec des instructions instanceof…

Un prochain article portera sur ces sealed classes et utilisera notamment ces nouvelles instructions switchs, nous verrons alors l’efficacité des deux features combinées.

Il faudra donc suivre à la loupe les prochaines versions… En tout cas, chez Liksi, nous sommes prêts !