Нека да погледнем по друг начин на нашия пример. В следната програма интерфейсът на play( ) е променен в процеса на подтискането му, което значи че той не е подтиснат (метода), а е претоварен. Компилаторът позволява да се претоварват методи затова не се оплаква. Поведението обаче вероятно не съвпада с желаното. Ето примера:
//: c07:WindError.java
// Accidentally changing the interface
class NoteX {
public static final int
MIDDLE_C = 0, C_SHARP = 1, C_FLAT = 2;
}
class InstrumentX {
public void play(int NoteX) {
System.out.println("InstrumentX.play()");
}
}
class WindX extends InstrumentX {
// OOPS! Changes the method interface:
public void play(NoteX n) {
System.out.println("WindX.play(NoteX n)");
}
}
public class WindError {
public static void tune(InstrumentX i) {
// ...
i.play(NoteX.MIDDLE_C);
}
public static void main(String[] args) {
WindX flute = new WindX();
tune(flute); // Not the desired behavior!
}
} ///:~
Тук изпъква и друг смущаващ аспект. В InstrumentX методът play( ) взема int с идентификатор NoteX. Тоест въпреки че NoteX е име на клас, то също може да се използува като идентификатор без оплаквания. Но в WindX play( ) взима NoteX манипулатор който има за идентификатор n. (Въпреки че може даже да се напише play(NoteX NoteX) без грешка.) По този начин сякаш програмистът е възнамерявал да подтисне play( ) но е направил малка печатна грешка. Компилаторът, обаче, е предположил че претоварване наместо подтискане е било намерението. Забележете, че ако следвате стандартната за имената в Java конвенция идентификаторът на аргумента би бил noteX, което би го отличавало от име на клас.
В tune InstrumentX i е изпратеното от play( ) съобщение с един от членовете на NoteX — (MIDDLE_C) като аргумент. Тъй като NoteX съдържа int дефиниции, това значи че int версията на сега претоварения play( ) метод се вика, а понеже той не е подтиснат се използва версията му в базовия клас.
Изходът е:
InstrumentX.play()
Това сигурно не е полиморфно извикване на метод. Като веднъж сте разбрали какво става, може да оправите нещата много лесно, но представете си колко трудно би могло да бъде това, ако този ред бе погребан в голяма програма.
Във всичките примери с инструменти методите на базовия клас Instrument бяха винаги “празни” методи. Щеше да е нередно, грешно, ако тези методи бяха някога викани. Така е защото целта на Instrument е да създаде общ интерфейс за всичките класове извлечени от него.
Единствената причина да се създаде такъв общ интерфейс е че така той може да се изрази различно за различните извлечени типове. Съставя се основна форма, така че може да се каже какво е общо между всичките типове. Друг начин да се изкаже това е назоваването на Instrument абстрактен базов клас (или просто абстрактен клас). Създавате абстрактен клас когато искате да манипулирате множество от обекти чрез неговия общ интерфейс. Всички методи на извлечен клас които имат същата сигнатура като на съответния метод на базовия клас ще бъдат викани автоматично когато трябва чрез механизма на късното свързване. (Обаче, както се видя в предната секция, ако името е същото но сигнатурата различна ще се получи фактически не подтискане, а претоварване, което надали е желания резултат.)
Ако имате абстрактен клас като Instrument, обектите които бихте създали от него почти винаги нямат смисъл. Тоест Instrument е само за да изрази интерфейса, а не канкретна реализация, така че създаване на обекти от Instrument няма смисъл и вероятно вие ще искате да пресотвратите създаването им от потребителя. Това може да се изпълни като се направят всичките методи на Instrument да извеждат съобщения за грешка, но това забавя обратната връзка чак до времето на изпълнение и изисква изтощително тестване от страна на потребителя (на класа - бел.пр.). Винаги е по-добре да се хващат проблемите още по време на компилация.
Java има механизъм за случая наречен абстрактен метод. Това е незавършен метод; само декларация без тяло на метода. Ето синтаксисът на декларацията на абстрактен метод:
abstract void X();
Клас съдържащ абстрактни методи се нарича абстрактен клас. Ако класът съдържа един или повече абстрактни методи той задължително трабва да бъде квалифициран с abstract. (Иначе компилаторът ще издаде съобщение за грешка.)
Ако абстрактният клас е незавършен, какво се очаква да направи компилаторът ако някой се опитва да създаде обект от него? Не може безопасно да се създаде обект от абстрактен клас, токо че ще получите съобщение за грешка от компилатора. По този начин компилаторът осигурява чистотата на абстрактния клас и няма нужда да се безпокоите от погрешно използване.
Ако наследите от абстрактен клас и искате да се създават обекти от новия тип, трябва да напишете методи които подтискат всички абстрактни методи. Ако не го направите (и може да изберете този вариант) извлеченият клас също е абстрактен и компилаторът ще ви застави да квалифицирате този клас с ключовата дума abstract.
Възможно е да се декларира клас с abstract без да се включват abstract методи. Това е полезно когато не искате да има abstract методи и все пак искате да предотвратите създаване на обекти от него клас.
Класът Instrument лесно може да се превърне в абстрактен. Само някои от методите ще бъдат абстрактни, понеже не се изисква всичките да бъдат. Ето как изглежда:
…
Ето оркестърският пример променен да използва abstract класове и методи:
//: c07:Music4.java
// Abstract classes and methods
import java.util.*;
abstract class Instrument4 {
int i; // storage allocated for each
public abstract void play();
public String what() {
return "Instrument4";
}
public abstract void adjust();
}
class Wind4 extends Instrument4 {
public void play() {
System.out.println("Wind4.play()");
}
public String what() { return "Wind4"; }
public void adjust() {}
}
class Percussion4 extends Instrument4 {
public void play() {
System.out.println("Percussion4.play()");
}
public String what() { return "Percussion4"; }
public void adjust() {}
}
class Stringed4 extends Instrument4 {
public void play() {
System.out.println("Stringed4.play()");
}
public String what() { return "Stringed4"; }
public void adjust() {}
}
class Brass4 extends Wind4 {
public void play() {
System.out.println("Brass4.play()");
}
public void adjust() {
System.out.println("Brass4.adjust()");
}
}
class Woodwind4 extends Wind4 {
public void play() {
System.out.println("Woodwind4.play()");
}
public String what() { return "Woodwind4"; }
}
public class Music4 {
// Doesn't care about type, so new types
// added to the system still work right:
static void tune(Instrument4 i) {
// ...
i.play();
}
static void tuneAll(Instrument4[] e) {
for(int i = 0; i < e.length; i++)
tune(e[i]);
}
public static void main(String[] args) {
Instrument4[] orchestra = new Instrument4[5];
int i = 0;
// Upcasting during addition to the array:
orchestra[i++] = new Wind4();
orchestra[i++] = new Percussion4();
orchestra[i++] = new Stringed4();
orchestra[i++] = new Brass4();
orchestra[i++] = new Woodwind4();
tuneAll(orchestra);
}
} ///:~
Може да видите че няма промени освен в базовия клас.
Полезно е да се създават abstract класове и методи понеже те правят явна абстрактността на класа и казват и на потребителя и на компилатора какво се цели.
Сподели с приятели: |