|
Paradygmat programowania zorientowanego
obiektowo w Javie opiera się na pojęciu klasy, które w
sposób istotny wzbogaca strukturę modularną i semantyczną
programów. Klasa jest modułem posiadającym: nazwę i atrybuty
w postaci pól danych i metod.
Definicja klasy jest jedynym sposobem
zdefiniowania nowego typu danych w Javie.
Posługując się pojęciami klasy, programista
może w wygodny i elegancki sposób definiować różnorodne typy
danych wykorzystując:
- strukturę hierarchiczną deklaracji klas,
- prefiksowanie klas tzw.
"dziedziczenie", umożliwiające tworzenie
hierarchii typu: ogólny - bardziej szczegółowy.
Klasa stanowi narzędzie do tworzenia nowych
typów danych, których elementy noszą nazwę obiektów (dla których definicja klasy stanowi wzorzec) i mogą
być przypisywane zmiennym obiektowym.
Definicja klasy przyjmuje następującą
formę:
[modyfikatory] class NazwaKlasy [extends NazwaNadklasy]
[implements NazwyInterfejsów]
{
// Ciało klasy:
// Tutaj znajdują się definicje pól danych , metod
// i klas wewnętrznych klasy
}
Elementy deklaracji pomiędzy nawiasami [ i ]
są opcjonalne. Deklaracja klasy definiuje następujące jej
właściwości:
- modyfikatory deklarują rodzaj klasy (np.: public, abstract
lub final static);
- NazwaKlasy określa nazwę deklarowanej klasy;
- NazwaNadklasy jest nazwą nadklasy ("prefiksu")
deklarowanej przez nas klasy;
- NazwyInterfejsów jest to lista, rozdzielona przecinkami, nazw interfejsów implementowanych przez naszą klasę.
W ciele klasy znajduje się dowolna liczba
definicji pól danych, metod i klas wewnętrznych. Definicje pól
danych i metod klasy mogą znajdować się w dowolnej
kolejności. Klasa może zawierać definicje pól danych (omówione w punkcie
2.3.2), metod (punkt 2.3.3) oraz klasy wewnętrzne, lokalne i anonimowe (omówione
w punkcie 2.3.7).
Pola danych są atrybutami klasy, pełniącymi
rolę podobną do zmiennych lub stałych. Są one deklarowane na
tych samych zasadach, co zmienne lokalne. Zaleca się, aby dla
przejrzystości programu stosować konwencję nazewnictwa, w
której nazwy deklarowanych pól danych klasy poprzedza się
literą 'm.' i znakiem podkreślenia (m od ang. data member).
Stosowanie się do tej konwencji zależy jednak od przyjętego
stylu programowania i, oczywiście, nie jest obowiązkowe.
Ogólnie, definicja pola danych klasy przyjmuje
postać:
modyfikatoryPola TypPola NazwaPola;
Gdzie:
- modyfikatoryPola określają tryb dostępu (np. private)
i właściwości pola (np. pole statyczne) (omówione w
punkcie 2.3.4)
- TypPola specyfikuje typ pola danych;
- NazwaPola określa nazwę deklarowanego pola.
Przykład 2.3 Definicja klasy Punkt
zawierającej tylko pola danych
class Punkt
{
int m_iWspX; //pole reprezentujące współrzędną x punktu
int m_iWspY; //pole reprezentujące współrzędną współrzędna y punktu
byte m_bKolor; //pole reprezentujące kolor punktu
}
Metody są modułami programowymi
przypominającymi funkcje z języka C++. Każda funkcja w Javie
jest związana z definicją klasy (spełnia rolę jej metody).
Definicja metody ma następującą składnię:
modyfikatory TypRezultatu NazwaMetody(ListaParametrówFormalnych)
{
//treść metody
}
Gdzie:
modyfikatory określają tryb dostępu i właściwości metody,
następnie TypRezultatu określa typ wyniku metody. Jeśli celem wykonania nie
jest uzyskanie rezultatu przekazywanego przez instrukcję return,
to w deklaracji typu metody występuje słowo void.
Następnym elementem deklaracji jest NazwaMetody, która musi być poprawnym identyfikatorem Javy. Po
nazwie metody definiujemy listę parametrów formalnych metody (ListaParametrówFormalnych), zbudowaną analogicznie jak w C/C++. Jeśli metoda
nie ma żadnych argumentów lista jest pusta. Odmiennie niż w
C/C++ parametr metody nie może być typu void.
Przykład 2.4 Klasa Test z
definicją pól danych i metod
class Test
{
// deklaracja pól danych klasy
int m_nWartosc = 0;
// deklaracje metod klasy
// po wykonaniu metody Wartosc() jako wynik otrzymujemy
// odpowiednią liczbę typu int, metoda ta jest więc funkcją
int Wartosc(int i)
{
if (i==10) return m_nWartosc;
if (i>10) return (m_nWartosc % 10);
return m_nWartosc;
}
// metoda Pokaz nie
void Pokaz(int i)
{
if (i == 10)
{
System.out.println("i = " + i);
return; //tu sterowanie może opuścić metodę gdy i == 10
}
System.out.println("i =" + i + " m_nWartosc =" + m_nWartosc);
//tu sterowanie może opuścić metodę gdy i != 10
}
}
W nawiasach '{ }'poniżej listy argumentów
znajduje się ciało metody.
Dla metod, których wynikiem działania jest
obiekt różny od void sterowanie musi opuścić metodę
tylko przy użyciu słowa kluczowego return. Wyrażenie
występujące po słowie return musi być zgodne z
zadeklarowanym typem wyniku metody. Dla metod o typie wyniku void
sterowanie opuszcza metodę albo poprzez słowo kluczowe return
bez parametrów lub w przypadku braku słowa return, po
wykonaniu wszystkich instrukcji w ciele metody.
W przypadku, gdy wynikiem działania metody
jest wartość inna niż void, przy wywołaniu możemy
zignorować wartość będącą wynikiem wykonania metody. Tak
więc poniższe wywołania funkcji są poprawne:
//funkcja zdefiniowana powyżej w klasie Test (zwraca int)
i = Wartosc(1,1); // "standardowe" wywołanie funkcji
Wartosc(1,1); // wywołanie funkcji jak procedury
Niedopuszczalne jest, aby jakikolwiek argument
miał taką samą nazwę, jak nazwa zmiennej lokalnej
zadeklarowanej w tej metodzie. W poniższym przykładzie wystąpi
więc błąd kompilacji:
int FunkcjaA(int i)
{
int j = 10;
for (int i = 1; i < j; i++ ) // ponowna deklaracja zmiennej 'i'
j=j+i;
return j
}
Możemy przeciążać (ang. function
overloading) nazwę metody, tzn. możemy stosować tę samą
nazwę dla różnych metod, byleby tylko różniły się one
między sobą liczbą lub rodzajem argumentów lub były metodami
różnych klas.
Przykład:
void MojaMetoda()
{ ... }
void MojaMetoda(int i, String s)
{ ... }
void MojaMetoda(String s)
{ ... }
W Javie modyfikatory możemy podzielić na dwa
rodzaje:
a) modyfikatory dostępu
b) modyfikatory właściwości modyfikowanego
elementu
Do modyfikatorów pierwszej grupy należą: private,
protected, public, package. Wpływają na reguły
widoczności i umożliwiają kontrolę dostępu do pól danych i
metod klasy z innych klas.
Deklaracja pola danych:
protected int m_nWiek;
powoduje, że będzie ono widoczne (będzie do
niego dostęp) w klasie, wszystkich podklasach (klasach
dziedziczących z klasy zawierających pole danych m_nWiek) i w całym pakiecie (użycie pakietów zostało omówione w punkcie 2.3.16).
Modyfikatory dostępu, mają następujące
znaczenie:
public - wszystkie klasy mają
dostęp do pól danych i metod public,
private - dostęp
do metod i pól danych posiadają jedynie inne metody tej samej
klasy,
protected - metoda lub pole
danych protected lub może być używana jedynie przez
metody swojej klasy oraz metody wszystkich jej klas pochodnych,
package - jest to modyfikator domyślny,
wszystkie metody i pola danych bez modyfikatora dostępu
traktowane są jako typu package. Metody (lub pola danych)
typu package mogą być używane przez inne klasy danego
pakietu.
Poniższa tabela pokazuje poziomy dostępu
określane przez każdy modyfikator:
| Modyfikator |
klasa |
podklasa |
pakiet |
wszędzie |
| private |
X |
|
|
|
| protected
|
X |
X |
X |
|
| public |
X |
X |
X |
X |
| package
|
X |
|
X |
|
Tabela 2-5
Modyfikatory dostępu w Javie
Oprócz modyfikatorów dostępu istnieją
jeszcze następujące modyfikatory właściwości:
- dla metod:
- dla pól danych:
Dla klas możemy używać modyfikatorów: public,
abstract, final.
Dziedziczenie pozwala klasom pochodnym
implementować na nowo dziedziczone metody. Oznacza to, że
odziedziczona metoda zostanie "przesłonięta" nową
implementacją. Modyfikator final powoduje, że
takie przesłanianie nie jest możliwe. Jeśli modyfikator final
dotyczy klasy oznacza to, że nie można z danej klasy
dziedziczyć (automatycznie wszystkie metody i pola danych są final).
W przypadku pól danych modyfikator final
oznacza, że wartość może dla tego pola zostać przypisana
tylko podczas tworzenia obiektu i nie może być później
modyfikowana. Pole danych jest stałą.
Pola danych typu transient nie
są trwałą częścią obiektu i nie zostają zachowane przy
archiwizacji obiektu. W JDK1.0 znacznik ten jest ignorowany.
Pole danych z modyfikatorem volatile oznacza, że może być ono modyfikowane
asynchronicznie, przez konkurencyjne wątki w programach
wielowątkowych (patrz rozdział Obsługa sytuacji wyjątkowych w Javie). W JDK1.0 znacznik ten jest ignorowany.
Pole danych lub metoda może zadeklarowana z
modyfikatorem static. Taka deklaracja oznacza, że pole
danych lub metoda dotyczy klasy a nie obiektu, tzn.
dla wszystkich obiektów danej klasy pole statyczne ma tę samą
wartość. Zadeklarujmy klasę opisującą rachunek bankowy. Dla
wszystkich rachunków jednego typu (tu konto osobiste)
oprocentowanie wkładów jest jednakowe, więc oprocentowanie
rachunku zadeklarowane zostało jako pole statyczne aby w razie
zmiany oprocentowania nie zmieniać jego wartości dla wszystkich
obiektów tej klasy.
Przykład 2.5 Deklaracja klasy KontoOsobiste
z polami i metodami statycznymi
class KontoOsobiste
{
static byte m_bOprocentowanie = 10;
private Osoba Wlasciciel;
...
// tu deklaracje innych pól danych klasy
...
static void ZmienOprocentowanie(byte nowyProcent)
{
m_bOprocentowanie = nowyProcent;
}
...
// tu deklaracje innych metod klasy
...
}
Odwołanie do statycznego pola danych może
mieć postać: NazwaKlasy.PoleDanych a dla metod statycznych NazwaKlasy.Metoda().
Aby zmienić wartość oprocentowania na 20
procent dla wszystkich obiektów typu KontoOsobiste wystarczy,
że wykonamy instrukcję:
KontoOsobiste.m_bOprocentowanie = 20;
Metody statyczne podobnie jak statyczne pola
danych są przypisane do klasy a nie konkretnego obiektu i
służą do operacji tylko na polach statycznych.
Przykład 2.6 użycie statycznych pól danych i metod.
public static void main(String[] args)
{
// Obiekt typu KontoOsobiste jeszcze nie istnieje, ale możemy
// zmienić oprocentowanie poprzez odwołanie do statycznego
// pola danych, gdyż jest ono częścią klasy a nie obiektu
// odwołanie poprzez nazwę klasy do pola statycznego
KontoOsobiste.m_bOprocentowanie = 20;
// utworzenie nowego obiektu rachunek typu KontoOsobiste
KontoOsobiste rachunek = new KontoOsobiste();
// odwołanie do pola statycznego poprzez obiekt
// (także spowoduje zmianę oprocentowania dla wszystkich
// obiektów klasy KontoOsobiste)
rachunek.ZmienOprocentowanie(40);
// odwołanie do metody statycznej poprzez nazwę klasy
KontoOsobiste.ZmienOprocentowanie(30);
}
Wszystkie odwołania do statycznych: pola
danych i metody są w powyższym przykładzie poprawne.
Możemy oczywiście zadeklarować pole danych
jako final static otrzymujemy wtedy stałą
klasy.
Statyczne pola danych mogą być
inicjalizowane. Inicjatory statycznych pól danych omówiono w punkcie
2.3.11.
Jak już wspomniano, klasa służy do
zdefiniowania typu danych, którego elementy zwane są obiektami
i referencje do tych obiektów stanowią wartości zmiennych
obiektowych.
Definicja klasy określa "budowę i
zachowanie" obiektu. Obiekt danej klasy jest generowany
dynamicznie na podstawie wzorca (definicji klasy). Raz
zdefiniowana klasa może mieć wiele obiektów. Przykładowo, po
zdefiniowaniu w programie klasy Punkt (Przykład 2.3) możemy użyć
wielu obiektów tej klasy ("egzemplarzy" klasy Punkt).
Deklaracja zmiennej typu obiektowego przyjmuje
postać podobną do deklaracji zmiennej typu wewnętrznego (omówionej w punkcie 2.2.4), a mianowicie:
NazwaTypu NazwaZmiennej [ = WartośćPoczątkowa ];
z tym, że w wyrażeniu nadającym wartość
początkową zmiennej może zostać utworzony nowy obiekt, który
zostanie przypisany do zadeklarowanej zmiennej obiektowej.
Przykładowa definicja zmiennej obiektowej
przyjmuje postać:
Date dzis = new Date();
W wyrażeniu tym deklarowana jest nowa zmienna
obiektowa dzis typu Date, następnie zostaje jej przypisany
utworzony za pomocą instrukcji new nowy obiekt (new Date()). Wyrażenie Date() jest
wywołaniem specjalnej metody zwanej konstruktorem, służącej do inicjalizacji obiektu. Jeśli
zdefiniowano wiele konstruktorów, możemy użyć dowolnego z
nich przy inicjalizacji zmiennych obiektowych, np.:
Date dzis = new Date(1997, 9, 25);
Gdy zmienną obiektową dzis
zadeklarujemy w postaci:
Date dzis;
oznacza to, że nie jest tworzony nowy obiekt a
jedynie zmienna, która może w przyszłości przechowywać
referencję do obiektów typu Date. Aby do tak
zadeklarowanej zmiennej przypisać nowy obiekt (a właściwie
referencję do niego) należy użyć instrukcji przypisania:
dzis = new Date(1997, 9, 30);
Natomiast, gdy chcemy przypisać istniejący
już obiekt:
dzis = wczoraj;
// gdzie zmienna 'wczoraj' jest referencją do obiektu typu Date,
// w tym przypadku obie zmienne będą przchowywały referencje do
// tego samego obiektu
Wszystkie typy wewnętrzne mają swoje
odpowiedniki obiektowe (np.: typ int ma odpowiadający mu
typ obiektowy Integer) i jako obiekty mają wiele
konstruktorów i metod, np.: metodę toString(), której
wynikiem jest reprezentacja znakowa zmiennej typu Integer.
Przykład 2.7 Użycie zmiennych obiektowych
odpowiadających zmiennym wewnętrznym
//deklaracja i inicjalizacja zmiennej obiektowej typu Integer
Integer iRok = Integer(1997);
// W wyniku użycia metody println() obiektu System.out
// na ekran wyprowadzony zostanie napis: Mamy teraz rok:1997
System.out.println("Mamy teraz rok:"+iRok.toString());
Dostęp do atrybutów obiektu, reprezentowanych
przez zmienną obiektową, realizujemy za pomocą wyrażeń
kropkowych postaci:
NazwaZmiennejObiektowej.NazwaAtrybutu;
gdzie:
- NazwaZmiennejObiektowej jest nazwą obiektu, w tym domyślną nazwą
bieżącego obiektu: this,
- NazwaAtrybutu jest nazwą pola danych lub nazwą metody.
Przykład 2.8 Dostęp do metod i pól danych klasy
Zdefiniujmy klasę:
class Wentylator
{
boolean m_bWlaczony = false;
int m_nTemperatura;
void sprawdzTemp(int temperatura)
{
m_nTemperatura = temperatura;
if (temperatura > 20)
m_bWlaczony = true;
else
m_bWlaczony = false;
}
} //Koniec deklaracji klasy Wentylator
//W klasie Budynek wywołana jest metoda sprawdzTemp oraz wypisana
// wartość pola danych m_bWlaczony obiektu klasy Wentylator
class Budynek
{
void Klimatyzacja()
{
//Deklaracja i utworzenie obiektu went klasy Wentylator.
// Użycie operatora new, powoduje utworzenie
// nowego obiektu (który zostaje przypisany do zmiennej
// obiektowej went).
Wentylator went = new Wentylator();
//Wykonanie metody sprawdzTemp() obiektu went klasy Wentylator
went.sprawdzTemp(21);
//Wypisanie na ekran wartości pola m_bWlaczony obiektu went
System.out.println("Wentylator działa = "+went.m_bWlaczony);
}
}
Rozszerzenie Javy z kwietnia 1997 pozwala na
użycie nie tylko inicjatorów klas, ale także inicjatorów
obiektów. Jeśli w klasie zdefiniowanych jest więcej
inicjatorów obiektów, to wykonywane są one w kolejności
wystąpienia, bezpośrednio po wywołaniu konstruktora nadklasy (konstruktor - to metoda o nazwie takiej jak nazwa klasy, wykonywana
przy tworzeniu nowego obiektu klasy z użyciem operatora new)
.Przykład
2.9 Inicjator obiektów
class MojaKlasa
{
// pole danych klasy:
boolean m_bSprawdzIniObj = false;
// konstruktor klasy MojaKlasa:
MojaKlasa()
{
System.out.print("m_bSprawdzIniObj = "+m_bSprawdzIniObj);
}
// inicjator obiektów klasy:
{
m_bSprawdzIniObj = true;
}
}
Gdy będziemy tworzyć obiekt klasy MojaKlasa, to wykonanie konstruktora spowoduje wyświetlenie na
ekranie napisu:
m_bSprawdzIniObj
= true
W kwietniu 1997 roku, definicję Javy
rozszerzono o pojęcia: klasy wewnętrznej, klasy anonimowej,
i klasy lokalnej. W poprzednich wersjach Javy możliwe
było definiowanie tylko takich klas, które musiały należeć
do pakietu. Od wersji Javy 1.1, możliwe jest definiowanie klas
należących do danej klasy (klas wewnętrznych), klas lokalnych
w bloku instrukcji oraz klas anonimowych deklarowanych w
wyrażeniu.
Klasą wewnętrzną
nazywamy klasę zdefiniowaną w miejscu, w którym może
wystąpić definicja pola danych lub metody. Klasy wewnętrzne, w
odróżnieniu od zewnętrznych (klas, w których definiowane są
klasy wewnętrzne), mogą mieć modyfikator static,
wskazujący że, klasa wewnętrzna ma takie same właściwości
jak klasa zewnętrzna. Oznacza to, że np. nie może
bezpośrednio odwoływać się do atrybutów klasy zewnętrznej
(musi użyć kwalifikowanego odnośnika). Oprócz tego, klasy
wewnętrzne mogą być oczywiście opatrzone modyfikatorami: protected
i public.
Zdefiniujmy dwie, proste, rozłączne
(zadeklarowane na tym samym poziomie w programie) klasy: Test1 i Licznik. W następnych przykładach w tym rozdziale klasa Licznik zostanie zdefiniowana jako wewnętrzna a następnie
jako anonimowa, aby pokazać różnice w deklarowaniu tych typów
klas.
Wykonanie metody main() klasy
Test1 powoduje wyprowadzenie na ekran tekstu: Licznik = 11.
Przykład 2.10 Rozłączne definicje klas
public class Test1
{
private int m_nLiczba = 0;
Licznik licznik;
public static void main(String args[])
{
Test1 test = new Test1(new Licznik());
test.licznik.ustaw(test.m_nLiczba = 10);
test.licznik.dodaj();
System.out.println("Licznik = "+test.licznik.wez());
}
Test1(Licznik licznik)
{
this.licznik = licznik;
}
}
class Licznik
{
private int m_nWartosc = 0;
public void ustaw(int i)
{
m_nWartosc = i;
}
public int wez()
{
return m_nWartosc;
}
public void dodaj()
{
m_nWartosc++;
}
}
Zadeklarujmy teraz klasę Licznik z
poprzedniego przykładu jako klasę wewnętrzną klasy Test2.
Uwaga:
W wersjach wcześniejszych niż JDK 1.1, do
wyświetlania na ekranie danych tekstowych używano obiektu System.out. W JDK1.1 obiekt System.out
powinien być używany tylko w celu sprawdzania poprawności
programu (ang. debug). Zamiast zastosowania System.out,
należy raczej utworzyć obiekt typu PrintWriter
i użyć go do wyprowadzenia wyników działania programu na
ekran. W poniższym przykładzie użycie tego nowego obiektu
zaznaczono czcionką pogrubioną i pochyloną (w pracy
zastosowano oba sposoby wyprowadzania danych na ekran).
Przykład 2.11 Wewnętrzne definicje klas
public class Test2
{
private int m_nLiczba = 0;
private Licznik licznik;
public static void main(String args[])
{
Test2 test = new Test2();
// utworzenie nowego obiektu klasy wewnętrznej
test.licznik = test.new Licznik();
test.licznik.ustaw(test.m_nLiczba = 10);
test.licznik.dodaj();
// zamias używanego w wersjach wcześniejszych niż JDK1.1
// obiektu System.out w postaci:
// System.out.println("Licznik ="+test.licznik.wez());
// użyjmy obiektu PrintWriter.
PrintWriter stdout = new PrintWriter(System.out, true);
stdout.println("Licznik = "+test.licznik.wez());
}
// Definicja klasy Licznik jako klasy wewnętrznej klasy Test2
class Licznik
{
public void ustaw(int i)
{
m_nLiczba = i;
}
public int wez()
{
return m_nLiczba;
}
public void dodaj()
{
m_nLiczba++;
}
}// koniec definicji klasy wewnętrznej Licznik
}// koniec definicji klasy zewnętrznej Test2
Jak widać klasa wewnętrzna Licznik ma bezpośredni dostęp do prywatnego pola danych m_nLiczba klasy Test2.
Nowy obiekt klasy wewnętrznej tworzymy poprzez
użycie wyrażenia:
obiekt.new KlasaWewnetrzna(arg, arg, ...)
gdzie obiekt jest referencją do obiektu klasy zewnętrznej (w tym
domyślnym odnośnikiem this), KlasaWewnetrzna jest nazwa konstruktora klasy wewnętrznej z
odpowiednimi argumentami.
W punkcie 2.6.3.4 "Obsługa zdarzeń w klasie
wewnętrznej", pokazano przykład
użycia klasy wewnętrznej do obsługi zdarzeń.
Klasa anonimowa jest to klasa bez nazwy
i konstruktora, definiowana za pomocą wyrażenia postaci:
new NazwaNadKlasy(arg, arg, ...) Blok
gdzie: NazwaNadKlasy jest nazwą nadklasy definiowanej klasy anonimowej, arg są argumentami konstruktora nadklasy (zależnie od
podanych argumentów wołany jest odpowiedni konstruktor
nadklasy: super NazwaNadKlasy(arg,
arg, ...)), Blok jest blokiem instrukcji definiujących klasę
anonimową. W przykładzie pokazanym na poniższym listingu, nie
ma zdefiniowanej wprost nadklasy Licznik.
Przyjmuje się, że nadklasą jest klasa Object, a
definicja klasy anonimowej dostarcza implementacji metod klasy Licznik.
Przykład 2.12 Definicja klasy anonimowej
public class Test3
{
private int m_nLiczba = 0;
private Licznik licznik;
public static void main(String args[])
{
Test3 test = new Test3();
test.licznik.ustaw(test.m_nLiczba = 10);
test.licznik.dodaj();
PrintWriter stdout = new PrintWriter(System.out, true);
stdout.println("Licznik = "+test.licznik.wez());
}
Test3()
{
this.licznik = new Licznik()
{
public void ustaw(int i)
{ m_nLiczba = i; }
public int wez()
{ return m_nLiczba; }
public void dodaj()
{ m_nLiczba++; }
};
}
}
W przypadku, gdy klasa anonimowa jest podklasą
klasy wewnętrznej definicja klasy anonimowej przyjmuje postać:
obiekt.new NazwaNadKlasy(arg, arg, ...) Blok
gdzie obiekt
jest obiektem klasy zewnętrznej zawierającej definicję
redefiniowanej anonimowo klasy wewnętrznej. W tym przypadku
wołany jest konstruktor nadklasy:
obiekt.super NazwaNadKlasy(arg, arg, ...)).
Deklarowanie klasy anonimowej klasy
wewnętrznej ilustruje poniższy przykład.
Przykład 2.13 Redefinicja anonimowa klasy wewnętrznej
public class Test4
{
private static int m_nLiczba = 0;
Liczydlo.Licznik licznik;
public static void main(String args[])
{
Liczydlo liczydlo = new Liczydlo();
// W poniższej klasie anonimowej redefiniowana jest tylko
// jedna metoda: wez(), inne metody wewnętrznej klasy Licznik,
// klasy Liczydlo pozostają bez zmian
Test4 test = new Test4(liczydlo.new Licznik()
{
public int wez()
{ return m_nLiczba + 100; }
} );
test.licznik.ustaw(test.m_nLiczba = 10);
test.licznik.dodaj();
PrintWriter stdout = new PrintWriter(System.out, true);
stdout.println("Licznik = "+test.licznik.wez());
}
Test4(Liczydlo.Licznik licznik)
{
this.licznik = licznik;
}
}
// Klasa zewnętrzna
class Liczydlo
{
// klasa wewnętrzna Licznik
class Licznik
{
private int m_nWartosc = 0;
public void ustaw(int i)
{ m_nWartosc = i; }
public int wez()
{ return m_nWartosc; }
public void dodaj()
{ m_nWartosc++; }
}
}
Wykonanie aplikacji Test4 spowoduje
wyprowadzenie na ekran tekstu: Licznik = 110.
W przypadku, gdy klasa anonimowa definiuje
interfejs, to klasa anonimowa staje się podklasą klasy Object
implementującą interfejs (po słowie kluczowym new
występuje nazwa interfejsu). Przykład użycia klas anonimowych
do implementacji interfejsu znajduje się w punkcie
2.6.3.6.
Klasy lokalne to klasy zdefiniowane w
bloku programu Javy. Klasa taka może odwoływać się do
wszystkich zmiennych widocznych w miejscu wystąpienia jej
definicji. Klasa lokalna jest widoczna, i może zostać użyta,
tylko w bloku w którym została zdefiniowana.
Przykład 2.14 Definicja klasy lokalnej
class Test4
{
void test()
{
// definicja klasy lokalnej
class KlasaLokalna
{ }
// deklaracja obiektu typu: KlasaLokalna
KlasaLokalna l = new KlasaLokalna();
}
}
Definiując nową klasę możemy, ale nie
musimy zadeklarować konstruktor, będący metodą o nazwie
identycznej, jak nazwa klasy. Konstruktor zostaje wywołany
podczas tworzenia nowego obiektu klasy. Każda klasa może
posiadać wiele konstruktorów, różniących się listą
argumentów. Ponieważ każda klasa w Javie dziedziczy (dziedziczenie
omówiono w punkcie 2.3.12) z klasy Object,
posiada też konstruktor bezparametrowy odziedziczony z tej
klasy.
Dla przykładu, w deklaracji klasy Punkt:
public class Punkt
{
public int m_nX;
public int m_nY;
}
nie ma definicji konstruktora, jednak
deklaracja zmiennej pkt postaci:
Punkt pkt = new Punkt();
jest poprawna, ponieważ istnieje domyślny
konstruktor bezparametrowy Punkt(),
który tworzy nowy obiekt klasy Punkt.
Jeżeli jednak chcemy zainicjować pola danych
przykładowej klasy powinniśmy zadeklarować konstruktor dla tej
klasy:
public class Punkt
{
public int m_nX;
public int m_nY;
public Punkt()
{
m_nX=10;
m_nY=10;
}
public Punkt(int X, int Y)
{
m_nX=X;
m_nY=Y;
}
}
W przykładzie tym mamy zadeklarowane dwa
konstruktory: konstruktor bezparametrowy Punkt(),
który inicjalizuje pola danych klasy zawsze w ten sam sposób
oraz konstruktor z dwoma parametrami Punkt(int X ,int Y), który inicjalizuje pola danych na podstawie wartości
argumentów X,Y.
Więcej o konstruktorach napisano w następnym
punkcie, natomiast zasady dziedziczenia konstruktorów (użycie
słowa kluczowego super) omówiono w
punkcie 2.3.12 poświęconym
dziedziczeniu.
Słowo kluczowe this oznacza
referencję do "samego siebie", czyli do obiektu, przez
który został wywołany. W metodzie Gdzie()
wartością this jest referencja do obiektu, w kontekście
którego wywołano metodę. W poniższym przykładzie, pola
danych klasy X i Y mają taką samą nazwę, jak argumenty konstruktora Punkt(int X, int Y), aby
jednoznacznie zidentyfikować zmienne użyto słowa kluczowego this:
public class Punkt
{
public int X;
public int Y;
public Punkt(int X, int Y)
{
this.X=X;
this.Y=Y;
}
public Punkt()
{
this.X=10;
this.Y=10;
}
public Punkt Gdzie()
{
// Zwracana jest referencja do tego (this) obiektu
return this;
}
}
W drugim konstruktorze klasy Punkt(), użycie this jest nadmiarowe (byłby on tam i
tak użyty domyślnie), bowiem zmienne identyfikowane są
jednoznacznie.
Za pomocą this możemy także wywołać
w ciele jednego konstruktora inny konstruktor danej klasy,
dlatego definicja konstruktora Punkt()
może przybrać następującą postać:
public Punkt()
{
this(10,10);
}
Taka deklaracja spowoduje wywołanie przez
konstruktor Punkt() konstruktora Punkt(int X, int Y) z parametrami odpowiednio: X=10 i Y=10;
A oto kolejny przykład użycia this w
procesie definiowania konstruktorów:
class Miejsce
{
//Przy definicji pól danych X i Y odstąpiono od przyjętej
// konwencji nazewnictwa m_xNazwaZm by uwidocznić zasady
// użycia this.
public int X;
public int Y;
String m_Nazwa;
public Miejsce(int X, int Y, String nazwa)
{
// gdyby argumenty i pola danych miały różne nazwy
// użycie this nie było by wymagane,
this.X=X;
this.Y=Y;
//tak jest dla pola m_Nazwa jednoznacznie identyfikowanego,
//jako pole danych klasy
m_Nazwa=nazwa;
}
public Miejsce(int X, int Y)
{
this(X,Y,"Miejsce Bez Nazwy");
}
public Miejsce()
{
this(10,10,"Miejsce Bez Nazwy");
}
public Miejsce PodajMiejsce()
{
return this;
}
}
Wywołanie w jednym konstruktorze innego
konstruktora danej klasy musi być pierwszą instrukcją tegoż
konstruktora.
Konstruktor kopiujący w Javie nie zajmuje
takiej pozycji jak w C++. Java nigdy nie używa konstruktora
kopiującego automatycznie, co nie oznacza, że konstruktor
kopiujący w Javie nie jest w pełni użyteczny. Dla dwu
istniejących obiektów: start, koniec typu Miejsce, wykonanie operacji:
koniec = start;
nie spowoduje utworzenia nowego obiektu koniec o wartościach identycznych, jak obiekt start, tylko skopiowanie referencji do obiektu start. Aby utworzyć nowy obiekt, należy użyć konstruktora
kopiującego w następujący sposób:
koniec = new Miejsce(start);
Definicja konstruktora kopiującego dla klasy Miejsce przyjmuje postać:
Miejsce(Miejsce wzorzec)
{
X = wzorzec.X;
Y = wzorzec.Y;
nazwa = new String(wzorzec.nazwa);
}
W konstruktorze tym, przy inicjalizacji pola nazwa utworzony został nowy obiekt typu String o
wartości równej polu nazwa
kopiowanego obiektu. Gdyby użyć następującej konstrukcji:
nazwa = wzorzec.nazwa;
to, w utworzonym nowym obiekcie, referencje do
pola nazwa posiadałyby dwa obiekty: kopiujący i kopiowany.
Konstruktor kopiujący przy tworzeniu nowego obiektu używa
obiektu źródłowego (kopiowanego) do uzupełnienia informacji
potrzebnych do inicjalizacji obiektu. Sposób użycia
konstruktora kopiującego w programie przedstawia poniższy
przykład:
public void JakasFunkcja(Miejsce mplac)
{
Miejsce mdzialka = new Miejsce(mplac);
}
Konstruktory służą do inicjalizacji pól
danych obiektu w momencie jego tworzenia. Jednak dane statyczne
istnieją nawet wtedy, gdy nie ma żadnego obiektu danej klasy -
są one atrybutami klasy. W celu umożliwienia inicjalizacji
zmiennych statycznych w Javie zdefiniowano inicjator
statycznych pól danych.
Przykład 2.15 Inicjator statycznych pól danych
class CBRadio
{
static final byte m_nLiczbaKanalow = 90;
static Kanal kanaly[] = new Kanal[m_nLiczbaKanalow];
// inicjalizacja pól statycznych klasy
static
{
for (byte i = 0; i < m_nLiczbaKanalow; i++)
{
kanaly[i] = new Kanal();
}
}
// definicje pozostałych pól danych i metod klasy...
}
Jak widać, klasa CBRadio
posiada zmienną statyczną kanaly,
która jest tablicą (tablice
omówiono w rozdziale 2.3.17) kanałów
CB radia. Za każdym razem, gdy tworzony jest nowy obiekt typu CBRadio, konstruktor przydziela nieużywany dotychczas kanał
radiowy. Oznacza to, że statyczna tablica kanałów musi być
zainicjowana zanim pierwszy z obiektów typu CDRadio zostanie utworzony. Sposób użycia inicjatora
statycznego został pokazany na powyższym listingu.
Inicjalizacja następuje wtedy, gdy klasa
jest pierwszy raz ładowana do pamięci.
Każda klasa może zawierać dowolną liczbę
inicjatorów statycznych. Inicjatory statyczne wykonywane są w
kolejności ich wystąpienia w definicji klasy.
Java umożliwia dziedziczenie pól i
metod z jednej klasy (tzw. nadklasy - ang. superclass) przez
inną klasę, tzw. podklasę (ang. subclass), ma ona te same pola
i metody, co nadklasa oraz te pola i metody, które są w niej
zdefiniowane. Można zatem powiedzieć, że podklasa jest
uszczegółowieniem nadklasy.

Rysunek 2-1
Dziedziczenie
Relację dziedziczenia między nadklasą i
podklasą wyrażamy za pomocą frazy ze słowem extends.
Zadeklarujmy klasę Miejsce w inny, niż poprzednio sposób. Będzie ona podklasą,
która oprócz pól i metod dziedziczonych z nadklasy Punkt posiada nowe pole m_Nazwa
opisujące nazwę miejsca:
class Miejsce extends Punkt
{
String m_Nazwa;
public Miejsce(int X, int Y, String nazwa)
{
//wywołanie konstruktora Punkt(X,Y) z nadklasy Punkt
super(X,Y);
m_Nazwa=nazwa;
}
public Miejsce(int X, int Y)
{
this(X,Y,"Miejsce Bez Nazwy");
}
public Miejsce()
{
this(10,10,"Miejsce Bez Nazwy");
}
public Miejsce(String nazwa)
{
m_Nazwa=nazwa;
}
}
Uwaga:
Pola danych i metody z modyfikatorem private
nie są dziedziczone.
Do pól i metod nadklasy odwołujemy się za
pomocą słowa kluczowego super. W podklasie,
słowo kluczowe super reprezentuje wtedy nazwę nadklasy.
Dzięki temu możemy odwoływać się do składników nadklasy przesłoniętych
(ang. shadow) (pól danych i metod na nowo zdefiniowanych w
podklasie) przy dziedziczeniu.
Jak widać, w konstruktorze Miejsce(X, Y, nazwa) użyto instrukcji super(X,Y).
Takie użycie powoduje wywołanie konstruktora: Punkt(X,Y) klasy Punkt z której dziedziczy klasa Miejsce.
Następnie, konstruktor klasy Miejsce
inicjuje pole danych m_Nazwa, które nie jest polem dziedziczonym z klasy Punkt. Gdy nie ma wywołania konstruktora nadklasy, Java
domyślnie przyjmuje wywołanie super()
konstruktor bezparametrowy nadklasy, jeśli taki konstruktor nie
istnieje, to sygnalizowany jest błąd. Przykładem konstruktora,
w którym nie jest wywołany explicite konstruktor nadklasy, jest
Miejsce(nazwa). Zainicjalizowano w nim jedynie pole m_Nazwa, natomiast pola X i Y
dziedziczone z klasy Punkt są inicjalizowane przez domyślne wywołanie
konstruktora nadklasy ( super(); ).
Dla pokazania zasad dziedziczenia w Javie
zdefiniujmy metodę o nazwie FunkcjaTest:
public FunkcjaTest()
{
Miejsce polozenie = new Miejsce();
System.out.println(polozenie.Gdzie().getClass().getName());
System.out.println(polozenie.Gdzie().getClass().getSuperclass().getName());
}
Po wykonaniu tej metody na ekranie otrzymujemy:
Miejsce
Punkt
Obiekt polozenie
klasy Miejsce wywołuje metodę Gdzie()
dziedziczoną z klasy Punkt, której wartością jest referencja do wołającego
ją obiektu. Następnie, dla tej referencji wykonywana jest
metoda getClass() dziedziczona z klasy Object (z
tej klasy dziedziczą domyślnie wszystkie klasy w Javie),
której wartością jest referencja do obiektu typu Class reprezentującego klasę obiektu w czasie wykonania
(ang. run-time) programu. Dla tego obiektu typu Class wykonywana jest metoda getName()
której wartością jest nazwa obiektu. W trzeciej linii
definicji ciała tej metody uzyskujemy informację o nadklasie
(ang. superclass), z której dziedziczy nasz obiekt.

Rysunek 2-2 Schemat dziedziczenia w Javie
Jak widać na rysunku, wszystkie klasy w Javie
dziedziczą z klasy Object. Klasa może dziedziczyć tylko z jednej nadklasy.
Natomiast każda klasa może implementować kilka interfejsów.
Zasady deklarowania i implementacji interfejsów omówiono w punkcie 2.3.15.
Przy dziedziczeniu występuje przesłanianie
pól i metod ponownie zdefiniowanych w klasie potomnej. Do
zdeklarowanej wcześniej klasy Punkt
dodajmy metodę Zeruj:
class Punkt
{
public int X;
public int Y;
// ...tu definicje konstruktorów klasy...
// i nowa metoda Zeruj
public void Zeruj()
{
X=0;
Y=0;
System.out.println("Punkt wyzerowany");
}
}
Do klasy Miejsce,
która jest rozszerzeniem klasy Punkt także
dodajmy metodę Zeruj(), która jest redefinicją (ang. overriding) metody z
nadklasy. W ciele metody następuje wywołanie metody Zeruj() z nadklasy przy użyciu słowa kluczowego super:
super.Zeruj().
class Miejsce extends Punkt
{
String m_Nazwa;
// ...tu definicje konstruktorów klasy...
// i nowa metoda Zeruj
public void Zeruj()
{
System.out.println("Zerowanie miejsca");
super.Zeruj();
m_Nazwa="";
System.out.println("Miejsce wyzerowane");
}
}
Dla sprawdzenia działania klasy Punkt i Miejsce zdefiniujmy klasę TestMiejsca:
public class TestMiejsca
{
Punkt gdzie = new Punkt();
Miejsce polozenie = new Miejsce();
Test()
{
System.out.println("Test dziedzicznia:");
gdzie.Zeruj();
System.out.println("...");
polozenie.Zeruj();
System.out.println("......");
gdzie=polozenie;
gdzie.Zeruj();
}
}
Po wykonaniu metody Test() klasy
TestMiejsca na ekranie otrzymujemy:
Test dziedziczenia:
Punkt wyzerowany
...
Zerowanie miejsca
Punkt wyzerowany
Miejsce wyzerowane
......
Zerowanie miejsca
Punkt wyzerowany
Miejsce wyzerowane
Dla obiektu typu Miejsce
wykonywana jest metoda Zeruj() z
nadklasy. Można zauważyć też właściwość, że w polach
będących referencjami do obiektów dowolnej nadklasy, można
przechowywać referencję do obiektów dowolnej klasy
dziedziczącej po danej nadklasie. Nie jest dopuszczalna
natomiast sytuacja odwrotna, tzn. nie można przyporządkować
np. zmiennej obiektowej typu Miejsce
referencji do obiektu typu Punkt.
Odwołanie typu super.super.NazwaMetody() nie jest poprawne. Aby uzyskać dostęp do metody lub
pola należącego do nie bezpośredniej nadklasy, należy
przeprowadzić konwersję odnośnika this.
class Raz
{
String m_SNazwa= "Raz";
String s()
{
return "1";
}
}
class Dwa extends Raz
{
String m_SNazwa= "Dwa";
String s()
{
return "2";
}
}
class Trzy extends Dwa
{
String m_SNazwa= "Trzy";
String s()
{
return "3";
}
void test()
{
java.io.PrintWriter stdout =new java.io.PrintWriter(System.out,true);
stdout.println("s()=\t\t\t"+s());
stdout.println("m_SNazwa=\t\t"+m_SNazwa);
stdout.println("...");
stdout.println("super.s()=\t\t"+super.s());
stdout.println("super.m_SNazwa=\t\t"+super.m_SNazwa);
stdout.println("......");
stdout.println("((Dwa)this).s()=\t"+((Dwa)this).s());
stdout.println("((Dwa)this).m_SNazwa=\t"+((Dwa)this).m_SNazwa);
stdout.println(".........");
stdout.println("((Raz)this).s()=\t"+((Raz)this).s());
stdout.println("((Raz)this).m_SNazwa=\t"+((Raz)this).m_SNazwa);
}
}
Sytuację zilustrujemy przykładem definiując
klasę SuperTest. Metoda pauza(), jest metodą pomocniczą, która służy do
zatrzymania wyniku wykonania aplikacji na ekranie, aż do
naciśnięcia klawisza Enter. Przy deklaracji metod main() i pauza() użyto frazy throws Exception, służącej do
obsługi wyjątków, jakie mogą wystąpić przy czytaniu znaku z
wejścia (System.in.read()). Omówienie obsługi wyjątków i użytej tu
konstrukcji znajduje się w rozdziale poświęconym wyjątkom.
public class SuperTest
{
public static void main(String[] args) throws Exception
{
//Tworzymy nowy obiekt trzy typu Trzy
Trzy trzy = new Trzy();
//Wywołanie metody test()
trzy.test();
//Zatrzymanie aplikacji do czasu nacisnięcia klawisza Enter
pauza();
}
static void pauza() throws Exception
{
System.out.print("Nacisnij Enter.....");
System.in.read();
}
}

Ilustracja 2-1 Wynik wykonania aplikacji SuperTest.
Dla pól danych użycie słowa kluczowego super
lub konwersji do klasy Raz lub Dwa
powoduje wypisanie na ekranie wartości pola m_SNazwa z odpowiedniej klasy. Natomiast dla metod, konwersja
typu ((Dwa)this).s() jest równoważna wywołaniu this.s().
Dzieje się tak dlatego, że w Javie każda metoda, która nie
jest statyczna (static) lub prywatna (private) jest
wirtualna (ang. virtual). Dlatego wywołanie metody s()
klasy Raz: (((Raz)this).s()) jest przekształcane w wywołanie przedefiniowującej
ją metody s() z klasy Trzy.
Java nie wymaga definiowania destruktorów.
Jest tak dlatego, że istnieje mechanizm automatycznego
zarządzania pamięcią (ang. garbage collection). Obiekt
istnieje w pamięci tak długo, jak długo istnieje do niego
jakakolwiek referencja w programie, w tym sensie, że gdy
referencja do obiektu nie jest już przechowywana przez żadną
zmienną obiekt jest automatycznie usuwany a zajmowana przez
niego pamięć zwalniana.
Ponieważ zarządzanie pamięcią jest w Javie
zautomatyzowane, nie ma potrzeby definiowania destruktorów. Mamy
jednak możliwość deklaracji specjalnej metody finalize,
która będzie wykonywana przed usunięciem obiektu z pamięci.
Deklaracja takiej metody ma zastosowanie, gdy nasz obiekt np.: ma
referencje do urządzeń wejścia-wyjścia i przed usunięciem
obiektu należy je zamknąć.
Proces zbierania nieużytków jest włączany
okresowo, uwalniając pamięć zajmowaną przez obiekty, które
nie są już potrzebne. W czasie działania programu przeglądany
jest obszar pamięci przydzielanej dynamicznie, zaznaczane są
obiekty, do których istnieją referencje. Po prześledzeniu
wszystkich możliwych ścieżek referencji do obiektów, te
obiekty, które nie są zaznaczone (tzn. do których nie ma
referencji) zostają usunięte.
Mechanizm oczyszczania pamięci z nieużytków
działa w wątku o niskim priorytecie synchronicznie lub
asynchronicznie, zależnie od sytuacji i środowiska systemu
operacyjnego na którym wykonywany jest program w Javie.
Program w Javie może jawnie uruchomić
mechanizm zbierania nieużytków poprzez wywołanie metody System.gc(). Wywołanie mechanizmu czyszczenia pamięci nie
gwarantuje tego, że obiekt zostanie usunięty. W systemach,
które pozwalają środowisku przetwarzania Javy sprawdzać,
kiedy wątek się rozpoczął i przerwał wykonanie innego wątku
(takich jak np. Windows 95/NT), mechanizm czyszczenia pamięci
działa asynchronicznie w czasie bezczynności systemu.
Mechanizm czyszczenia pamięci umożliwia
obiektowi przed usunięciem "posprzątanie po sobie"
poprzez wywołanie metody finalize. Proces ten nazywany
"finalizacją". Podczas finalizacji obiekt może
zwolnić zasoby systemowe takie, jak pliki i gniazdka (ang.
sockets) lub referencje do innych obiektów. Metoda finalize
jest zdefiniowana w klasie java.lang.Object. Definiowana
klasa musi redefiniować metodę finalize aby umożliwić
finalizację dla zasobów używanych przez obiekty tego typu.
Załóżmy, że mamy klasę, która otwiera
plik wtedy, gdy jest tworzony obiekt tej klasy:
class OtwórzPlik
{
FileInputStream m_plik = null;
OtwórzPlik(String nazwaPliku)
{
//Otwarcie pliku
try
{ m_plik = new FileInputStream(nazwaPliku); }
//Obsługa wyjątku
catch (java.io.FileNotFoundException e)
{ System.err.println("Nie moge otworzyc pliku" + nazwaPliku);}
}
}
Definiując klasę powinniśmy zadbać, aby
wszystkie otwarte pliki, zostały zamknięte przed zakończeniem
istnienia obiektu tej klasy. Zdefiniujmy więc metodę finalize
dla klasy OtwórzPlik:
protected void finalize () throws Throwable
{
if (m_plik != null)
{
m_plik.close();
m_plik = null;
}
}
Fraza throws Throwable oraz blok try{...}
catch(...){...} związane są z obsługą sytuacji
wyjątkowych jakie mogą wystąpić np. podczas wykonywania
operacji na plikach. Wyjątki
omówiono w punkcie 2.4.2 .
Jeśli nadklasa danej klasy ma zadeklarowaną
metodę finalize, to dana klasa powinna wywoływać
metodę finalize z nadklasy, aby ta uporządkowała zasoby
systemowe, które rezerwowane są w jej definicji, np.:
protected void finalize() throws Throwable
{
. . .
// tutaj kod czyszczący dla zasobów naszej klasy
. . .
// wywołanie metody finalize() z nadklasy:
super.finalize();
}
Niekiedy definiujemy klasę reprezentującą
jakąś abstrakcyjną koncepcję, opisującą pewne własności
wspólne dla reprezentowanej abstrakcji. Przykładem takiej
koncepcji abstrakcyjnej niech będzie pojęcie: "ptak".
W świecie rzeczywistym nie spotkamy obiektu typu ptak. Istnieją
za to obiekty typu wróbel, sokół, jemiołuszka i inne. Ptak
reprezentuje zatem pojęcie abstrakcyjne.

Rysunek 2-3 Klasa abstrakcyjna i klasy potomne.
Definicja klasy abstrakcyjnej zawiera,
definicję niektórych metod (z modyfikatorem abstract)
zostawiając jednak ich implementację dla klasach potomnych. Dla
klas abstrakcyjnych nie mamy możliwości bezpośredniego
tworzenia obiektów danej klasy.
W przykładzie zdefiniujemy klasę
abstrakcyjną Figura z deklaracją dwu metod abstrakcyjnych Rysuj() i Pole(). Metody te zadeklarowane są jako abstrakcyjne,
ponieważ rysowanie jak i obliczenie pola dla każdej figury
(np.: Koło, Kwadrat, Trójkąt) wymaga odrębnej implementacji.
abstract class Figura
{
private int m_nwspX;
private int m_nwspY;
Figura()
{
m_nwspX = 10;
m_nwspY = 10;
}
void Przesun(int dx, int dy)
{
m_nwspX =m_nwspX + dx;
m_nwspY =m_nwspY + dy;
}
abstract void Rysuj();
abstract float Pole();
}
Klasa Kwadrat,
która dziedziczy z nadklasy Figura,
dostarcza implementacji dla dwu metod abstrakcyjnych Rysuj() i Pole() zadeklarowanych w klasie abstrakcyjnej Figura.
class Kwadrat extends Figura
{
private byte m_bDlugoscBoku;
void Rysuj()
{
//instrukcje rysujące kwadrat
}
float Pole()
{
return m_bDlugoscBoku * m_bDlugoscBoku;
}
}
Nie jest wymagane, aby klasa abstrakcyjna
zawierała metody abstrakcyjne. Jednakże każda klasa, która ma
metodę abstrakcyjną, lub która nie implementuje metod
abstrakcyjnych dziedziczonych z nadklasy, musi być zadeklarowana
jako klas abstrakcyjna.
W Javie istnieje podobna do klas koncepcja interfejsów.
Interfejsy są kolekcją metod abstrakcyjnych. Interfejs może
być publiczny lub prywatny. Wszystkie metody w interfejsie są
publiczne i abstrakcyjne. Jeśli istnieją w interfejsie pola
danych, to są one domyślnie publiczne, finalne i statyczne
(ang. public, final i static) co oznacza,
że są stałymi.
Składnia definicji interfejsu:
[modyfikator] interface NazwaInterfejsu [extends listaInterfejsów]
{
. . .
}
Interfejs może dziedziczyć z innych
interfejsów, ale nie może dziedziczyć z klas.
Interfejs opisuje zbiór właściwości, które
klasa musi implementować.
Zadeklarujmy interfejs Kolekcja,
składający się z jednej stałej i trzech metod:
interface Kolekcja
{
int MAXIMUM = 200;
void dodaj(Object obj);
Object znajdz(Object obj);
int liczbaObiektow();
}
Interfejs Kolekcja
może być zaimplementowany np. przez klasy reprezentujące
kolekcję innych obiektów, takich jak sterty, wektory, listy i
inne.
Relację dziedziczenia pomiędzy klasą a
interfejsem wyrażamy za pomocą frazy ze słowem implements.
Każda klasa, która implementuje interfejs, musi posiadać
definicję wszystkich metod zadeklarowanych w interfejsie. Jeśli
nie wszystkie metody będą zadeklarowane w klasie to klasa taka
będzie klasą abstrakcyjną.
Przykład 2.16 Definicja klasy Wektor
implementującej interfejs Kolekcja
class Wektor implements Kolekcja
{
private Object obiekty[] = new Object[MAXIMUM];
private short m_sLicznik = 0;
public void dodaj(Object obj)
{
obiekty[m_sLicznik++]=obj;
}
public Object znajdz(Object obj)
{
for (int i = 0; i<m_sLicznik;i++)
{
if (obiekty[i].getClass() == obj.getClass())
{
System.out.println("Znaleziono obiekt klasy "
+ obj.getClass() );
return obiekty[i];
}
}
return null;
}
public int liczbaObiektow()
{
return m_sLicznik;
}
}
Podczas definiowania metod z interfejsu Kolekcja musimy pamiętać, aby miały one modyfikator public,
który jest domyślny dla wszystkich metod zadeklarowanych w
interfejsie. Dla sprawdzenia klasy Wektor zdefiniowana została
klasa TestWektora.
Przykład 2.17 Definicja klasy TestWektora
class TestWektora
{
public static void main(String[] args) throws Exception
{
Wektor wektor = new Wektor();
wektor.dodaj(new String("str"));
wektor.dodaj(new Integer(5));
System.out.println("Liczba ob:"+wektor.liczbaObiektow());
System.out.println(wektor.znajdz(new String()));
System.out.println(((Integer)(wektor.znajdz(new Integer(0)))).intValue());
pauza();
}
static void pauza() throws Exception
{
System.out.print("Nacisnij Enter.....");
System.in.read();
}
}

Ilustracja 2-2 Rezultat wykonania programu TestWektora.
Dopuszcza się możliwość implementowania
przez jedną klasę wielu interfejsów (patrz
Rysunek 2-2 Schemat dziedziczenia w Javie). Dziedziczenie z wielu interfejsów umożliwia
zaimplementowanie w Javie mechanizmu podobnego do
wielodziedziczenia (którego w Javie nie ma). Choć mechanizm
implementacji interfejsów rozwiązuje problem podobny do
wielodziedziczenia klas, nie jest to jednak wielodziedzicznie
ponieważ:
- nie można dziedziczyć zmiennych z
interfejsu;
- nie można dziedziczyć implementacji
metod;
- hierarchia interfejsu jest niezależna od
hierarchii klas; klasy które implementują ten sam
interfejs mogą zarówno być, jaki i nie być powiązane
przez hierarchię klas, co nie jest prawdą w przypadku
wielodziedziczenia.
Interfejsy mogą być użyte jako typy
zmiennych występujących w klasie. Przykładowo zadeklarujmy
klasę:
class SuperKolekcja
{
private Kolekcja zbior[];
//... deklaracje innych pól danych
public DodajKolekcje(Kolekcja ko, int numer)
{
//... implementacja metody ...
}
//...deklaracje innych metod klasy
}
W przykładzie tym zadeklarowana jest tablica zbior[], elementami której są obiekty typu interfejsowego Kolekcja podobnie, jak parametr ko metody DodajKolekcje(). Kolekcja jest interfejsem, co oznacza, że w obu tych
przypadkach każdy obiekt, który implementuje interfejs Kolekcja, niezależnie od tego, gdzie znajduje się w hierarchii
klas, może być przekazywany do metody DodajKolekcję() lub może być elementem tablicy zbior[].
Aby ułatwić pracę z klasami, uniknąć
konfliktów nazw wprowadzono w Javie pakiety (ang. packages).
Pakiety w Javie są pewnym podzbiorem biblioteki, zawierają
przeważnie funkcje związane tematycznie. Pakiety mogą także
zawierać definicje interfejsów.
Możemy tworzyć własne pakiety zawierające
definicje klas i interfejsów przy użyciu wyrażenia package.
Załóżmy, że implementujemy grupę klas
reprezentującą kolekcję obiektów graficznych takich, jak
kwadrat, koło, prostokąt, punkt i inne oraz inne klasy
służące do operacji na obiektach tych klas. Jeśli chcemy
udostępnić te klasy innym programistom, grupujemy je w pakiecie
o nazwie np. graf, z kolei pakiet ten jest częścią pakietu moje
zawierającym pakiety zdefiniowane przeze mnie.
Poszczególne klasy publiczne definiujemy w
pliku o nazwie:
NazwaKlasyPublicznej.java
oprócz definicji jednej klasy publicznej w
pliku tym mogą znajdować się definicje innych klas
niepublicznych. I tak, klasę Kwadrat
definiujemy w pliku Kwadrat.java:
package moje.graf;
public class Kwadrat
{
// definicja pól danych i metod klasy Kwadrat
}
// ... ew. definicje innych klas niepublicznych
Podobnie jest dla klasy Punkt -
definiujemy ją w pliku Punkt.java :
package moje.graf;
public class Punkt
{
// definicja pól danych i metod klasy Punkt
}
// ... ew. definicje innych klas niepublicznych
Po skompilowaniu, dla każdej klasy tworzone
są pliki z kodem pośrednim (kodem bajtowym) o nazwach:
NazwaKlasy.class
To, że klasa należy do pakietu, determinuje
także położenie pliku z kodem bajtowym klasy w strukturze
katalogów. Pliki zawierające klasy z pakietu moje.graf muszą znajdować się w podkatalogach moje\graf, a te katalogi powinny znajdować się w miejscu
zdefiniowanym przez zmienną systemową CLASSPATH, która
określa położenie plików z kodem bajtowym klas. Jeśli
zmienna ta przyjmuje wartość:
CLASSPATH = C:\Windows\java\classes;
to ostatecznie nasze pliki znajdą się w
katalogu:
C:\Windows\java\classes\moje\graf
Klasy należące do różnych pakietów mogą
mieć takie same nazwy ponieważ każdy pakiet tworzy swoją
"przestrzeń nazw".
Aby mieć dostęp do klas z danego pakietu
należy użyć słowa kluczowego import. Załóżmy, że
chcemy w programie użyć klas zdefiniowanych w pakiecie moje.graf, program przyjmuje wtedy postać:
import moje.graf;
class Cos
{
Punkt p = Punkt();
}
Deklaracji import nie musimy stosować,
wówczas w programie należy odwołać się bezpośrednio do
interesującej nas klasy, poprzez dostęp kropkowy:
class Cos2
{
moje.graf.Punkt p = moje.graf.Punkt();
}
Gdy chcemy mieć dostęp do wszystkich klas np.
z pakietu java.io, deklaracja import wygląda następująco:
import java.io.*;
Klasy w naszym programie mogą być importowane
nie tylko z dysku lokalnego ale mogą być ładowane także z
Internetu, wtedy nazwa pakietu zaczyna się od odwróconej nazwy
domeny.
Dla przykładu, załóżmy, że nasza domena ma
nazwę: webforce.com.pl. to w Internecie nasz pakiet będzie dostępny pod
nazwą:
pl.com.webforce.windows.java.classes.moje.graf
Więcej o standardowych pakietach Javy napisano
w rozdziale: Realizacja
funkcji bibliotecznych w Javie.
Oprócz pakietów standardowych, wiele firm
(np.: Microsoft, Sun, Novell, Borland, i innych) dostarcza swoje
pakiety klas rozszerzające standardową bibliotekę Javy.
Należy do nich np. pakiet firmy Sun: sun.net.ftp
implementujący protokół FTP.
Tablice w Javie, w odróżnieniu od tablic w
C++, są obiektami. Typ tablicowy jest podklasą klasy Object
i implementuje interfejs Cloneable. Dla każdej
nowoutworzonej tablicy Java tworzy odpowiadającą jej klasę
tablicową. Każdy obiekt tablicowy posiada pole length,
które zawiera informację o długości tablicy (jeśli tablica
została alokowana).
Deklaracja zmiennej będącej tablicą składa
się z dwu części: nazwy typu tablicy i nazwy tablicy. Typ
tablicy określa typ danych, jakie tablica będzie zawierała.
Przykładowo, deklaracja tablicy zawierającej elementy typu int
przyjmuje postać:
int tablicaInt[];
lub
int[] tablicaInt;
Deklaracja tablicy, podobnie jak deklaracja
innych obiektów, nie alokuje dla niej pamięci. Aby pamięć
została przydzielona dla danej tablicy, musimy utworzyć obiekt
typu tablicowego, używając operatora new:
// przydzielenie pamięci dla tablicy 10 elementów typu int
tablicaInt = new int[10];
Gdy deklarujemy tablicę dla typów
wewnętrznych (np. int, char, byte, itd.) możemy użyć
listy inicjalizującej:
int tablicaInt[] = {1, 2, 3, 4, 5};
Powyższe wyrażenie alokuje tablicę
składającą się z pięciu liczb typu int i przypisuje
im wartości od 1 do 5. Operatora new użyto domyślnie,
podobnie jak w instrukcji:
System.out.println("Witamy");
Deklaracja literału tekstowego "Witamy" powoduje alokację obiektu typu String i
przypisanie mu wartości początkowej "Witamy".
Tablicę możemy także zainicjować w
następujący sposób:
int tablicaInt[] = new int[100];
for (int i =0; i < 100; i++)
{
tablicaInt[i] = i+10;
}
Java sprawdza każde odwołanie do elementów
tablicy tablicaInt. Jeśli indeks i nie jest
liczbą z zakresu 0 do 100, to odwołanie tablicaInt[i]
powoduje wystąpienie wyjątku ArrayIndexOutOfBoundException.
Jeśli wystąpienie tego wyjątku zostanie obsłużone, program
może kontynuować swoje działanie, w przeciwnym razie jego
wykonanie zostanie przerwane, a na ekranie wyświetlona zostanie
informacja o miejscu wystąpienia błędu (wyjątki opisano w
rozdziale: Obsługa
sytuacji wyjątkowych w Javie.).
Uzupełnienie Javy z kwietnia 1997 dopuszcza
wystąpienie inicjatora tablicy nie tylko w deklaracji tablicy,
ale także przy tworzeniu obiektu typu tablicowego (słowo
kluczowe new) gdy nie określimy liczby elementów
tworzonej tablicy:
int tablicaInt[];
tablicaInt = new int[] {1, 2, 3, 4, 5};
co jest równoważne:
int tablicaInt[] = {1, 2, 3, 4, 5};
Przy tworzeniu tablicy, której elementy są
typu obiektowego, deklaracja:
MojaKlasa tablicaMK[] = new MojaKlas[10];
powoduje utworzenie tablicy zawierającej 10 referencji
do obiektów MojaKlasa, wszystkie referencje mają wartość null.
Aby mieć możliwość odwołania się do
elementów tej tablicy, musimy zainicjalizować jej elementy
(obiekty) w pętli np.:
MojaKlasa tablicaMK[] = new MojaKlas[10]; // alokacja 10 referencji
for (int i = 0; i < 10; i++ )
{
tablicaMK[i] = new MojaKlasa(); // przypisanie obiektów
}
W Javie tablice wielowymiarowe można
zrealizować tworząc tablice, których elementami są tablice:
MojaKlasa multiTabMK[][] = new MojaKlasa[5][];
Powyższa deklaracja tworzy pięcio-elementową
tablicę, której elementami są tablice obiektów typu MojaKlasa. Każda z tablic obiektów MojaKlasa
musi być oczywiście zainicjowana.
Przykład 2.18 Inicjalizacja wielowymiarowych tablic
obiektów
MojaKlasa multiTabMK[][] = new MojaKlasa[5][];
for(int i = 0; i < 5; i++)
{
// utworzenie nowej tablicy jednowymiarowej
// (wiersza tablicy dwu-wymiarowej)
multiTabMK[i] = new MojaKlasa[1+i];
for(int j = 0; j < 1+i; j++)
{
// inicjalizacja elementów tablicy
multiTabMK[i][j] = new MojaKlasa();
}
}
W powyższym przykładzie utworzona zostaje
pięcio-elementowa tablica, której elementami są tablice
obiektów typu MojaKlasa. Jak widać tablice te mogą mieć różne rozmiary (tu
od 1 do 5). Tablice w Javie nie muszą być więc regularne
(ortogonalne).
Przykład
2.19 Aplikacja Tablice
Aplikacja Tablice
ilustruje użycie własności obiektowych tablic. Na ekranie
wypisywane są sygnatury tablicy MojaKlasa i
wszystkich tablic, jakie ona zawiera oraz nazwy obiektów
będących elementami poszczególnych tablic:
public class Tablice
{
public static void main(String args[])
{
MojaKlasa multiTabMK[][] = new MojaKlasa[5][];
for(int i = 0; i < 5; i++)
{
multiTabMK[i] = new MojaKlasa[1+i];
for(int j = 0; j < 1+i; j++)
{
multiTabMK[i][j] = new MojaKlasa();
}
}
System.out.println(multiTabMK.getClass().getName()
+" zawiera:");
for (int i = 0; i < multiTabMK.length; i++)
{
System.out.print(multiTabMK[i].getClass().getName()+" : ");
for(int j = 0; j < multiTabMK[i].length; j++)
System.out.print(multiTabMK[i][j].getClass().getName()+", ");
System.out.println("");
}
}
}
class MojaKlasa
{ }
Po skompilowaniu pliku Tablice.java
i wykonaniu aplikacji Tablice na ekranie otrzymamy następujący rezultat:

Ilustracja 2-3 Efekt wykonania aplikacji Tablice
Każda tablica ma sygnaturę (odpowiednik nazwy
klasy dla obiektów nie będących tablicami), zaczynającą się
od znaków [ w ilości odpowiadającej liczbie wymiarów tablicy,
dalej sygnatura składa się z litery "L" i nazwy klasy
(której obiekty tablica zawiera). Dla typów wewnętrznych ich
nazwy kodowane są przy użyciu jednej litery, odpowiednio: byte
B, char C, float F, double D, int I, long J, short S, boolean Z.
Przykładowe sygnatury tablic:
int[] sygnatura: [I
String[] [Ljava.lang.String;
MojaKlasa[] [] [[LMojaKlasa;
Należy pamiętać, iż tablice w Javie są
indeksowane od 0. Oznacza to, że gdy utworzymy tablicę 10
elementową, to możemy odwoływać się jedynie do elementów o
indeksach od 0 do 9.
spis treści
|