Причина, по которой вы получаете метод базового класса, заключается в том, что версия базового класса func
объявлена private
, а Java не позволяет частным методам переопределяться подклассами. Это означает, что если вы расширяете базовый класс и по чистой случайности решаете присвоить закрытой функции-члену то же имя, что и закрытой функции-члену в базовом классе, вы случайно не измените поведение функций-членов базового класса. Вы действительно работаете с объектом типа derived
, но поскольку ссылка статически типизируется как mainApp
, вызов func
интерпретируется как вызов приватного метода func
в mainApp
а не публичный метод func
в derived
. Изменение кода для чтения
derived d = new derived();
d.func();
исправляет это, потому что статический тип d
теперь derived
, а вызов func
имеет другое значение.
На уровне байт-кода JVM инструкция, используемая для вызова закрытых функций-членов, равна invokespecial
, тогда как инструкция, используемая для вызова нормальных переопределяемых функций-членов, равна invokevirtual
. У обоих совершенно разная семантика; invokespecial
начинает поиск в текущем классе (или некотором базовом классе для таких вещей, как конструкторы), тогда как invokevirtual
ищет в классе, соответствующем типу объекта во время выполнения.