Remplacer l'objet par var: qu'est-ce qui pourrait mal tourner?

J'ai récemment rencontré une situation où le remplacement d'Object par var dans un programme Java 10 lève une exception lors de l'exécution. Je me suis intéressé au nombre de façons différentes de parvenir à cet effet et j'ai adressé ce problème à la communauté:



Il s'est avéré que vous pouvez obtenir l'effet de différentes manières. Bien que tous soient légèrement compliqués, il est intéressant de rappeler les différentes subtilités de la langue avec l'exemple d'une telle tâche. Voyons quelles méthodes ont été trouvées.


Les membres


Parmi les répondants, il y avait beaucoup de gens célèbres et peu nombreux. Voici Sergey bsideup Egorov, employé de Pivotal, conférencier, l'un des créateurs de Testcontainers. Voici Victor Polishchuk , célèbre pour ses reportages sur l'entreprise sanglante. A également noté Nikita Artyushov de Google; Dmitry Mikhailov et Maccimo . Mais j'ai été particulièrement ravi de l'arrivée de Wouter Coekaerts . Il est connu pour son article de l'année dernière , où il a parcouru le système de type Java et a expliqué à quel point il était désespérément brisé. Une partie de cet article jbaruch et j'ai même utilisé dans la quatrième version de Java Puzzlers .


Tâche et solutions


Donc, l'essence de notre tâche est la suivante: il y a un programme Java dans lequel il y a une déclaration d'une variable de la forme Object x = ... (honnête standard java.lang.Object , pas de substitution de type). Le programme compile, exécute et imprime quelque chose comme "Ok". Nous remplaçons Object par var , nécessitant une inférence de type automatique, après quoi le programme continue de compiler, mais se bloque au lancement avec une exception.


Les solutions peuvent être grossièrement divisées en deux groupes. Dans le premier, après avoir remplacé par var, la variable devient primitive (c'est-à-dire qu'elle était à l'origine une autoboxing). Le deuxième type reste objet, mais plus spécifique que Object . Ici, vous pouvez mettre en évidence un sous-groupe intéressant qui utilise des génériques.


La boxe


Comment distinguer un objet d'une primitive? Il existe de nombreuses façons différentes. Le plus simple est de vérifier l'identité. Cette solution a été proposée par Nikita:


 Object x = 1000; if (x == new Integer(1000)) throw new Error(); System.out.println("Ok"); 

Lorsque x est un objet, il ne peut certainement pas être égal par référence au nouvel objet new Integer(1000) . Et s'il s'agit d'une primitive, alors selon les règles du langage, le new Integer(1000) se déploie immédiatement en une primitive aussi, et les nombres sont comparés en tant que primitives.


Une autre façon est les méthodes surchargées. Vous pouvez écrire le vôtre, mais Sergey a proposé une option plus élégante: utilisez la bibliothèque standard. La méthode List.remove est List.remove , elle est surchargée et peut supprimer soit un élément par index si une primitive est passée, soit un élément par valeur si un objet est passé. Cela a conduit à plusieurs reprises à des bugs dans de vrais programmes si vous utilisez List<Integer> . Pour notre tâche, la solution peut ressembler à ceci:


 Object x = 1000; List<?> list = new ArrayList<>(); list.remove(x); System.out.println("Ok"); 

Maintenant, nous essayons de supprimer l'élément inexistant 1000 de la liste vide, c'est juste une action inutile. Mais si nous remplaçons Object par var , nous appellerons une autre méthode qui supprime l'élément avec l'index 1000. Et cela conduit déjà à IndexOutOfBoundsException .


La troisième méthode est l'opérateur de conversion de type. Nous pouvons convertir avec succès une autre primitive en un type primitif, mais un objet n'est converti que s'il existe un wrapper sur le même type vers lequel nous allons convertir (alors une boîte de réception se produira). En fait, nous avons besoin de l'effet inverse: une exception est dans le cas d'une primitive, et non dans le cas d'un objet, mais en utilisant try-catch c'est facile à réaliser, ce que Viktor a utilisé:


 Object x = 40; try { throw new Error("Oops :" + (char)x); } catch (ClassCastException e) { System.out.println("Ok"); } 

Ici, ClassCastException est le comportement attendu, puis le programme se ferme normalement. Mais après avoir utilisé var cette exception disparaît et nous lançons autre chose. Je me demande si cela est inspiré par le vrai code de l'entreprise sanglante? ..


Une autre option de conversion de type a été proposée par Wouter. Vous pouvez utiliser l'étrange logique de l'opérateur ?: . Certes, son code donne simplement des résultats différents, vous devez donc le modifier d'une manière ou d'une autre pour qu'il y ait une exception. Donc, il me semble, assez élégamment:


 Object x = 1.0; System.out.println(String.valueOf(false ? x : 100000000000L).substring(12) + "Ok"); 

La différence entre cette méthode est que nous n'utilisons pas directement la valeur de x , mais le type x affecte le type de l'expression false ? x : 100000000000L false ? x : 100000000000L . Si x est un Object , alors le type de l'expression entière est Object , et alors nous avons juste une boxe, String.valueOf() chaîne de 100000000000 , pour laquelle substring(12) est une chaîne vide. Si vous utilisez var , alors le type x devient double , et donc le type false ? x : 100000000000L false ? x : 100000000000L également double , c'est-à-dire que 100000000000L se transformera en 1.0E11 , où il est beaucoup moins de 12 caractères, donc appeler la substring - substring entraîne une StringIndexOutOfBoundsException .


Enfin, nous profitons du fait qu'une variable peut effectivement être modifiée après sa création. Et dans la variable objet, contrairement à la primitive, vous pouvez mettre null . Mettre null dans une variable est facile, il y a plusieurs façons. Mais ici, Wouter a également adopté une approche créative en utilisant la méthode ridicule Integer.getInteger :


 Object x = 1; x = Integer.getInteger("moo"); System.out.println("Ok"); 

Tout le monde ne sait pas que cette méthode lit une propriété système appelée moo et, s'il y en a une, essaie de la convertir en nombre, sinon elle retourne null . S'il n'y a pas de propriété, nous affectons discrètement null à l'objet, mais tombons de NullPointerException lorsque vous essayez de l'attribuer à une primitive (une boîte de réception automatique se produit là-bas). Cela aurait pu être plus facile, bien sûr. Version approximative x = null; il n’explore pas - il ne compile pas, mais le compilateur va l’avaler maintenant:


 Object x = 1; x = (Integer)null; System.out.println("Ok"); 

Type d'objet


Supposons que vous ne puissiez plus jouer avec les primitives. À quoi d'autre pouvez-vous penser?


Eh bien, tout d'abord, la méthode de surcharge la plus simple proposée par Dmitry:


 public static void main(String[] args) { Object x = "Ok"; sayWhat(x); } static void sayWhat(Object x) { System.out.println(x); } static void sayWhat(String x) { throw new Error(); } 

La liaison des méthodes surchargées en Java se produit statiquement, au stade de la compilation. La méthode sayWhat sayWhat(Object) est sayWhat(Object) , mais si nous déduisons automatiquement le type x , la String sayWhat(String) , et donc la méthode sayWhat(String) plus spécifique sera liée.


Une autre façon de faire un appel ambigu en Java est d'utiliser des arguments variables (varargs). Wouter l'a rappelé à nouveau:


 Object x = new Object[] {}; Arrays.asList(x).get(0); System.out.println("Ok"); 

Lorsque le type de variable est Object , le compilateur pense qu'il s'agit d'un argument variable et encapsule le tableau dans un autre tableau d'un élément, donc get() remplit correctement. Si vous utilisez var , le type Object[] s'affiche et il n'y aura pas de wrapping supplémentaire. De cette façon, nous obtenons une liste vide et l'appel get() échouera.


Maccimo a opté pour le hardcore: il a décidé d'appeler println via l'API MethodHandle:


 Object x = "Ok"; MethodHandles.Lookup lookup = MethodHandles.lookup(); MethodHandle mh = lookup.findVirtual( PrintStream.class, "println", MethodType.methodType(void.class, Object.class)); mh.invokeExact(System.out, x); 

La méthode invokeExact et plusieurs autres méthodes du java.lang.invoke ont la soi-disant "signature polymorphe". Bien qu'elle soit déclarée comme la méthode vararg invokeExact(Object... args) habituelle, elle ne se produit pas dans un emballage de tableau standard. Au lieu de cela, une signature est générée dans le bytecode qui correspond aux types d'arguments réellement passés. La méthode invokeExact conçue pour invoquer très rapidement les descripteurs de méthode, elle ne fait donc aucune transformation d'argument standard comme le casting ou la boxe. Le type de méthode de gestion devrait correspondre exactement à la signature de l'appel. Ceci est vérifié lors de l'exécution, et comme dans le cas de var correspondance est rompue, nous obtenons une WrongMethodTypeException .


Génériques


Bien sûr, le paramétrage des types peut ajouter un scintillement à n'importe quelle tâche en Java. Dmitry a apporté une solution similaire au code que j'ai rencontré initialement. La décision de Dmitry est verbeuse, donc je vais montrer mon:


 public static void main(String[] args) { Object x = foo(new StringBuilder()); System.out.println(x); } static <T> T foo(T x) { return (T)"Ok"; } 

Le type T est StringBuilder tant que StringBuilder , mais dans ce code, le compilateur n'est pas tenu d'insérer une vérification de type sur le cadran-pair dans le bytecode. Il lui suffit que StringBuilder puisse être assigné à Object , ce qui signifie que tout va bien. Personne n'est contre le fait que la méthode avec la valeur de retour StringBuilder fait renvoyé la chaîne si vous avez quand même affecté le résultat à une variable de type Object . Le compilateur avertit honnêtement que vous avez un casting non contrôlé, ce qui signifie qu'il se lave les mains. Cependant, lors du remplacement de x par var type x est également affiché en tant que StringBuilder , et il n'est plus possible sans vérification de type, car attribuer autre chose à la variable de type StringBuilder est sans valeur. Par conséquent, après avoir changé en var programme se bloque en toute sécurité avec une ClassCastException .


Wouter a suggéré une variante de cette solution en utilisant des méthodes standard:


 Object o = ((List<String>)(List)List.of(1)).get(0); System.out.println("Ok"); 

Enfin, une autre option de Wouter:


 Object x = ""; TreeSet<?> set = Stream.of(x) .collect(toCollection(() -> new TreeSet<>((a, b) -> 0))); if (set.contains(1)) { System.out.println("Ok"); } 

Ici, selon l'utilisation de var ou Object le type de flux est affiché en tant que Stream<Object> ou en tant que Stream<String> . En conséquence, le type TreeSet et le type lambda comparateur sont affichés. Dans le cas de var , les chaînes doivent arriver au lambda, donc lors de la génération d'une représentation d'exécution lambda, une conversion de type est automatiquement insérée, ce qui donne une ClassCastException lors de la tentative de ClassCastException d'une unité en chaîne.


En général, le résultat était très ennuyeux. Si vous pouvez trouver des méthodes fondamentalement différentes pour casser var , alors écrivez dans les commentaires.

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


All Articles