Как компилятор выбирает перегрузку метода, если аргументом служит null и подходят несколько ссылочных типов?
null совместим с любым ссылочным типом, но не с примитивным. Компилятор выбирает наиболее специфичную применимую перегрузку: например, перегрузка с параметром String предпочтительнее перегрузки с параметром CharSequence, поскольку String является подтипом CharSequence. Если подходящие типы не находятся в отношении наследования, вызов становится неоднозначным и не компилируется.
Перегрузка методов в Java проектировалась как механизм статического выбора операции по сигнатуре вызова. Решение принимается компилятором на основе типов выражений, чтобы вызов имел однозначное значение ещё до запуска программы.
Для ссылочных типов естественно учитывать отношение подтипов: более конкретный тип даёт более специфичную перегрузку. Значение null не содержит информации о конкретном классе объекта, поэтому в некоторых комбинациях перегрузок компилятор не может выбрать единственный вариант.
null можно передать параметру любого ссылочного типа: класса, интерфейса, массива или другого ссылочного типа. Поэтому добавление перегрузок может неожиданно изменить результат компиляции: вызов, который раньше был однозначным, после появления новой перегрузки может стать неоднозначным.
Важно не путать это с переопределением. Выбор перегрузки выполняется на этапе компиляции, а выбор переопределённой реализации экземплярного метода — во время выполнения. Для null сначала определяется сама перегрузка, и только затем при необходимости применяется динамический полиморфизм.
Сначала компилятор формирует множество применимых перегрузок. Для аргумента null применимы параметры всех ссылочных типов, но варианты с примитивными параметрами не подходят.
Затем компилятор сравнивает применимые варианты. Если тип параметра одной перегрузки является подтипом типа параметра другой, первая перегрузка более специфична и выбирается. Поэтому среди CharSequence и String будет выбрана String.
Если типы параметров независимы, единственной наиболее специфичной перегрузки нет. Например, String и Integer являются ссылочными типами, но ни один из них не является подтипом другого. Вызов с null в таком случае завершается ошибкой неоднозначности на этапе компиляции.
Неоднозначность можно устранить явным приведением null к нужному ссылочному типу. Однако это связывает вызывающий код с конкретной перегрузкой. Если перегрузки различаются по смыслу, часто лучше использовать разные имена методов или передавать объект-параметр с явно выраженным назначением.
В библиотеке есть метод отправки сообщения с перегрузками для String и CharSequence. Вызовы с переменной типа String или с обычным текстовым выражением разрешаются предсказуемо, но вызов с литералом null выбирает более специфичную перегрузку String.
Позже разработчик добавляет перегрузку для byte[]. Теперь вызов с null становится неоднозначным: String и byte[] не связаны наследованием. Вариант с приведением null к String быстро исправляет конкретный вызов, но делает API хрупким для дальнейшего расширения.
Вариант с сохранением перегрузок удобен для типизированных аргументов, но требует документировать поведение null и проверять обратную совместимость. Вариант с разными именами методов уменьшает риск неоднозначности, однако делает API более многословным. Для публичного API разумнее выбрать разные имена, если перегрузки обозначают разные операции, а не одну операцию с разными представлениями данных.
null выбрать перегрузку с параметром примитивного типа?Нет. null обозначает отсутствие ссылочного значения и совместим только со ссылочными типами. Автоматическая распаковка не превращает null в значение примитива: если единственная подходящая перегрузка требует, например, int, такой вызов не скомпилируется. Если одновременно существуют перегрузки для ссылочного типа-обёртки и примитива, применимой для null будет только перегрузка с обёрткой, но передача самого null может привести к исключению при последующей распаковке.
Будет выбрана перегрузка с классом, поскольку класс является более специфичным типом относительно интерфейса. Это верно даже при передаче null: компилятор использует не фактический тип значения, а отношения типов, известные из объявления классов и интерфейсов. Если класс не реализует интерфейс или типы иным образом независимы, правило наиболее специфичной перегрузки не позволит выбрать вариант автоматически.
null сначала сохранить в переменную общего ссылочного типа?Да. Перегрузка определяется типом выражения на этапе компиляции, а не скрытым происхождением значения. Если null хранится в переменной типа Object, компилятор рассматривает аргумент как Object; он не восстанавливает предполагаемый будущий тип объекта. Поэтому для предсказуемого выбора нужно использовать переменную нужного статического типа или явное приведение, понимая, что это меняет контракт вызывающего кода.