L'opérateur diamant (<>), introduit dans Java 7, ne prenait initialement en charge que les expressions de création d'instances de classes concrètes, excluant explicitement les classes internes anonymes. Lorsque les développeurs tentaient de créer des constructions comme new Comparable<String>() { ... }, le compilateur rejetait la variante diamant new Comparable<>() { ... } parce que les classes anonymes pouvaient introduire des membres de type faisant référence à des paramètres de type inférés, pouvant potentiellement créer des systèmes de types non sûrs.
Le problème central concernait les types non-dénotables. Les classes anonymes peuvent déclarer des méthodes ou des champs dont les types dépendent des paramètres de type de la classe. Si le compilateur déduisait un type d'intersection complexe pour le diamant, comme dans le scénario problématique où une classe anonyme déclare void foo(Box<T> t) {}, le type T pourrait représenter un joker capturé qui ne peut pas être exprimé dans le code source. Cela créait une situation où l'API de la classe anonyme contenait des types impossibles à nommer ou à vérifier au niveau du code source, violant l'exigence fondamentale de Java selon laquelle tous les types dans les API publiques doivent être dénotables.
Java 9 a résolu cela par le biais de JEP 213 en mettant en œuvre une analyse des types dénotables. Le compilateur vérifie maintenant que le type inféré pour l'instanciation de la classe anonyme est dénotable, c'est-à-dire exprimable à l'aide de la syntaxe de type Java. L'exemple suivant démontre une utilisation légale :
// Valide en Java 9+ Comparator<String> c = new Comparator<>() { @Override public int compare(String a, String b) { return a.length() - b.length(); } };
Si l'inférence produit un type complexe impliquant des jokers ou des intersections qui ne peuvent pas être dénotés, le compilateur exige des arguments de type explicites. Cela garantit la sécurité des types tout en permettant une syntaxe concise pour les cas courants.
Dans une plateforme de trading financier construite sur Java 8, l'équipe de développement maintenait des milliers de gestionnaires d'événements. Ces gestionnaires utilisaient des implémentations anonymes de Comparator<TradeEvent> et Predicate<MarketData> dans tout le moteur d'appariement des commandes, nécessitant des arguments de type explicites qui créaient un bruit visuel significatif lors des revues de code.
L'équipe a envisagé trois approches pour réduire le code répétitif. La première approche consistait à migrer toutes les classes anonymes vers des expressions lambda. Bien que cela éliminât la verbosité pour les cas simples, de nombreux gestionnaires nécessitaient des méthodes d'assistance privées ou des blocs de gestion des exceptions qui dépassaient les capacités des lambdas. Cette limitation a forcé un refactoring maladroit en classes internes nommées, augmentant le nombre de classes et réduisant la proximité du comportement.
La deuxième approche suggérait de maintenir les arguments de type explicites. Cela préservait l'intégralité des fonctionnalités et fonctionnait avec l'infrastructure existante de Java 8, mais perpétuait le fardeau de maintenance. Les développeurs rencontraient fréquemment des conflits de fusion lors de la modification des signatures de type, et les déclarations redondantes augmentaient la charge cognitive lors des sessions de débogage.
La troisième approche proposait de passer à Java 9 pour tirer parti du support de l'opérateur diamant pour les classes anonymes. Après avoir évalué le coût de migration par rapport aux gains de productivité, l'équipe a choisi la mise à niveau vers Java 9 car la plateforme nécessitait de toute façon une intégration du système de modules Jigsaw. L'analyse des types dénotables leur permettait d'écrire new Comparator<>() { public int compare(TradeEvent a, TradeEvent b) { ... } } tandis que le compilateur vérifiait que TradeEvent représentait un type dénotable.
Ce changement a réduit la définition moyenne d'un gestionnaire de quatre lignes à une, éliminant environ 2 400 lignes de déclarations de type redondantes. Par conséquent, les conflits de fusion dans les modules riches en types génériques ont diminué de manière significative en supprimant le besoin de synchroniser des arguments de type explicites à travers les branches de fonctionnalités. La vélocité de développement s'est améliorée de quinze pour cent lors des trimestres suivants grâce à la réduction de la surcharge de refactoring.
Pourquoi l'opérateur diamant échoue-t-il lorsqu'il infère des arguments de type pour des constructeurs génériques dans des types bruts ?
Lorsque vous instanciez une classe brute comme new ArrayList()<>, l'opérateur diamant ne peut pas inférer d'arguments de type car les types bruts effacent complètement les informations génériques. Le compilateur considère le type brut comme n'ayant pas de paramètres de type, rendant l'inférence impossible puisque la signature du constructeur elle-même perd la paramétrisation. Les candidats confondent souvent cela avec des avertissements de conversion non vérifiés, mais le problème fondamental concerne l'effacement complet des métadonnées génériques dans les contextes de types bruts, et non simplement des opérations non vérifiées.
Comment l'interaction entre les expressions poly et l'opérateur diamant impacte-t-elle la résolution de surcharge des méthodes ?
L'opérateur diamant crée une expression poly dont le type dépend du contexte d'attribution. Dans des contextes d'invocation de méthode comme process(new ArrayList<>()), le compilateur doit déterminer le type cible à partir des paramètres formels de la méthode avant de compléter l'inférence de type. Cela crée une dépendance bidirectionnelle : l'applicabilité de la méthode dépend du type inféré, mais le type inféré dépend du type cible. Le compilateur résout cela par la génération de contraintes et des phases d'incorporation, sélectionnant potentiellement des surcharges différentes de celles qui se produiraient avec des arguments de type explicites. Les candidats négligent souvent que la résolution de surcharge se produit avant l'inférence de type complète, ce qui entraîne des erreurs de compilation surprenantes lorsque plusieurs surcharges pourraient correspondre.
Qu'est-ce qui distingue la restriction des types dénotables de l'exigence des types réifiables lors de la création de tableaux ?
Bien que les deux restrictions empêchent certaines opérations génériques, les types dénotables (pertinents pour l'inférence de l'opérateur diamant) garantissent que les types peuvent être exprimés dans le code source, tandis que les types réifiables (pertinents pour new T[10]) nécessitent des informations de type à l'exécution. Un type comme List<String> est dénotable mais pas réifiable. Les candidats confondent souvent ces contraintes, croyant que les types non dénotables posent des risques de sécurité à l'exécution similaires aux exceptions de stockage de tableaux. En réalité, les types non dénotables compromettent l'expressibilité des types au niveau source et la cohérence de l'API, tandis que les types non réifiables compromettent la sécurité des types à l'exécution. Comprendre cette distinction est crucial lors de la conception d'APIs génériques qui doivent rester compatibles à la fois avec les classes anonymes et le code hérité basé sur les tableaux.