Практическая ситуация: какой метод будет вызван в Java и почему?
class Printer {
void print(Object value) { System.out.println("Object"); }
void print(String value) { System.out.println("String"); }
public static void main(String[] args) {
Object value = "текст";
new Printer().print(value);
}
}
Будет вызван print(Object), поэтому программа выведет Object. Перегрузка выбирается на этапе компиляции по статическому типу выражения value, а он равен Object; фактический тип объекта String на этот выбор не влияет.
Перегрузка методов позволяет использовать одно имя для операций с разными типами аргументов. В статически типизированном Java такой подход должен разрешаться до выполнения программы, чтобы компилятор мог проверить корректность вызова и сформировать конкретную сигнатуру метода.
Это отличается от переопределения: при переопределении метод с уже выбранной сигнатурой может быть найден по фактическому типу объекта во время выполнения.
В коде переменная value хранит ссылку на объект String, но объявлена как Object. Компилятор видит именно тип Object и выбирает перегрузку, совместимую с ним.
Ожидание, что Java автоматически выберет print(String) по фактическому типу объекта, приводит к ошибкам в логировании, сериализации и вызовах API. Особенно трудно обнаружить такую проблему, когда значение проходит через параметр или поле общего типа Object.
Сначала компилятор анализирует доступные перегрузки и типы аргументов. Для выражения value его статический тип — Object, поэтому подходит print(Object); перегрузка print(String) не может быть выбрана без небезопасного предположения о типе.
Во время выполнения JVM уже не перебирает перегрузки заново по классу объекта. Она вызывает ранее выбранную сигнатуру print(Object).
Минимальная проверка:
Явное приведение к String меняет статический тип выражения и позволяет выбрать print(String). Если объект действительно не является String, такое приведение завершится ClassCastException, поэтому его следует применять только при гарантии типа.
Для переопределённых методов есть дополнительный этап: после выбора сигнатуры JVM может направить вызов в реализацию фактического класса объекта. Но динамический полиморфизм не превращает перегрузку в динамический выбор по типу аргумента.
В библиотеке логирования были перегрузки log(Object) и log(String). Сервис передавал сообщения через параметр типа Object, поэтому строки обрабатывались общей веткой: форматирование выполнялось иначе, а диагностические поля терялись.
Рассматривались варианты:
(String) — быстро, но хрупко и повышает риск ClassCastException;String или использовать разные явно названные методы — немного увеличивает объём изменений, зато делает контракт очевидным.Выбрали передачу строк как String и отдельный метод для структурированных объектов. После этого выбор метода стал предсказуемым, а намерение вызывающего кода — видимым из сигнатуры.
1. Что произойдёт, если объявить переменную как String?
Будет выбран print(String). При выборе перегрузки важен статический тип выражения в конкретном месте вызова, поэтому изменение объявления переменной меняет результат даже при том же объекте.
2. Что произойдёт при вызове new Printer().print(null)?
Из двух перегрузок будет выбрана print(String): null совместим с любым ссылочным типом, а String является более специфичным типом по сравнению с Object. Если добавить, например, независимую перегрузку print(Integer), вызов print(null) станет неоднозначным и не скомпилируется, потому что ни String, ни Integer не специфичнее другого.
3. Может ли переопределение изменить вывод при таком вызове?
Да, но только реализацию уже выбранной сигнатуры. Если подкласс переопределит print(Object), то вызов через ссылку базового типа может попасть в переопределённый print(Object) подкласса. Переопределённый print(String) не будет вызван вместо print(Object), поскольку выбор перегрузки выполняется раньше и основан на статическом типе аргумента.