Трудността с Music.java може да се види чрез пускане на програмата. Изходът е Wind.play( ). Това е точно желаният изход, но не изглежда да има смисъл да работи по този начин. Погледнете tune( ) метода:
public static void tune(Instrument i) {
// ...
i.play(Note.middleC);
}
Той приема Instrument манипулатор. Така че как е възможно компилаторът да знае че Instrument манипулаторът сочи Wind в този случай а не Brass или Stringed? Компилаторът не може да знае. За да се постигне по-дълбоко разбиране на въпроса първо трябва да погледнем отблизо свързването.
Свързване при извикването на метод
Свързването на извикване на метод с тялото на метода се нарича binding. Когато то се направи преди програмата да е стартирана (от компилатора и линкера, ако има такъв), това се нарича ранно свързване. Може да не сте чували термина преди понеже той никога не е съществувал като алтернатива в процедурното програмиране. C компилаторите имат само един начин за викане на функции и това е ранното свързване.
Смущаващата част от горната програма се върти около ранното свързване, понеже компилаторът не би могъл да знае кой метод да извика след като приема Instrument манипулатор.
Решението се нарича късно свързване, което значи че свързването става по време на изпълнение и е основано на типа на обекта. Късното свързване също се нарича динамично свързване или свързване по време на изпълнение. Когато в езика има късно свързване, трябва да има и съответен механизъм за разпознаване на типа и викане на подходящия метод по време на изпълнение. Тоест компилаторът пак не знае типа на обекта, но механизмът на извикването на методи го намира и скача в необходимото тяло на метод. Механизмът на късното свързване се мени от език на език, но е лесно да се съобрази, че някакъв вид запис на информация в обектите би трябвало да съществува.
В Java се използва късно свързване освен ако методът е деклариран като final. Това значи че обикновено не трябва да се замисляте за типа на свързването – то става автоматично.
Защо бихте декларирали метод като final? Както беше отбелязано в предната глава, това предотвратява изменянето му от когото и да било. Може би по-важно, това ефективно “изключва” динамичното свързване или по-скоро казва на компилатора че то не е нужно. Това позволява на компилатора да генерира по-ефективен код за извикванията на методи които са final.
Постигане на точното поведение
След като знаете че свързването в Java полиморфно и късно, вие може да направите вашия код да говори на базовия клас и да сте сигурни, че товаще става с всякакви извлечени класове. Или, казано с други думи “пращате съобщение на обект и го оставяте да реши какво точно трабва да направи.”
Класическият пример в ООП е примерът “форма”. Това се използва всеобщо поради лесната му визуализация, но за нещастие може да смути начинаещия програмист и да го накара да мисли, че ОО програмирането е точно за програмиране на графика, което разбира се не е така.
Примерът има базов клас наречен Shape и различни извлечени типове: Circle, Square, Triangle и т.н. Причината примерът да е толкова подходящ е че е лесно да се каже “кръгът е вид форма” и това да бъде разбрано. Диаграмата на наследяването показва зависимостите:
(липсва тук - б.пр.)
Ъпкастингът може да се наложи в ред простичъък като този:
Shape s = new Circle();
Тук Circle обект се създава и манипулаторът непосредствено се присвоява на Shape, което би изглеждало грешка (присвояване един тип на друг) и все пак всичко е наред понеже Circle е Shape по наследство. Така че компилаторът се съгласява с операторите и не издава съобщение за грешка.
Когато викате един от методите на базовия клас (които са били подтиснати в идвлечения клас):
s.draw();
отново бихте могли да очаквате че draw( ) на Shape се вика понеже това е, най-сетне, манипулатор на Shape токо че откъде да знае компилаторът да прави нещо друго? И пак правилният (метод-б.пр.) Circle.draw( ) се вика поради късното свързване (полиморфизма).
Следващия пример показва това по малко различен начин:
//: c07:Shapes.java
// Polymorphism in Java
class Shape {
void draw() {}
void erase() {}
}
class Circle extends Shape {
void draw() {
System.out.println("Circle.draw()");
}
void erase() {
System.out.println("Circle.erase()");
}
}
class Square extends Shape {
void draw() {
System.out.println("Square.draw()");
}
void erase() {
System.out.println("Square.erase()");
}
}
class Triangle extends Shape {
void draw() {
System.out.println("Triangle.draw()");
}
void erase() {
System.out.println("Triangle.erase()");
}
}
public class Shapes {
public static Shape randShape() {
switch((int)(Math.random() * 3)) {
default: // To quiet the compiler
case 0: return new Circle();
case 1: return new Square();
case 2: return new Triangle();
}
}
public static void main(String[] args) {
Shape[] s = new Shape[9];
// Fill up the array with shapes:
for(int i = 0; i < s.length; i++)
s[i] = randShape();
// Make polymorphic method calls:
for(int i = 0; i < s.length; i++)
s[i].draw();
}
} ///:~
Базовият клас Shape основава общ интерфейс за всичко, което е наследено от Shape – тоест всички форми могат да бъдат чертани и трити. Извлечените класове подтискат тези дефиниции за да може да се осигури специфично необходимото поведение за всяка форма.
Главният клас Shapes съдържа static метод randShape( ) който произвежда манипулатор към случайно избран Shape обект всеки път когато го викате. Забележете че ъпкастинг става с всеки от return операторите, който взима манипулатор Circle, Square, или Triangle и го изпраща извън метода като връщан тип, Shape. Така че когато и да извикате този метод, никога нямате шанс да видите какъв точно тип е, понеже получавате обратно просто манипулатор Shape.
main( ) съдържа масив от Shape манипулатори запълнен чрез извиквания на randShape( ). В този момент вие знаете че имате Shapes (Форми -б.пр.), но не знаете нищо по специфично от това (а също и компилаторът не знае). Обаче като се движите през масива и викате draw( ) за всеки един, коректното специфично за типа поведение магически се проявява, както може да видите от примера на изхода:
Circle.draw()
Triangle.draw()
Circle.draw()
Circle.draw()
Circle.draw()
Square.draw()
Triangle.draw()
Square.draw()
Square.draw()
Разбира се тъй като формите са случайно избирани всеки път, изходът от вашите пускания все ще е различен. Доводът да се избере случайно избиране е да се покаже, че компилаторът няма никакви допълнителни знания, които биха му помогнали предварително да вземе решение. Всичките викания на draw( ) са чрез динамично свързване.
Разширяемост
Сега нека да се върнем към примера с музикалния инструмент. Поради полиморфизма може да добавяте колкото си искате типове без да изменяте метода tune( ). В добре проектирана ОО програма повечето или всичките ви методи ще следват примера на tune( ) и ще комуникират само с интерфейса на базовия клас. Такава програма е разширяема понеже може да се добавя функционалност чрез наследяване но абекти от базовия клас. Методите които манипулират интерфейса на базовия клас не е необходимо въобще да се пипат за да работят и с новите класове.
Да видим какво ще стане ако вземем примера с инструментите и добавим нови методи към базовия клас и няколко нови класа. Ето диаграмата:
…
Всички нови класове работят перфектно със стария, непроменен tune( ) метод. Даже ако tune( ) е в отделен файл и нови методи са добавени в интерфейса на Instrument, tune( ) работи коректно без рекомпилация. Ето реализацията на горната програма:
//: c07:Music3.java
// An extensible program
import java.util.*;
class Instrument3 {
public void play() {
System.out.println("Instrument3.play()");
}
public String what() {
return "Instrument3";
}
public void adjust() {}
}
class Wind3 extends Instrument3 {
public void play() {
System.out.println("Wind3.play()");
}
public String what() { return "Wind3"; }
public void adjust() {}
}
class Percussion3 extends Instrument3 {
public void play() {
System.out.println("Percussion3.play()");
}
public String what() { return "Percussion3"; }
public void adjust() {}
}
class Stringed3 extends Instrument3 {
public void play() {
System.out.println("Stringed3.play()");
}
public String what() { return "Stringed3"; }
public void adjust() {}
}
class Brass3 extends Wind3 {
public void play() {
System.out.println("Brass3.play()");
}
public void adjust() {
System.out.println("Brass3.adjust()");
}
}
class Woodwind3 extends Wind3 {
public void play() {
System.out.println("Woodwind3.play()");
}
public String what() { return "Woodwind3"; }
}
public class Music3 {
// Doesn't care about type, so new types
// added to the system still work right:
static void tune(Instrument3 i) {
// ...
i.play();
}
static void tuneAll(Instrument3[] e) {
for(int i = 0; i < e.length; i++)
tune(e[i]);
}
public static void main(String[] args) {
Instrument3[] orchestra = new Instrument3[5];
int i = 0;
// Upcasting during addition to the array:
orchestra[i++] = new Wind3();
orchestra[i++] = new Percussion3();
orchestra[i++] = new Stringed3();
orchestra[i++] = new Brass3();
orchestra[i++] = new Woodwind3();
tuneAll(orchestra);
}
} ///:~
Новите методи са what( ), който връща String манипулатор с описание на класа и adjust( ), който дава някакъв начин за нагласяване на всеки инструмент.
В main( ) когато слагате нещо вътре в Instrument3 масива автоматично става ъпкастинг към Instrument3.
Може да видите че метода tune( ) е блажено невеж относно промените на кода станали около него, и все пак работи коректно. Това е точно нещото, което се очаква да даде полиморфизмът. Промените във вашия код не засягат частите на програмата, които не трябва да се засягат. Казано с други думи полиморфизмът е най-важния инструмент който помага на програмиста да “отдели нещата които се променят от тези които ще останат така.”
Сподели с приятели: |