ПрограммированиеBackend разработчик

Объясните, как работает порядок разрешения перегрузки методов (method resolution order) при наследовании в Java, и какие подводные камни могут возникнуть.

Проходите собеседования с ИИ помощником Hintsage

Ответ.

В Java разрешение перегрузки (overloading) и переопределения (overriding) методов происходит по определённым правилам. При перегрузке (overloading) компилятор выбирает наиболее подходящий метод среди всех версий на этапе компиляции, исходя из типа аргументов в месте вызова.

Важные моменты:

  • При наличии перегруженных методов во всех классах и суперклассах, сначала ищутся наивысшие возможные совпадения в текущем классе, затем — в суперклассах.
  • Выбор метода осуществляется на этапе компиляции, а не выполнения.
  • Преобразования типов (widening, autoboxing, varargs) применяются при необходимости.

Пример:

class Super { void print(Number n) { System.out.println("Super Number"); } } class Sub extends Super { void print(Integer i) { System.out.println("Sub Integer"); } } ... Sub s = new Sub(); s.print(5); // Sub Integer s.print(5.0); // Super Number

Здесь print(Integer) будет вызван для аргумента типа Integer, а для типа Double — родительский метод.

Вопрос с подвохом.

Если в наследнике переопределён (override) и перегружен (overload) метод, какой из них вызовется при полиморфизме?

Ответ: При полиморфизме всегда выбирается версия метода на основе реального типа объекта для переопределённых методов (overriding). Для перегруженных методов выбор происходит на этапе компиляции на основе типа ссылки.

class A { void test(Number n) { System.out.println("A:Number"); } } class B extends A { void test(Integer n) { System.out.println("B:Integer"); } @Override void test(Number n) { System.out.println("B:Number"); } } ... A obj = new B(); obj.test(1); // Вызовет B:Number, несмотря на то, что существует test(Integer)

Примеры реальных ошибок из-за незнания тонкостей темы.


История

Разработчик добавил перегруженную версию метода в подкласс, ожидая, что она будет вызвана для всех наследников. В продакшене стало ясно, что вызов через ссылку родительского класса приводит к выбору метода по типу ссылки, а не объекта. В итоге, необходимая логика не выполнялась.


История

В крупном проекте ошибочно перегрузили метод equals(Object) с сигнатурой equals(MyClass obj), думая, что она заменит стандартный equals. При сравнении в коллекциях HashSet работал дефолтный equals(Object), что приводило к несоответствию логики и потерям данных.


История

При добавлении новой версии метода с varargs в иерархию классов, казалось, что для массивов будет вызываться нужная версия. Однако при передаче массива явно, компилятор выбрал не ту перегрузку, в результате чего обработка шла некорректно, а баг долго оставался незамеченным.