Оператор алмаза (<>), введенный в Java 7, изначально поддерживал только выражения создания экземпляров конкретных классов, специально исключая анонимные внутренние классы. Когда разработчики пытались использовать конструкции вроде new Comparable<String>() { ... }, компилятор отклонял вариант с алмазом new Comparable<>() { ... }, потому что анонимные классы могут вводить члены типа, которые ссылаются на выводимые параметры типа, потенциально создавая небезопасные системы типов.
Основная проблема заключалась в недемонстрируемых типах. Анонимные классы могут объявлять методы или поля, типы которых зависят от параметров типа класса. Если компилятор вывел бы сложный тип пересечения для алмаза, как показано в проблемной ситуации, где анонимный класс объявляет void foo(Box<T> t) {}, тип T может представлять захваченный подстановочный символ, который невозможно выразить в исходном коде. Это создавало ситуацию, при которой API анонимного класса содержало типы, которые невозможно было назвать или проверить на уровне исходного кода, что нарушало основное требование Java о том, что все типы в публичных API должны быть демонстрируемыми.
Java 9 разрешила это через JEP 213, внедрив анализ демонстрируемых типов. Теперь компилятор проверяет, что выводимый тип для экземпляра анонимного класса демонстрируемый, то есть может быть выражен с использованием синтаксиса типов Java. Следующий пример демонстрирует законное использование:
// Допустимо в Java 9+ Comparator<String> c = new Comparator<>() { @Override public int compare(String a, String b) { return a.length() - b.length(); } };
Если вывод приводит к сложному типу, включающему подстановочные символы или пересечения, которые нельзя продемонстрировать, компилятор возвращается к требованию явных аргументов типов. Это гарантирует безопасность типов, позволяя при этом краткий синтаксис для обычных случаев.
На финансовой торговой платформе, созданной на Java 8, команда разработки поддерживала тысячи обработчиков событий. Эти обработчики использовали анонимные реализации Comparator<TradeEvent> и Predicate<MarketData> по всему движку сопоставления заказов, требуя явных аргументов типов, что создавало значительное визуальное загромождение во время код-ревью.
Команда рассматривала три подхода к уменьшению шаблонного кода. Первый подход заключался в миграции всех анонимных классов на лямбда-выражения. Хотя это устраняло многословие для простых случаев, многие обработчики требовали частные вспомогательные методы или блоки обработки исключений, которые превышали возможности лямбда. Это ограничение вынудило неудобное переработку в именованные внутренние классы, увеличив количество классов и уменьшив локальность поведения.
Второй подход предложил сохранить явные аргументы типов. Это сохранило полную функциональность и работало с существующей инфраструктурой Java 8, но продолжало обременять поддержку. Разработчики часто сталкивались с конфликтами при слиянии, когда изменяли сигнатуры типов, а избыточные декларации увеличивали когнитивную нагрузку во время отладки.
Третий подход предложил обновиться до Java 9, чтобы воспользоваться поддержкой оператора алмаза для анонимных классов. После оценки стоимости миграции в сравнении с увеличением производительности команда выбрала обновление до Java 9, поскольку платформа требовала интеграции с системой модулей Jigsaw в любом случае. Анализ демонстрируемых типов позволил им написать new Comparator<>() { public int compare(TradeEvent a, TradeEvent b) { ... } }, в то время как компилятор проверял, что TradeEvent представляет собой демонстрируемый тип.
Это изменение снизило среднюю определение обработчика с четырех строк до одной, устранив примерно 2,400 строк избыточных деклараций типов. В результате конфликты при слиянии в модулях с большим количеством обобщений значительно уменьшились благодаря устранению необходимости синхронизации явных аргументов типов между ветками функций. Скорость разработки увеличилась на пятнадцать процентов в последующих кварталах благодаря снижению накладных расходов на переработку.
Почему оператор алмаза не работает при выводе аргументов типов для обобщенных конструкторов в необработанных типах?
При инициализации необработанного класса, такого как new ArrayList()<>, оператор алмаза не может выводить аргументы типов, поскольку необработанные типы полностью стирают информацию об обобщениях. Компилятор рассматривает необработанный тип как не имеющий параметров типа, что делает вывод невозможным, поскольку сама сигнатура конструктора теряет параметризацию. Кандидаты часто путают это с предупреждениями о необработанной конверсии, но основная проблема заключается в полной стирании метаданных об обобщениях в контексте необработанных типов, а не только в необработанных операциях.
Как взаимодействие между полиморфными выражениями и оператором алмаза влияет на разрешение перегрузки методов?
Оператор алмаза создает полиморфное выражение, тип которого зависит от контекста присваивания. В контекстах вызова методов, таких как process(new ArrayList<>()), компилятор должен определить целевой тип из формальных параметров метода, прежде чем завершить вывод типов. Это создает взаимозависимость: применимость метода зависит от выводимого типа, но выводимый тип зависит от целевого типа. Компилятор разрешает это через генерацию ограничений и этапы интеграции, потенциально выбирая разные перегрузки, чем это произошло бы с явными аргументами типов. Кандидаты часто упускают из виду, что разрешение перегрузки происходит перед полным выводом типов, что приводит к неожиданным ошибкам компиляции, когда несколько перегрузок могут совпадать.
Что отличает ограничение демонстрируемых типов от требования о воспроизводимых типах при создании массивов?
Хотя оба ограничения предотвращают определенные обобщенные операции, демонстрируемые типы (относящиеся к выводу оператора алмаза) гарантируют возможность выражения типов в исходном коде, в то время как воспроизводимые типы (относящиеся к new T[10]) требуют информации о типах во время выполнения. Тип, такой как List<String>, является демонстрируемым, но не воспроизводимым. Кандидаты часто путают эти ограничения, полагая, что недемонстрируемые типы создают риски безопасности во время выполнения, аналогичные исключениям хранения массива. На самом деле недемонстрируемые типы компрометируют выразительность типов на уровне исходного кода и согласованность API, в то время как невоспроизводимые типы компрометируют безопасность типов во время выполнения. Понимание этого различия является решающим при проектировании обобщенных API, которые должны оставаться совместимыми как с анонимными классами, так и с унаследованным кодом на основе массивов.