Paradygmat programowania zorientowanego obiektowo w Javie

Klasa jako typ danych

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:

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:

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 klasy

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:

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 klasy

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)
{ ... }

Modyfikatory klas, metod i pól

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 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.

Statyczne pola danych i metody

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.

Obiekty

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:

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

Klasy wewnętrzne, anonimowe i lokalne

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();
	}
}

Konstruktory

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

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

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);
}

Inicjator statycznych pól danych

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.

Dziedziczenie, słowo kluczowe super

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.

Usuwanie obiektów w Javie

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();

}

Klasy abstrakcyjne

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.

Interfejsy

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ż:

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[].

Pakiety

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

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.