6. Klasy pochodne i interfejsy
6.1. Rozszerzanie klas
Języki obiektowo zorientowane (obiektowe) stały się standardem ze względu na następujące zalety:
Zwartą i przejrzystą formę zapisu wiedzy;
Możliwość tworzenia bibliotek modułów, które są następnie wielokrotnie wykorzystywane;
Skrócenie procesu usuwania błędów i ułatwienie przeróbek oprogramowania.
W języku obiektowo zorientowanym świat jest zbiorem obiektów pogrupowanych w klasy.
Podstawową cechą hierarchicznego opisu obiektów jest zasada dziedziczenia: wszystkie własności opisane na wyższym poziomie obowiązują również na poziomach niższych.
Informację o tym, że dana klasa jest potomną (rozszerzeniem) innej klasy, wprowadzamy za pomocą słowa kluczowego extends:
Przykład:
class Student extends Osoba {
int nrIndeksu;
static int liczbaStudentów = 0;
final static String JESTEM = "Jestem student ";
final static String INDEKS = ", mam indeks Nr ";
Student ( String imię, String nazwisko ) {
super ( imię, nazwisko, NIEZNANE );
nrIndeksu = ++liczbaStudentów;
}
Student ( String imię, String nazwisko, String nrTel ) {
super ( imię, nazwisko, nrTel );
nrIndeksu = ++liczbaStudentów;
}
void jestem ( ) {
System.out.println ( JESTEM+ imię + " "
+ nazwisko + INDEKS + nrIndeksu );
}
Zgodnie z zasadą dziedziczenia cechy imię i nazwisko przechodzą do klasy potomnej Student. W klasie tej wprowadzamy jedynie cechę wyróżniającą studenta − numer jego indeksu.
Podobnie jak słowo kluczowe this oznaczało dany obiekt, słowo super odsyła do klasy nadrzędnej. W konstruktorze obiektu klasy Student widzimy zatem odwołanie do konstruktora klasy Osoba.
W klasie potomnej Student użyliśmy tej samej nazwy jestem dla metody wyświetlającej dane obiektu, co w klasie bazowej Osoba. Jednakże metoda klasy potomnej daje inny wynik, niż metoda klasy bazowej. Takie postępowanie nazywamy przesłanianiem metody (patrz rysunek).
Chcąc wykorzystać w klasie potomnej wersję pierwotną metody jestem musimy do niej odwołać się poprzez słowo kluczowe super. Możemy przy tym nadal używać nazwy jestem, lecz musimy zmienić sygnaturę. Takie postępowanie nazywamy przeciążaniem metody. Poniższy przykład pokazuje przeciążanie metody jestem :
void jestem ( String nrTel ) {
super.jestem ( );
}
Widzimy tu odwołanie do oryginalnej wersji tej metody przechowywanej w klasie Osoba.
Jeżeli utworzymy obiekt
Student s = new Student ( "Jan", "Kowalski" );
i wywołamy jego metodę prezentacji
s.jestem ( );
to otrzymamy napis
Jestem student Jan Kowalski, mam indeks Nr 1
Natomiast instrukcja
s.jestem ( nrTel );
wyświetli napis
Jestem Jan Kowalski, tel. ??? , mam Nr 1
zgodnie z definicją metody jestem podaną w klasie Osoba.
6.2. Pakiety
Pisząc w Javie większy program wygodnie pogrupować klasy w grupy zwane pakietami. W tym celu w pierwszym wierszu zbioru źródłowego podajemy nazwę pakietu, np.:
package ludzie;
class Osoba {
… // opis klasy
}
class Student {
… // opis klasy
}
Każda klasa w Javie jest częścią jakiegoś pakietu. Jeżeli nie określimy nazwy pakietu deklaracją package, to klasa zostanie przypisana do domyślnego pakietu bez nazwy.
Tekst źródłowy i kod bajtowy klasy należącej do pakietu nienazwanego powinny się znajdować w katalogu bieżącym (classpath = . ), a odpowiednie zbiory dla klas należących do pakietu nazwanego − w podkatalogu o nazwie zgodnej z nazwą pakietu.
Na przykład, jeżeli katalogiem bieżącym jest
C:\Java\Programy
to klasy Osoba będziemy szukali w katalogu
C:\Java\Programy\ludzie
W razie potrzeby, można poinformować kompilator i interpreter, gdzie znajdują się teksty źródłowe i kody bajtowe klas:
javac -sourcepath <ścieżka> <nazwa.java>
java -classpath <ścieżka> <nazwa>
Dodatkową zaletą podziału klas na pakiety jest zmniejszenie ryzyka konfliktu nazw. Załóżmy, że chcemy skorzystać z opracowanej przez kogoś bazy danych osobowych, której klasy należą do pakietu baza. W tym celu wystarczy zaimportować ten pakiet:
import baza;
Okazuje się jednak, że w pakiecie tym jest również klasa Osoba, nie mająca wiele wspólnego z klasą o tej samej nazwie z pakietu ludzie. W takim przypadku kompilator nie pozwoli nam utworzyć obiektu
Osoba os = new Osoba ("Jan", "Kowalski");
Trzeba będzie określić dokładnie, o klasę z którego pakietu chodzi:
ludzie.Osoba os = new ludzie.Osoba ("Jan", "Kowalski");
Pakiety mogą być zagnieżdżane, np.
kadry.płace.pracownicy_etatowi.Pracownik
jest odwołaniem do klasy Pracownik, która należy do pakietu pracownicy_etatowi, zagnieżdzonego w pakietach płace i kadry.
Podobnie jak inne nowoczesne języki, Java daje użytkownikowi bogaty zestaw gotowych klas. Można je znaleźć w bibliotece klas java. Należą do niej następujące pakiety:
java.lang podstawowe elementy języka
java.io metody wejścia-wyjścia
java.util metody pomocnicze
java.awt elementy graficzne
java.applet elementy apletów
java.net elementy sieciowe
java.math metody matematyczne
java.text metody przetwarzania tekstu
java.security elementy systemu bezpieczeństwa
java.sql elementy dostępu do baz danych
Importować można określoną klasę, np.
import ludzie.Student;
lub wszystkie klasy pakietu:
import ludzie.*;
W przeciwieństwie do znanego z C polecenia #include , importowanie klas w Javie odbywa się dynamicznie: interpretator ładuje do pamięci tylko te klasy, które są potrzebne przy wykonywaniu programu.
6.3. Kontrola dostępu
Wewnątrz klasy są zawsze dostępne zmienne, stałe i metody zadeklarowane w tej klasie. Natomiast widoczność i dziedziczenie pomiędzy klasami i pakietami regulują modyfikatory dostępu:
public |
widoczne wszędzie, gdzie widoczna jest dana klasa, i dziedziczone przez wszystkie jej klasy potomne |
private |
widoczne tylko w ramach danej klasy |
protected |
widoczne w danym pakiecie i dziedziczone przez klasy potomne |
(package) |
elementy bez modyfikatorów dostępu są widoczne w danym pakiecie i dziedziczone przez klasy potomne z tego samego pakietu |
Widzialność elementów można ująć dodatkowo w takiej tabeli:
Widoczne |
public |
protected |
package |
private |
wewnątrz danej klasy |
tak |
tak |
tak |
tak |
w każdej klasie tego samego pakietu |
tak |
tak |
tak |
nie |
w każdej klasie potomnej umieszczonej w tym samym pakiecie |
tak |
tak |
tak |
nie |
w każdej klasie potomnej umieszczonej w innym pakiecie |
tak |
tak |
nie |
nie |
w klasie z innego pakietu |
tak |
nie |
nie |
nie |
Skutecznym środkiem budowy programów odpornych na błędy i łatwych w utrzymaniu jest ograniczanie dostępu do wnętrza modułów:
W języku obiektowym podstawowym modułem jest klasa. Należy zatem dążyć do tego, aby tylko niektóre zmienne i metody były widoczne na zewnątrz klasy (należały do jej części publicznej). Reszta powinna być ukryta (encapsulated) przed niepowołanym dostępem, czyli zadeklarowana jako private lub protected.
Dobrym zwyczajem jest traktowanie wszystkich zmiennych instancyjnych jako prywatnych. Jeżeli któraś z nich ma być dostępna z zewnątrz, to określamy dla niej specjalną metodę dostępu:
class Student extends Osoba {
private int nrIndeksu;
static int liczba = 1;
public int getIndex ( ) {
return nrIndeksu;
}
6.4. Interfejsy
W odróżnieniu od C++, w Javie wszystkie klasy są pochodnymi klasy bazowej Object. Jest to korzeń drzewa hierarchii klas. W drzewie tym obowiązuje pojedyncze dziedziczenie: klasa pochodna może mieć tylko jednego przodka (jedną klasę nadrzędną).
Pozwala to uniknąć dylematów typu "romboidalnego":
A
B C
D
Czasem jednak dziedziczenie pojedyncze staje się niewygodne: jeżeli klasy C i E mają korzystać z tych samych metod, to musimy umieścić te metody w klasie bazowej A. Powoduje to zbytnie rozrastanie się klas bliskich korzeniowi Object.
Aby temu zaradzić, wprowadzono w Javie wielokrotne dziedziczenie metod. Służy do tego konstrukcja zwana interfejsem:
interface < nazwa > {
… // nagłówki metod
}
Wszystkie metody deklarowane w interfejsie są publiczne z definicji. Klasa korzystająca z interfejsu musi podać implementacje wszystkich jego metod:
class < nazwa klasy > implements < nazwa interfejsu > {
… // implementacje metod
}
W Javie istnieje tylko jedno drzewo hierarchii klas. Równolegle można definiować drzewa hierarchii interfejsów. Klasa może korzystać z dowolnej liczby interfejsów, lecz musi implementować wszystkie ich metody.
Aplikacja Lektor
interface Kojarzenie {
Lektor kojarz_z ( Student student );
}
public class Lektor extends Osoba implements Kojarzenie {
public static int liczbaLektorów = 0;
public static int liczbaStudEN = 0;
public static int liczbaStudDE = 0;
public static Lektor [ ] lektorzy;
public static Student [ ] studenci;
public static Student [ ] studenciEN;
public static Student [ ] studenciDE;
private String język;
private int nrLektora;
private final static String JESTEM = "Jestem lektor ";
private final static String JĘZYK = ", prowadze jezyk ";
private final static String EN = "angielski";
private final static String DE = "niemiecki";
private final static int MAX_L = 2;
private final static int MAX_S = 4;
// blok inicjowania tablic
static {
lektorzy = new Lektor [ MAX_L ];
lektorzy [0] = new Lektor ( "Bond", EN );
lektorzy [1] = new Lektor ( "Brunner", DE );
studenci = new Student [ MAX_S ];
studenci [0] = new Student ( "Kowalski", EN );
studenci [1] = new Student ( "Cichocki", DE );
studenci [2] = new Student ( "Malinowski", EN );
studenci [3] = new Student ( "Janas", DE );
}
// konstruktory
Lektor ( String nazwisko, String język ) {
super ( NIEZNANE, nazwisko, NIEZNANE );
this.język = język;
nrLektora = liczbaLektorów++;
}
Lektor ( ) {
super ( NIEZNANE, NIEZNANE, NIEZNANE );
this.język = NIEZNANE;
nrLektora = liczbaLektorów++;
}
// prezentacja
void jestem ( ) {
System.out.print ( JESTEM );
if ( imię != NIEZNANE )
System.out.print ( imię + " " );
if ( nazwisko != NIEZNANE )
System.out.print ( nazwisko );
if ( język != NIEZNANE )
System.out.print ( JĘZYK + język );
System.out.println ( MAM + nrLektora );
}
// implementacja metod interfejsu
public Lektor kojarz_z ( Student student ) {
Lektor lektor = null;
for (int i = 0; i < lektorzy.length ; i++) {
if ( lektorzy [ i ].język == student.język )
lektor = lektorzy [ i ];
}
return lektor;
}
// ustalanie liczby studentów studiujących dany język
public static int policz ( String język ) {
int liczba = 0;
for ( int i = 0; i < studenci.length ; i++ )
if ( studenci [ i ].język == język ) liczba++;
return liczba;
}
// tworzenie tablic językowych
private static void wybierz ( Student [ ] studenciEN, Student [ ] studenciDE ) {
Lektor lektor = new Lektor ();
int j_EN = 0, j_DE = 0;
for (int i = 0; i < studenci.length; i++)
{
lektor = lektor.kojarz_z ( studenci [ i ] );
if ( lektor.język == EN )
{
studenciEN [ j_EN ] = studenci [ i ];
j_EN++;
}
else
{
studenciDE [ j_DE ] = studenci [ i ];
j_DE++;
}
}
}
private static void wybierz ( Student [ ] tablica, String język ) {
Lektor lektor = new Lektor ();
int j= 0;
for (int i = 0; i < studenci.length; i++)
{
lektor = lektor.kojarz_z ( studenci [ i ] );
if ( lektor.język == język ) {
tablica [ j ] = studenci [ i ];
j++;
}
}
}
// wyświetlanie wyników
public static void drukuj ( Osoba [ ] osoba ) {
for (int i = 0; i < osoba.length ; i++ )
osoba [ i ].jestem ( );
System.out.println("---------------");
}
// metoda główna
public static void main ( String args [ ] ) {
drukuj ( lektorzy );
drukuj ( studenci );
liczbaStudEN = policz ( EN );
liczbaStudDE = policz ( DE );
studenciEN = new Student [ liczbaStudEN ];
studenciDE = new Student [ liczbaStudDE ];
wybierz ( studenciEN, EN );
wybierz ( studenciDE, DE );
drukuj ( studenciEN );
drukuj ( studenciDE );
}
}
Adam Borkowski Język programowania „Java” 6-1
Adam Borkowski Język programowania „Java” 6-11
Adam Borkowski Język programowania „Java” 6-16
Cichocki
Obiekt B
Obiekt A
Klasa
pochodna 2
Klasa
pochodna 1
Klasa
podstawowa
Kowalski
Lektor
Student
Osoba
Część publiczna
Część
publiczna
Część
prywatna
Część
prywatna
Moduł A
Moduł B
Klasa
E
Klasa
C
Klasa
D
Klasa
B
Klasa Object
Interfejs 3
Interfejs 2
Interfejs 1
Klasa
A