В языках программирования и теории типов полиморфизмом называется единообразная обработка разнотипных данных. Целью полиморфизма, применительно к объектно-ориентированному программированию, является использование одного имени для задания общих для класса действий.
В языке Java объектные переменные являются полиморфными (polymorphic). Например:
Переменная типа King может ссылаться как на объект типа King, так и на объект любого подкласса King.
Возьмем следующий пример:
Что происходит, когда вызывается метод, принадлежащий объекту king?
1. Компилятор проверяет объявленный тип объекта и имя метода, нумерует все методы с именем speech в классе AerusTargarien и все открытые методы speech в суперклассах AerusTargarien. Теперь компилятору известны возможные кандидаты при вызове метода.
2. Компилятор определяет типы передаваемых в метод аргументов. Если найден единственный метод, сигнатура которого совпадает с аргументами, происходит вызов. Этот процесс называется разрешением перегрузки (overloading resolution). Т.е. при вызове king.speech("Homo homini lupus est") компилятор выберет метод speech(String quotation), а не speech().
Если компилятор находит несколько методов с подходящими параметрами (или ни одного), выдается сообщение об ошибке.
Теперь компилятор знает имя и типы параметров метода,подлежащего вызову.
3. В случае, если вызываемый метод является private, static, final или конструктором, используется статическое связывание (early binding). В остальных случаях метод, подлежащий вызову, определяется по фактическому типу объекта, через который происходит вызов. Т.е. во время выполнения программы используется динамическое связывание (late binding).
4. Виртуальная машина заранее создает таблицу методов для каждого класса, в которой перечисляются сигнатуры всех методов и фактические методы, подлежащие вызову.
Таблица методов для класса King выглядит так:
Методы, унаследованные от Object, в данном примере игнорируются.
При вызове king.speech():
А что произойдет, если вызвать в конструкторе динамически связываемый метод конструируемого объекта? Например:
Результат:
Конструктор базового класса всегда вызывается в процессе конструирования производного класса. Вызов автоматически проходит вверх по цепочке наследования, так что в конечном итоге вызываются конструкторы всех базовых классов по всей цепочке наследования.
Это значит, что при вызове конструктора new AerysTargaryen() будут вызваны:
По определению, задача конструктора — дать объекту жизнь. Внутри любого конструктора объект может быть сформирован лишь частично — известно только то, что объекты базового класса были проинициализированы. Если конструктор является лишь очередным шагом на пути построения объекта класса, производного от класса данного конструктора, «производные» части еще не были инициализированы на момент вызова текущего конструктора.
- выполняйте в конструкторе лишь самые необходимые и простые действия по инициализации объекта
- по возможности избегайте вызова методов, не определенных как private или final (что в данном контексте одно и то же).
Использованы материалы:
В языке Java объектные переменные являются полиморфными (polymorphic). Например:
class King { public static void main(String[] args) { King king = new King(); king = new AerysTargaryen(); king = new RobertBaratheon(); } } class RobertBaratheon extends King { } class AerysTargaryen extends King { }
Переменная типа King может ссылаться как на объект типа King, так и на объект любого подкласса King.
Возьмем следующий пример:
class King { public void speech() { System.out.println("I'm the King of the Andals!"); } public void speech(String quotation) { System.out.println("Wise man said: " + quotation); } public void speech(Boolean speakLoudly){ if (speakLoudly) System.out.println("I'M THE KING OF THE ANDALS!!!11"); else System.out.println("i'm... the king..."); } } class AerysTargaryen extends King { @Override public void speech() { System.out.println("Burn them all..."); } @Override public void speech(String quotation) { System.out.println(quotation+" ... And now burn them all!"); } } class Kingdom { public static void main(String[] args) { King king = new AerysTargaryen(); king.speech("Homo homini lupus est"); } }
Что происходит, когда вызывается метод, принадлежащий объекту king?
1. Компилятор проверяет объявленный тип объекта и имя метода, нумерует все методы с именем speech в классе AerusTargarien и все открытые методы speech в суперклассах AerusTargarien. Теперь компилятору известны возможные кандидаты при вызове метода.
2. Компилятор определяет типы передаваемых в метод аргументов. Если найден единственный метод, сигнатура которого совпадает с аргументами, происходит вызов. Этот процесс называется разрешением перегрузки (overloading resolution). Т.е. при вызове king.speech("Homo homini lupus est") компилятор выберет метод speech(String quotation), а не speech().
Если компилятор находит несколько методов с подходящими параметрами (или ни одного), выдается сообщение об ошибке.
Теперь компилятор знает имя и типы параметров метода,подлежащего вызову.
3. В случае, если вызываемый метод является private, static, final или конструктором, используется статическое связывание (early binding). В остальных случаях метод, подлежащий вызову, определяется по фактическому типу объекта, через который происходит вызов. Т.е. во время выполнения программы используется динамическое связывание (late binding).
4. Виртуальная машина заранее создает таблицу методов для каждого класса, в которой перечисляются сигнатуры всех методов и фактические методы, подлежащие вызову.
Таблица методов для класса King выглядит так:
- speech() - King.speech()
- speech(String quotation) - King.speech(String quotation)
- speech(Boolean speakLoudly) - King.speech(Boolean speakLoudly)
- speech() - AerysTargaryen.speech()
- speech(String quotation) - AerysTargaryen.speech(String quotation)
- speech(Boolean speakLoudly) - King.speech(Boolean speakLoudly)
Методы, унаследованные от Object, в данном примере игнорируются.
При вызове king.speech():
- Определяется фактический тип переменной king. В данном случае это AerysTargaryen.
- Виртуальная машина определяет класс, к которому принадлежит метод speech()
- Происходит вызов метода.
А что произойдет, если вызвать в конструкторе динамически связываемый метод конструируемого объекта? Например:
class King { King() { System.out.println("Call King constructor"); speech(); //polymorphic method overriden in AerysTargaryen } public void speech() { System.out.println("I'm the King of the Andals!"); } } class AerysTargaryen extends King { private String victimName; AerysTargaryen() { System.out.println("Call Aerys Targaryen constructor"); victimName = "Lyanna Stark"; speech(); } @Override public void speech() { System.out.println("Burn " + victimName + "!"); } } class Kingdom { public static void main(String[] args) { King king = new AerysTargaryen(); } }
Результат:
Call King constructor Burn null! Call Aerys Targaryen constructor Burn Lyanna Stark!
Конструктор базового класса всегда вызывается в процессе конструирования производного класса. Вызов автоматически проходит вверх по цепочке наследования, так что в конечном итоге вызываются конструкторы всех базовых классов по всей цепочке наследования.
Это значит, что при вызове конструктора new AerysTargaryen() будут вызваны:
- new Object()
- new King()
- new AerysTargaryen()
По определению, задача конструктора — дать объекту жизнь. Внутри любого конструктора объект может быть сформирован лишь частично — известно только то, что объекты базового класса были проинициализированы. Если конструктор является лишь очередным шагом на пути построения объекта класса, производного от класса данного конструктора, «производные» части еще не были инициализированы на момент вызова текущего конструктора.
Однако динамически связываемый вызов может перейти во «внешнюю» часть иерархии, то есть к производным классам. Если он вызовет метод производного класса в конструкторе, это может привести к манипуляциям с неинициализированными данными, что мы и видим в результате работы данного примера.
Результат работы программы обусловлен выполнение алгоритма иницализации объекта:
- Память, выделенная под новый объект, заполняется двоичными нулями.
- Конструкторы базовых классов вызываются в описанном ранее порядке. В этот момент вызывается переопределенный метод speech() (да, перед вызовом конструктора класса AerysTargaryen), где обнаруживается, что переменная victimName равна null из-за первого этапа.
- Вызываются инициализаторы членов класса в порядке их определения.
- Исполняется тело конструктора производного класса.
- выполняйте в конструкторе лишь самые необходимые и простые действия по инициализации объекта
- по возможности избегайте вызова методов, не определенных как private или final (что в данном контексте одно и то же).
Использованы материалы:
- Eckel B. - Thinking in Java, 4th Edition - Chapter 8
- Cay S. Horstmann, Gary Cornell - Core Java 1 - Chapter 5
- Wikipedia
Комментариев нет:
Отправить комментарий