Klasy abstrakcyjne
Czasami zachodzi potrzeba stworzenia klasy bazowej, która definiuje jedynie ogólną postać obiektów, pozostawiając szczegóły klasom pochodnym. Taka klasa bazowa określa tylko naturę metod, które muszą zostać zaimplementowane w klasach pochodnych. Sama nie dostarcza implementacji jednej lub więcej z tych metod. Sytuacja tak pojawia się na przykład, gdy klasa bazowa nie może stworzyć sensownej implementacji metody. Przykładem może być ostatnia wersja klasy TwoDShape, w której definicja metody area() stanowiła jedynie atrapę, gdyż nie obliczała ani nie wyświetlała pola powierzchni dla żadnego typu obiektu.
Tworząc własne biblioteki klas, przekonasz się, że brak sensownej definicji metody w kontekście klasy bazowej nie należy do rzadkości. Istnieją dwa sposoby postępowania w takiej sytuacji. Pierwszy pokazałem w poprzednim przykładzie i polega na wyświetleniu komunikatu ostrzegawczego. Chociaż rozwiązanie takie może być przydatne na przykład podczas wyszukiwania błędów w działaniu programu, to w większości sytuacji nie jest właściwe. Mogą bowiem istnieć metody, które muszą zostać
przesłonięte, aby klasa pochodna miała sens. Weźmy na przykład klasę Triangle. Będzie ona niekompletna, jeśli nie zdefiniujemy w niej metody area(). W takim przypadku przydałby się sposób zapewnienia, że klasa pochodna rzeczywiście przesłoni wszystkie metody, które powinna. Rozwiązaniem tego problemu w języku Java jest metoda abstrakcyjna.
Metodę abstrakcyjną tworzymy, poprzedzając jej deklarację modyfikatorem abstract. Metoda abstrakcyjna nie ma ciała i wobec tego nie zostaje zaimplementowana w klasie bazowej. Klasa pochodna musi zatem przesłonić metodę abstrakcyjną, bo nie może użyć wersji zdefiniowanej w klasie bazowej. Ogólną postać deklaracji metody abstrakcyjnej przedstawiłem poniżej:
Jak łatwo zauważyć, deklaracja nie zawiera ciała metody. Modyfikator abstract można stosować tylko dla zwykłych metod. Nie możesz go użyć dla metody zadeklarowanej jako static ani dla konstruktora. Klasa zawierająca jedną lub więcej metod abstrakcyjnych musi również zostać zadeklarowana jako abstrakcyjna poprzez umieszczenie modyfikatora abstract przed jej deklaracją. Ponieważ klasa abstrakcyjna nie definiuje kompletnej implementacji, nie można tworzyć obiektów tej klasy. Próba
utworzenia obiektu klasy abstrakcyjnej za pomocą operatora new spowoduje błąd kompilacji.
Gdy klasa pochodna dziedziczy po klasie abstrakcyjnej, musi dostarczyć implementacji wszystkich metod abstrakcyjnych klasy bazowej. Jeśli tego nie robi, musi również zostać zadeklarowana jako abstract. Zatem atrybut abstract jest dziedziczony do momentu, aż powstanie klasa dostarczająca kompletnej implementacji.
Stosując klasę abstrakcyjną, możemy ulepszyć klasę TwoDShape. Skoro nie istnieje sensowna implementacja metody wyznaczającej pole powierzchni niezdefiniowanej figury dwuwymiarowej, to w obecnej wersji poprzedniego programu zadeklarujemy metodę area() jako abstract w klasie TwoDShape. W związku z tym klasę TwoDShape również musimy zadeklarować jako abstract. Oznacza to, że wszystkie jej klasy pochodne muszą przesłonić metodę area(). Kompletny tekst obecnej wersji programu przedstawiłem na listingu 7.18.
Listing 7.18. AbsShape.java
abstract class TwoDShape { //Klasa TwoDShape zadeklarowana jako abstract.
private double width;
private double height;
private String name;
// Konstruktor domyślny.
TwoDShape() {
width = height = 0.0;
name = "none";
}
// Konstruktor z parametrami.
TwoDShape(double w, double h, String n) {
width = w;
height = h;
name = n;
}
// Tworzy obiekt, którego szerokość jest taka sama jak wysokość.
TwoDShape(double x, String n) {
width = height = x;
name = n;
}
// Tworzy obiekt na podstawie innego obiektu.
TwoDShape(TwoDShape ob) {
width = ob.width;
height = ob.height;
name = ob.name;
}
// Metody dostępowe dla składowych width i height.
double getWidth() { return width; }
double getHeight() { return height; }
void setWidth(double w) { width = w; }
void setHeight(double h) { height = h; }
String getName() { return name; }
void showDim() {
System.out.println("Szerokość i wysokość: " + width + " i " + height);
}
// Teraz metoda area() jest abstrakcyjna.
abstract double area(); Również metoda area() została zadeklarowana jako abstract.
}
// Klasa pochodna klasy bazowej TwoDShape reprezentująca trójkąty.
class Triangle extends TwoDShape {
private String style;
// Konstruktor domyślny.
Triangle() {
super();
style = "nieokreślony";
}
// Konstruktor z parametrami.
Triangle(String s, double w, double h) {
super(w, h, "trójkąt");
style = s;
}
// Konstruktor o jednym parametrze
Triangle(double x) {
super(x, "trójkąt"); // wywołanie konstruktora klasy bazowej
style = "wypełniony";
}
// Tworzy obiekt na podstawie innego obiektu.
Triangle(Triangle ob) {
super(ob); // przekazuje obiekt konstruktorowi klasy TwoDShape
style = ob.style;
}
double area() {
return getWidth() * getHeight() / 2;
}
void showStyle() {
System.out.println("Trójkąt jest " + style);
}
}
// Klasa pochodna klasy bazowej TwoDShape reprezentująca prostokąty.
class Rectangle extends TwoDShape {
// Konstruktor domyślny.
Rectangle() {
super();
}
// Konstruktor z parametrami.
Rectangle(double w, double h) {
super(w, h, "prostokąt"); // wywołanie konstruktora klasy bazowej
}
// Konstruktor o jednym parametrze.
Rectangle(double x) {
super(x, "prostokąt"); // wywołanie konstruktora klasy bazowej
}
// Tworzy obiekt na podstawie innego obiektu.
Rectangle(Rectangle ob) {
super(ob); // przekazuje obiekt konstruktorowi klasy TwoDShape
}
boolean isSquare() {
if(getWidth() == getHeight()) return true;
return false;
}
double area() {
return getWidth() * getHeight();
}
}
class AbsShape {
public static void main(String args[]) {
TwoDShape shapes[] = new TwoDShape[4];
shapes[0] = new Triangle("pusty", 8.0, 12.0);
shapes[1] = new Rectangle(10);
shapes[2] = new Rectangle(10, 4);
shapes[3] = new Triangle(7.0);
for(int i=0; i < shapes.length; i++) {
System.out.println("Typ obiektu: " + shapes[i].getName());
System.out.println("Powierzchnia wynosi " + shapes[i].area());
System.out.println();
}
}
}
Program ten pokazuje, że klasy pochodne klasy bazowej TwoDShape muszą przesłaniać metodę area(). Aby się o tym przekonać, spróbuj utworzyć klasę pochodną, która nie przesłania metody area(). Próba kompilacji tej klasy zakończy się błędem. Oczywiście nadal możemy tworzyć zmienne referencyjne typu TwoDShape, co zresztą robi obecna wersja programu. Nie możemy natomiast tworzyć obiektów klasy TwoDShape. Z tego powodu rozmiar tablicy shapes deklarowanej w metodzie main() skurczył
się do czterech. I jeszcze jedno: zwróć uwagę, że klasa bazowa TwoDShape nadal zawiera metody showDim() i getName(), których deklaracje nie zostały poprzedzone modyfikatorem abstract. Sytuacja taka jest nie tylko dozwolona, ale nawet często spotykana. Klasy abstrakcyjne mogą zawierać konkretne metody używane przez klasy pochodne w niezmienionej postaci. Jedynie metody zadeklarowane jako abstract muszą zostać przesłonięte w klasach pochodnych.