static / dynamic binding
Binding — это момент, когда Java решает, какой именно метод вызвать. Бывает два вида, и разница в том, когда принимается решение.
Static binding (статическое связывание) — решение принимается на этапе компиляции. Так работают перегруженные методы и static-методы:
void print(String s) { ... }
void print(int n) { ... }
print("hi"); // компилятор уже знает: вызовется print(String)
Dynamic binding (динамическое связывание) — решение принимается в рантайме, по реальному типу объекта. Так работают переопределённые методы:
Animal a = new Dog();
a.makeSound(); // вызовется Dog.makeSound() — несмотря на то, что тип переменной Animal
Подробнее про сам механизм переопределения — в теме «Перегрузка и переопределение».
Копнуть глубже
Ключевое наблюдение: при dynamic binding важен реальный тип объекта в куче, а не тип переменной, через которую к нему обращаются:
Animal a = new Dog(); // тип переменной: Animal, реальный тип объекта: Dog
a.makeSound(); // вызовется версия Dog — по реальному типу
Это и есть полиморфизм в действии: один и тот же код (a.makeSound()) ведёт себя по-разному в зависимости от того, какой объект реально лежит в переменной.
static, private и final методы всегда решаются статически — их нельзя переопределить, значит выбирать не из чего, JVM точно знает, какой метод вызвать, ещё на компиляции.
Под капотом
Для dynamic binding у каждого класса с переопределяемыми методами JVM строит виртуальную таблицу методов (vtable) — список указателей на реальные реализации методов этого класса. Вызов a.makeSound() в байт-коде превращается в invokevirtual — инструкцию «посмотри в vtable реального класса объекта и вызови, что там лежит».
Это работает быстро (поиск по индексу в таблице, не перебор), но всё же чуть медленнее, чем invokestatic для static binding, где адрес метода уже известен заранее и просто «зашит» в байт-код. На практике эта разница пренебрежимо мала для подавляющего большинства программ.
• почему важен реальный тип объекта, а не тип переменной (если дошёл до 2-го слоя).