Non loin se trouve la nouvelle
14e version de Java, ce qui signifie qu'il est temps de voir quelles nouvelles fonctionnalités de syntaxe cette version de Java contiendra. L'une de ces possibilités syntaxiques est
la mise en correspondance de modèles du type qui sera implémenté à l'aide de l'opérateur
instanceof
amélioré (étendu).
Aujourd'hui, je voudrais jouer avec ce nouvel opérateur et examiner plus en détail les caractéristiques de son travail. Étant donné que la correspondance de modèles par type n'est pas encore entrée dans le référentiel JDK principal, j'ai dû télécharger le référentiel
du projet Amber , qui développe de nouvelles constructions de syntaxe Java, et
compiler le JDK à partir de ce référentiel.
Donc, la première chose que nous ferons est de vérifier la version Java pour nous assurer que nous utilisons vraiment JDK 14:
> java -version openjdk version "14-internal" 2020-03-17 OpenJDK Runtime Environment (build 14-internal+0-adhoc.osboxes.amber-amber) OpenJDK 64-Bit Server VM (build 14-internal+0-adhoc.osboxes.amber-amber, mixed mode, sharing)
C'est vrai.
Maintenant, nous allons écrire un petit morceau de code avec «l'ancien» opérateur
instanceof
et l'exécuter:
public class A { public static void main(String[] args) { new A().f("Hello, world!"); } public void f(Object obj) { if (obj instanceof String) { String str = (String) obj; System.out.println(str.toLowerCase()); } } }
> java A.java hello, world!
Ça marche. Il s'agit d'une vérification de type standard suivie d'un plâtre. Nous écrivons des constructions similaires tous les jours, quelle que soit la version de Java que nous utilisons, au moins 1.0, au moins 13.
Mais maintenant, nous avons Java 14 entre nos mains, et réécrivons le code en utilisant l'opérateur
instanceof
amélioré (j'omettre de répéter des lignes de code à l'avenir):
if (obj instanceof String str) { System.out.println(str.toLowerCase()); }
> java --enable-preview --source 14 A.java hello, world!
Super. Le code est plus propre, plus court, plus sûr et plus lisible. Il y avait trois répétitions du mot String, une est devenue. Notez que nous n'avons pas oublié de spécifier les arguments
--enable-preview --source 14
, comme Le nouvel opérateur est une
fonction d'aperçu . De plus, un lecteur attentif a probablement remarqué que nous exécutions directement le fichier source A.java, sans compilation. Cette fonctionnalité
est apparue dans Java 11.
Essayons d'écrire quelque chose de plus sophistiqué et ajoutons une deuxième condition qui utilise la variable qui vient d'être déclarée:
if (obj instanceof String str && str.length() > 5) { System.out.println(str.toLowerCase()); }
Il compile et fonctionne. Mais que faire si vous échangez des conditions?
if (str.length() > 5 && obj instanceof String str) { System.out.println(str.toLowerCase()); }
A.java:7: error: cannot find symbol if (str.length() > 5 && obj instanceof String str) { ^
Erreur de compilation. Ce qui est normal: la variable
str
n'a pas encore été déclarée, ce qui signifie qu'elle ne peut pas être utilisée.
Au fait, qu'en est-il de la mutabilité? La variable est-elle finale ou non? Nous essayons:
if (obj instanceof String str) { str = "World, hello!"; System.out.println(str.toLowerCase()); }
A.java:8: error: pattern binding str may not be assigned str = "World, hello!"; ^
Oui, la dernière variable. Cela signifie que le mot «variable» n'est pas entièrement correct ici. Et le compilateur utilise le terme spécial «liaison de modèle». C'est pourquoi je propose désormais de ne pas dire «variable», mais «pattern binding» (malheureusement, le mot «binding» n'est pas très bien traduit en russe).
Avec mutabilité et terminologie triées. Allons expérimenter plus loin. Et si nous parvenons à «casser» le compilateur?
Que faire si vous nommez une variable et une liaison de modèle par le même nom?
if (obj instanceof String obj) { System.out.println(obj.toLowerCase()); }
A.java:7: error: variable obj is already defined in method f(Object) if (obj instanceof String obj) { ^
Est logique. Le chevauchement d'une variable de la portée externe ne fonctionne pas. Cela équivaut à comme si nous venions de terminer la variable
obj
deuxième fois dans la même portée.
Et si oui:
if (obj instanceof String str && obj instanceof String str) { System.out.println(str.toLowerCase()); }
A.java:7: error: illegal attempt to redefine an existing match binding if (obj instanceof String str && obj instanceof String str) { ^
Le compilateur est aussi solide que du béton.
Que pouvez-vous essayer d'autre? Jouons avec les portées. Si la liaison est définie dans la branche
if
, sera-t-elle définie dans la branche
else
si la condition est inversée?
if (!(obj instanceof String str)) { System.out.println("not a string"); } else { System.out.println(str.toLowerCase()); }
Ça a marché. Le compilateur est non seulement fiable, mais également intelligent.
Et si oui?
if (obj instanceof String str && true) { System.out.println(str.toLowerCase()); }
Cela a encore fonctionné. Le compilateur comprend correctement que la condition se résume à une simple
obj instanceof String str
.
N'est-il vraiment pas possible de «casser» le compilateur?
Peut-être que oui?
if (obj instanceof String str || false) { System.out.println(str.toLowerCase()); }
A.java:8: error: cannot find symbol System.out.println(str.toLowerCase()); ^
Ouais! Cela ressemble déjà à un bug. Après tout, les trois conditions sont absolument équivalentes:
obj instanceof String str
obj instanceof String str && true
obj instanceof String str || false
Les règles de délimitation de flux, en revanche, ne sont pas
triviales , et peut-être qu'un tel cas ne devrait vraiment pas fonctionner. Mais si vous regardez uniquement d'un point de vue humain, je pense que c'est un bug.
Mais allez, essayons autre chose. Est-ce que cela fonctionnera:
if (!(obj instanceof String str)) { throw new RuntimeException(); } System.out.println(str.toLowerCase());
Compilé. C'est bien, car ce code est équivalent à ce qui suit:
if (!(obj instanceof String str)) { throw new RuntimeException(); } else { System.out.println(str.toLowerCase()); }
Et puisque les deux options sont équivalentes, le programmeur s'attend à ce qu'elles fonctionnent de la même manière.
Qu'en est-il des champs qui se chevauchent?
public class A { private String str; public void f(Object obj) { if (obj instanceof String str) { System.out.println(str.toLowerCase()); } else { System.out.println(str.toLowerCase()); } } }
Le compilateur n'a pas juré. C'est logique, car les variables locales peuvent toujours chevaucher des champs. Apparemment, ils ont également décidé de ne pas faire d'exceptions pour les liaisons de motifs. En revanche, un tel code est assez fragile. Un geste imprudent, et vous ne remarquerez peut-être pas comment votre
if
branche s'est cassée:
private boolean isOK() { return false; } public void f(Object obj) { if (obj instanceof String str || isOK()) { System.out.println(str.toLowerCase()); } else { System.out.println(str.toLowerCase()); } }
Les deux branches utilisent désormais le champ
str
, auquel un programmeur inattentif ne peut pas s'attendre. Pour détecter de telles erreurs le plus tôt possible, utilisez les inspections dans l'EDI et la coloration syntaxique différente pour les champs et les variables. Je recommande également que vous utilisiez toujours le qualificatif
this
pour les champs. Cela ajoutera encore plus de fiabilité.
Quoi d'autre est intéressant? Comme la "vieille"
instanceof
, la nouvelle ne correspond jamais à
null
. Cela signifie que vous pouvez toujours compter sur le fait que les classeurs de motifs ne peuvent jamais être
null
:
if (obj instanceof String str) { System.out.println(str.toLowerCase());
Soit dit en passant, en utilisant cette propriété, vous pouvez raccourcir ces chaînes:
if (a != null) { B b = a.getB(); if (b != null) { C c = b.getC(); if (c != null) { System.out.println(c.getSize()); } } }
Si vous utilisez
instanceof
, le code ci-dessus peut être réécrit comme ceci:
if (a != null && a.getB() instanceof B b && b.getC() instanceof C c) { System.out.println(c.getSize()); }
Écrivez dans les commentaires ce que vous pensez de ce style. Utiliseriez-vous cet idiome?
Et les génériques?
import java.util.List; public class A { public static void main(String[] args) { new A().f(List.of(1, 2, 3)); } public void f(Object obj) { if (obj instanceof List<Integer> list) { System.out.println(list.size()); } } }
> java --enable-preview --source 14 A.java Note: A.java uses unchecked or unsafe operations. Note: Recompile with -Xlint:unchecked for details. 3
Très intéressant. Si "l'ancien"
instanceof
ne prend en charge que
instanceof List
ou
instanceof List<?>
, Alors le nouveau fonctionne avec n'importe quel type particulier. Nous attendons que la première personne tombe dans un tel piège:
if (obj instanceof List<Integer> list) { System.out.println("Int list of size " + list.size()); } else if (obj instanceof List<String> list) { System.out.println("String list of size " + list.size()); }
Pourquoi ça ne marche pas?Réponse: manque de génériques réifiés en Java.
À mon humble avis, c'est un problème assez grave. D'un autre côté, je ne sais pas comment le réparer. Il semble que vous devez à nouveau vous fier aux inspections dans l'EDI.
Conclusions
En général, le nouveau type de correspondance de motifs fonctionne très bien. L'opérateur
instanceof
amélioré vous permet non seulement d'effectuer un test de type, mais également de déclarer des classeurs prêts à l'emploi de ce type, ce qui élimine le besoin de coulée manuelle. Cela signifie qu'il y aura moins de bruit dans le code et qu'il sera beaucoup plus facile pour le lecteur de discerner une logique utile. Par exemple, la plupart des implémentations
equals()
peuvent être écrites sur une seule ligne:
public class Point { private final int x, y; … @Override public int hashCode() { return Objects.hash(x, y); } @Override public boolean equals(Object obj) { return obj instanceof Point p && px == this.x && py == this.y; } }
Le code ci-dessus peut être écrit encore plus court. Comment?Utiliser des
entrées qui seront également incluses dans Java 14. Nous en parlerons la prochaine fois.
En revanche, plusieurs points controversés soulèvent de petites questions:
- Règles d'étendue pas complètement transparentes (exemple avec
instanceof || false
). - Champs qui se chevauchent.
instanceof
et génériques.
Cependant, ce sont plus mesquins que des allégations sérieuses. Dans l'ensemble, les énormes avantages du nouvel opérateur
instanceof
valent vraiment la peine d'être ajouté. Et s'il quitte toujours l'état de prévisualisation et devient une syntaxe stable, alors ce sera une grande motivation pour enfin laisser Java 8 à la nouvelle version de Java.
PS J'ai une
chaîne dans Telegram où j'écris sur les nouvelles Java. Je vous invite à vous y abonner.