Essayer l'opérateur instanceof amélioré dans Java 14

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

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.

Source: https://habr.com/ru/post/fr477654/


All Articles