Przewiń do głównej treści
  1. Java - Tutorial Programowania (1990s)/

4. Programowanie obiektowe

·6539 słów·31 min·

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:

  • 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](../#Modyfikatory klas, metod i pól/) 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](../#Metody klasy/)) 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:

  • 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 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](../#Modyfikatory klas, metod i pól/) 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 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:

Modyfikatorklasapodklasapakietwszędzie
privateX
protectedXXX
publicXXXX
packageXX

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 dla metod oznacza, że nie może zostać ona przedefiniowana w klasach pochodnych. Podobnie modyfikator final dla klasy oznacza, że klasa nie może służyć jako nadklasa dla innych klas.

Pole danych z modyfikatorem final może być zainicjalizowane tylko raz podczas deklaracji. Oznacza to, że po inicjalizacji wartość takiej zmiennej nie może zostać zmieniona. Jest to odpowiednik stałych z języka C/C++.

transient

Pole danych z modyfikatorem transient oznacza, że nie będzie ono poddane tzw. serializacji (ang. serialization) (patrz rozdział Obsługa sytuacji wyjątkowych w Javie).

volatile

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 konkretnego obiektu tej klasy. W szczególności statyczne pola danych istnieją nawet wtedy, gdy nie ma żadnego obiektu danej klasy.

Przykład 2.5 Deklaracja klasy KontoOsobiste z polami i metodami statycznymi

class KontoOsobiste
{
	private double m_dSaldo; // pole danych "zwykłe"
	// pola statyczne (wspólne dla wszystkich obiektów klasy)
	private static double s_dOprocentowanie; // oprocentowanie dla wszystkich kont
	private static int s_nLiczbaKont; // liczba kont
	
	// metoda statyczna
	public static void UstawOprocentowanie(double dOprocentowanie)
	{
		s_dOprocentowanie = dOprocentowanie;
	}
	
	public static int PodajLiczbeKont()
	{
		return s_nLiczbaKont;
	}
	
	// konstruktor
	public KontoOsobiste(double dSaldo)
	{
		m_dSaldo = dSaldo;
		s_nLiczbaKont++; // zwiększamy liczbę kont
	}
	
	// zwykłe metody
	public double PodajSaldo()
	{
		return m_dSaldo;
	}
}

Odwołanie do statycznego pola danych może mieć postać: NazwaKlasy.NazwaPola a dla metod statycznych NazwaKlasy.NazwaMetody(argumenty).

Aby zmienić wartość oprocentowania na 20 procent dla wszystkich obiektów typu KontoOsobiste wystarczy, że wykonamy instrukcję:

KontoOsobiste.UstawOprocentowanie(20.0);

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.

class TestKonta
{
	public static void main(String args[])
	{
		// utworzenie kilku obiektów klasy KontoOsobiste
		KontoOsobiste konto1 = new KontoOsobiste(1000.0);
		KontoOsobiste konto2 = new KontoOsobiste(2000.0);
		KontoOsobiste konto3 = new KontoOsobiste(3000.0);
		
		// ustawienie oprocentowania dla wszystkich kont
		KontoOsobiste.UstawOprocentowanie(15.0);
		
		// sprawdzenie liczby kont
		System.out.println("Liczba kont: " + KontoOsobiste.PodajLiczbeKont());
		
		// można także wywołać metodę statyczną dla konkretnego obiektu
		System.out.println("Liczba kont: " + konto1.PodajLiczbeKont());
	}
}

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 i static jednocześnie otrzymując w ten sposób stałą klasy.

Statyczne pola danych mogą być inicjalizowane. Inicjatory statycznych pól danych omówiono w [punkcie 2.3.11](../#Inicjator statycznych pól danych/).

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:

NazwaKlasy nazwaZmiennej;

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 dzisiaj = new Date();

W wyrażeniu tym deklarowana jest nowa zmienna obiektowa dzisiaj typu Date, następnie zostaje jej przypisany utworzony za pomocą instrukcji new Date() nowy obiekt klasy Date. Instrukcja new powoduje wywołanie konstruktora klasy Date, 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 data1 = new Date();
Date data2 = new Date(97, 3, 15);

Gdy zmienną obiektową

Date dzisiaj;

zadeklarujemy w postaci:

Date dzisiaj;

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:

dzisiaj = new Date();

Natomiast, gdy chcemy przypisać istniejący już obiekt:

dzisiaj = data1;

Wszystkie typy wewnętrzne mają swoje odpowiedniki obiektowe (np.: typ int - klasa Integer, typ char - klasa Character, typ boolean - klasa Boolean itd.). Dla obiektów tych klas dostępne są różne użyteczne metody, takie jak toString(), equals() i inne.

Przykład 2.7 Użycie zmiennych obiektowych odpowiadających zmiennym wewnętrznym

class TestObiektow
{
	public static void main(String args[])
	{
		Integer liczba1 = new Integer(10);
		Integer liczba2 = new Integer(20);
		
		System.out.println("liczba1 = " + liczba1.toString());
		System.out.println("liczba2 = " + liczba2.toString());
		
		if (liczba1.equals(liczba2))
			System.out.println("Liczby są równe");
		else
			System.out.println("Liczby są różne");
	}
}

Dostęp do atrybutów obiektu, reprezentowanych przez zmienną obiektową, realizujemy za pomocą wyrażeń kropkowych postaci:

nazwaObiektu.nazwaPola
nazwaObiektu.nazwaMetody(argumenty)

gdzie:

  • nazwaObiektu jest nazwą obiektu, w tym domyślną nazwą bieżącego obiektu: this,
  • nazwaPola jest nazwą pola danych lub nazwą metody.

Przykład 2.8 Dostęp do metod i pól danych klasy

Zdefiniujmy klasę:

class TestDostep
{
	private int m_nWartosc = 100;
	
	public int PodajWartosc()
	{
		return m_nWartosc;
	}
	
	public void UstawWartosc(int nWartosc)
	{
		m_nWartosc = nWartosc;
	}
	
	public void TestMetod()
	{
		// dostęp do pola danych
		System.out.println("m_nWartosc = " + m_nWartosc);
		
		// dostęp za pomocą metody
		System.out.println("Wartość = " + PodajWartosc());
		
		// dostęp za pomocą this
		System.out.println("Wartość = " + this.PodajWartosc());
	}
}

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 TestInicjator
{
	private int m_nLicznik;
	
	// inicjator obiektów
	{
		m_nLicznik = 100;
		System.out.println("Inicjator obiektów: m_nLicznik = " + m_nLicznik);
	}
	
	// konstruktor
	public TestInicjator()
	{
		System.out.println("Konstruktor: m_nLicznik = " + m_nLicznik);
	}
}

Gdy będziemy tworzyć obiekt klasy TestInicjator to wykonanie konstruktora spowoduje wyświetlenie na ekranie napisu:

Inicjator obiektów: m_nLicznik = 100
Konstruktor: m_nLicznik = 100

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: Licznik i Test. 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.

class Licznik
{
	private int m_nWartosc = 100;
	
	public int PodajWartosc()
	{
		return m_nWartosc;
	}
	
	public void ZwiekszWartosc(int nWartosc)
	{
		m_nWartosc += nWartosc;
	}
}

class Test
{
	public static void main(String args[])
	{
		Licznik licznik = new Licznik();
		licznik.ZwiekszWartosc(10);
		System.out.println("Licznik = " + licznik.PodajWartosc());
	}
}

Wykonanie metody main powoduje wyprowadzenie na ekran tekstu: Licznik = 110.

Przykład 2.10 Rozłączne definicje klas

Zadeklarujmy teraz klasę Licznik jako klasę wewnętrzną klasy Test.

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

import java.io.*;

class Test
{
	private int m_nWartoscKlasy = 200;
	
	// definicja klasy wewnętrznej
	class Licznik
	{
		private int m_nWartosc = 100;
		
		public int PodajWartosc()
		{
			return m_nWartosc;
		}
		
		public void ZwiekszWartosc(int nWartosc)
		{
			m_nWartosc += nWartosc;
			// klasa wewnętrzna ma dostęp do prywatnych pól klasy zewnętrznej
			m_nWartoscKlasy += nWartosc;
		}
		
		public int PodajWartoscKlasyZewnetrznej()
		{
			return m_nWartoscKlasy;
		}
	}
	
	public static void main(String args[])
	{
		Test test = new Test();
		// utworzenie obiektu klasy wewnętrznej
		Test.Licznik licznik = test.new Licznik();
		
		licznik.ZwiekszWartosc(10);
		System.out.println("Licznik = " + licznik.PodajWartosc());
		System.out.println("Wartość klasy zewnętrznej = " + licznik.PodajWartoscKlasyZewnetrznej());
		
		// użycie PrintWriter
		PrintWriter out = new PrintWriter(System.out, true);
		out.println("Licznik = " + licznik.PodajWartosc());
	}
}

Jak widać klasa wewnętrzna Licznik ma bezpośredni dostęp do prywatnego pola danych m_nWartoscKlasy klasy Test.

Nowy obiekt klasy wewnętrznej tworzymy poprzez użycie wyrażenia:

obiektKlasyZewnetrznej.new KonstruktorKlasyWewnetrznej(argumenty)

gdzie obiektKlasyZewnetrznej jest referencją do obiektu klasy zewnętrznej (w tym domyślnym odnośnikiem this), KonstruktorKlasyWewnetrznej 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(argumenty) { ciałoKlasy }

gdzie: NazwaNadklasy jest nazwą nadklasy definiowanej klasy anonimowej, argumenty są argumentami konstruktora nadklasy (zależnie od podanych argumentów wołany jest odpowiedni konstruktor nadklasy: NazwaNadklasy(), NazwaNadklasy(arg1), NazwaNadklasy(arg1, arg2) itd.), ciałoKlasy 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

class Test
{
	public static void main(String args[])
	{
		// definicja klasy anonimowej
		Object licznik = new Object()
		{
			private int m_nWartosc = 100;
			
			public int PodajWartosc()
			{
				return m_nWartosc;
			}
			
			public void ZwiekszWartosc(int nWartosc)
			{
				m_nWartosc += nWartosc;
			}
		};
		
		// użycie obiektu klasy anonimowej wymaga rzutowania
		System.out.println("Licznik = " + ((Licznik)licznik).PodajWartosc());
	}
}

W przypadku, gdy klasa anonimowa jest podklasą klasy wewnętrznej definicja klasy anonimowej przyjmuje postać:

obiektKlasyZewnetrznej.new NazwaKlasyWewnetrznej(argumenty) { ciałoKlasy }

gdzie obiektKlasyZewnetrznej jest obiektem klasy zewnętrznej zawierającej definicję redefiniowanej anonimowo klasy wewnętrznej. W tym przypadku wołany jest konstruktor nadklasy:

NazwaKlasyWewnetrznej(argumenty)

Deklarowanie klasy anonimowej klasy wewnętrznej ilustruje poniższy przykład.

Przykład 2.13 Redefinicja anonimowa klasy wewnętrznej

import java.io.*;

class Test
{
	private int m_nWartoscKlasy = 200;
	
	// definicja klasy wewnętrznej
	class Licznik
	{
		private int m_nWartosc = 100;
		
		public int PodajWartosc()
		{
			return m_nWartosc;
		}
		
		public void ZwiekszWartosc(int nWartosc)
		{
			m_nWartosc += nWartosc;
		}
	}
	
	public static void main(String args[])
	{
		Test test = new Test();
		
		// definicja anonimowa klasy wewnętrznej
		Licznik licznik = test.new Licznik()
		{
			// przedefiniowanie metody ZwiekszWartosc
			public void ZwiekszWartosc(int nWartosc)
			{
				super.ZwiekszWartosc(nWartosc);
				m_nWartoscKlasy += nWartosc; // dostęp do pola klasy zewnętrznej
			}
			
			public int PodajWartoscKlasyZewnetrznej()
			{
				return m_nWartoscKlasy;
			}
		};
		
		licznik.ZwiekszWartosc(10);
		System.out.println("Licznik = " + licznik.PodajWartosc());
	}
}

Wykonanie aplikacji Test 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 Test
{
	public void MetodaZKlasaLokalna()
	{
		final int nStalaLokalna = 50;
		
		// definicja klasy lokalnej
		class LicznikLokalny
		{
			private int m_nWartosc = 100;
			
			public int PodajWartosc()
			{
				return m_nWartosc + nStalaLokalna; // dostęp do zmiennej lokalnej
			}
		}
		
		// użycie klasy lokalnej
		LicznikLokalny licznik = new LicznikLokalny();
		System.out.println("Licznik lokalny = " + licznik.PodajWartosc());
	}
	
	public static void main(String args[])
	{
		Test test = new Test();
		test.MetodaZKlasaLokalna();
	}
}

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](../#Dziedziczenie, słowo kluczowe super/)) z klasy Object, posiada też konstruktor bezparametrowy odziedziczony z tej klasy.

Dla przykładu, w deklaracji klasy Punkt:

class Punkt
{
	int m_iWspX;
	int m_iWspY;
	byte m_bKolor;
}

nie ma definicji konstruktora, jednak deklaracja zmiennej punkt postaci:

Punkt punkt = 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:

class Punkt
{
	int m_iWspX;
	int m_iWspY;
	byte m_bKolor;
	
	// konstruktor bezparametrowy
	public Punkt()
	{
		m_iWspX = 0;
		m_iWspY = 0;
		m_bKolor = 0;
	}
	
	// konstruktor z parametrami
	public Punkt(int x, int y, byte kolor)
	{
		m_iWspX = x;
		m_iWspY = y;
		m_bKolor = kolor;
	}
}

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, byte kolor) który inicjalizuje pola danych na podstawie wartości argumentów x, y i kolor.

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](../#Dziedziczenie, słowo kluczowe super/)

Słowo kluczowe this
#

Słowo kluczowe this jest referencją do bieżącego obiektu. Może być używane w ciele metody nie statycznej do odwołania się do obiektu, dla którego została wywołana metoda. Najczęściej słowo kluczowe this stosuje się w sytuacjach, gdy nazwa argumentu metody jest identyczna z nazwą pola danych klasy:

class Punkt
{
	private int m_iWspX;
	private int m_iWspY;
	
	public void UstawWspolrzedne(int m_iWspX, int m_iWspY)
	{
		this.m_iWspX = m_iWspX; // this.m_iWspX to pole klasy
		this.m_iWspY = m_iWspY; // m_iWspX to argument metody
	}
}

Słowo kluczowe this może być także używane w konstruktorze do wywołania innego konstruktora tej samej klasy:

class Punkt
{
	private int m_iWspX;
	private int m_iWspY;
	private byte m_bKolor;
	
	public Punkt()
	{
		this(0, 0, (byte)0); // wywołanie konstruktora Punkt(int, int, byte)
	}
	
	public Punkt(int x, int y, byte kolor)
	{
		m_iWspX = x;
		m_iWspY = y;
		m_bKolor = kolor;
	}
}

Wywołanie this(argumenty) musi być pierwszą instrukcją w konstruktorze.

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: punkt1, punkt2 typu Punkt, wykonanie operacji:

punkt2 = punkt1;

nie spowoduje utworzenia nowego obiektu punkt2 o wartościach identycznych, jak obiekt punkt1, tylko skopiowanie referencji do obiektu punkt1. Aby utworzyć nowy obiekt, należy użyć konstruktora kopiującego w następujący sposób:

Punkt punkt2 = new Punkt(punkt1);

Definicja konstruktora kopiującego dla klasy Punkt przyjmuje postać:

public Punkt(Punkt punkt)
{
	m_iWspX = punkt.m_iWspX;
	m_iWspY = punkt.m_iWspY;
	m_bKolor = punkt.m_bKolor;
}

W konstruktorze tym, przy inicjalizacji pola m_sNazwa utworzony został nowy obiekt typu String:

m_sNazwa = new String(punkt.m_sNazwa);

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 RadioCB
{
	private int m_nKanalRadiowy; // kanał radiowy dla danego obiektu
	private static boolean[] s_bKanaly = new boolean[40]; // 40 kanałów CB
	
	// inicjator statyczny
	static
	{
		// inicjalizacja tablicy kanałów - wszystkie kanały są wolne
		for (int i = 0; i < s_bKanaly.length; i++)
			s_bKanaly[i] = false;
	}
	
	// konstruktor
	public RadioCB()
	{
		// znajdź pierwszy wolny kanał
		for (int i = 0; i < s_bKanaly.length; i++)
		{
			if (!s_bKanaly[i])
			{
				s_bKanaly[i] = true; // zarezerwuj kanał
				m_nKanalRadiowy = i + 1; // kanały numerowane od 1
				break;
			}
		}
	}
}

Jak widać, klasa RadioCB posiada zmienną statyczną s_bKanaly, 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 RadioCB konstruktor przydziela nieużywany dotychczas kanał radiowy. Oznacza to, że statyczna tablica kanałów musi być zainicjowana zanim pierwszy z obiektów typu RadioCB 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 właściwości jednej klasy przez inną klasę. Klasa, z której dziedziczymy nazywa się nadklasą (ang. superclass) a klasa dziedzicząca - podklasą (ang. subclass).

Diagram

Rysunek 2-1 Dziedziczenie

Relację dziedziczenia między nadklasą i podklasą wyrażamy za pomocą frazy ze słowem extends. Zadeklarujmy klasę PunktNazwany 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_sNazwa opisujące nazwę miejsca:

class PunktNazwany extends Punkt
{
	private String m_sNazwa;
	
	// konstruktor
	public PunktNazwany(int x, int y, byte kolor, String nazwa)
	{
		super(x, y, kolor); // wywołanie konstruktora nadklasy
		m_sNazwa = nazwa;
	}
	
	// konstruktor domyślny
	public PunktNazwany()
	{
		// domyślne wywołanie super() - konstruktor bezparametrowy nadklasy
		m_sNazwa = "Bez nazwy";
	}
	
	public String PodajNazwe()
	{
		return m_sNazwa;
	}
}

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 jest szczególnie użyteczne w przypadku przesłoniętych (ang. shadow) (pól danych i metod na nowo zdefiniowanych w podklasie) przy dziedziczeniu.

Jak widać, w konstruktorze PunktNazwany użyto instrukcji super(x, y, kolor). Takie użycie powoduje wywołanie konstruktora: Punkt(int x, int y, byte kolor) klasy Punkt z której dziedziczy klasa PunktNazwany. Następnie, konstruktor klasy PunktNazwany inicjuje pole danych m_sNazwa, 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 PunktNazwany(). Zainicjalizowano w nim jedynie pole m_sNazwa, natomiast pola m_iWspX, m_iWspY, m_bKolor dziedziczone z klasy Punkt są inicjalizowane przez domyślne wywołanie konstruktora nadklasy (super()).

Dla pokazania zasad dziedziczenia w Javie zdefiniujmy metodę o nazwie main:

public static void main(String args[])
{
	PunktNazwany punkt = new PunktNazwany(10, 20, (byte)1, "Warszawa");
	
	System.out.println("Punkt: " + punkt.PodajNazwe());
	System.out.println("Klasa obiektu: " + punkt.getClass().getName());
	System.out.println("Nadklasa: " + punkt.getClass().getSuperclass().getName());
}

Po wykonaniu tej metody na ekranie otrzymujemy:

Punkt: Warszawa
Klasa obiektu: PunktNazwany
Nadklasa: Punkt

Obiekt punkt klasy PunktNazwany wywołuje metodę getClass() dziedziczoną z klasy Object, której wartością jest referencja do wołającego ją obiektu. Następnie, dla tej referencji wykonywana jest metoda getName() dziedziczona z klasy Class (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.

Diagram

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ę toString():

public String toString()
{
	return "Punkt[" + m_iWspX + "," + m_iWspY + "," + m_bKolor + "]";
}

Do klasy PunktNazwany, która jest rozszerzeniem klasy Punkt dodajmy metodę toString(), która jest redefinicją (ang. overriding) metody z nadklasy. W ciele metody następuje wywołanie metody toString() z nadklasy przy użyciu słowa kluczowego super:

public String toString()
{
	return super.toString() + "[" + m_sNazwa + "]";
}

Dla sprawdzenia działania klasy Punkt i PunktNazwany zdefiniujmy klasę TestDziedziczenia:

class TestDziedziczenia
{
	public static void main(String args[])
	{
		Punkt punkt1 = new Punkt(1, 2, (byte)3);
		PunktNazwany punkt2 = new PunktNazwany(4, 5, (byte)6, "Kraków");
		
		System.out.println("punkt1: " + punkt1.toString());
		System.out.println("punkt2: " + punkt2.toString());
		
		// referencja do nadklasy może wskazywać na obiekt podklasy
		Punkt punkt3 = punkt2;
		System.out.println("punkt3: " + punkt3.toString());
	}
}

Po wykonaniu metody main na ekranie otrzymujemy:

punkt1: Punkt[1,2,3]
punkt2: Punkt[4,5,6][Kraków]
punkt3: Punkt[4,5,6][Kraków]

Dla obiektu typu PunktNazwany wykonywana jest metoda toString() 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 PunktNazwany referencji do obiektu typu Punkt.

Odwołanie typu:

punkt3.PodajNazwe()

nie jest poprawne. Aby uzyskać dostęp do metody lub pola należącego do nie bezpośredniej nadklasy, należy przeprowadzić konwersję typu (ang. type cast), która może zakończyć się niepowodzeniem i spowodować zgłoszenie wyjątku (więcej o wyjątkach w rozdziale poświęconym wyjątkom).

((PunktNazwany)punkt3).PodajNazwe()

Diagram

Ilustracja 2-1 Wynik wykonania aplikacji TestDziedziczenia

Dla pól danych użycie słowa kluczowego super lub konwersji do klasy Punkt powoduje wypisanie na ekranie wartości pola m_iWspX z odpowiedniej klasy. Natomiast dla metod, konwersja typu (Punkt)punkt2 jest równoważna wywołaniu punkt2.toString(). 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 toString() klasy Punkt: (((Punkt)punkt2).toString()) jest przekształcane w wywołanie przedefiniowującej ją metody toString() z klasy PunktNazwany.

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 jeśli chce wykorzystać ten mechanizm. Metoda finalize jest zdeklarowana z klauzulą throws Throwable, więc musi być wywoływana w bloku try{...} catch(...){...} (obsługa wyjątków omówiona jest w rozdziale 2.4).

protected void finalize() throws Throwable
{
	// kod finalizujący obiekt
	super.finalize(); // wywołanie finalizacji nadklasy
}

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.

Diagram

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 PolePowierzchni(). 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
{
	protected int m_nX, m_nY; // położenie figury
	
	// konstruktor
	public Figura(int x, int y)
	{
		m_nX = x;
		m_nY = y;
	}
	
	// metody abstrakcyjne
	public abstract void Rysuj();
	public abstract double PolePowierzchni();
	
	// metoda nie abstrakcyjna
	public void Przesun(int dx, int dy)
	{
		m_nX += dx;
		m_nY += dy;
	}
}

Klasa Kolo, która dziedziczy z nadklasy Figura, dostarcza implementacji dla dwu metod abstrakcyjnych Rysuj() i PolePowierzchni() zadeklarowanych w klasie abstrakcyjnej Figura.

class Kolo extends Figura
{
	private int m_nPromien;
	
	public Kolo(int x, int y, int promien)
	{
		super(x, y);
		m_nPromien = promien;
	}
	
	public void Rysuj()
	{
		System.out.println("Rysowanie koła o promieniu " + m_nPromien + 
						   " w punkcie [" + m_nX + "," + m_nY + "]");
	}
	
	public double PolePowierzchni()
	{
		return Math.PI * m_nPromien * m_nPromien;
	}
}

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:

[public] interface NazwaInterfejsu [extends NazwaInterfejsuNadrzednego]
{
	// deklaracje stałych
	typ NAZWA_STALEJ = wartość;
	
	// deklaracje metod abstrakcyjnych
	typ nazwaMetody(parametry);
}

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 MAX_ELEMENTOW = 1000; // stała interfejsu
	
	void Dodaj(Object obiekt);
	Object Pobierz(int indeks);
	int Rozmiar();
}

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[] m_tabElementy;
	private int m_nLiczbaElementow;
	
	public Wektor()
	{
		m_tabElementy = new Object[MAX_ELEMENTOW];
		m_nLiczbaElementow = 0;
	}
	
	public void Dodaj(Object obiekt)
	{
		if (m_nLiczbaElementow < MAX_ELEMENTOW)
		{
			m_tabElementy[m_nLiczbaElementow] = obiekt;
			m_nLiczbaElementow++;
		}
	}
	
	public Object Pobierz(int indeks)
	{
		if (indeks >= 0 && indeks < m_nLiczbaElementow)
			return m_tabElementy[indeks];
		else
			return null;
	}
	
	public int Rozmiar()
	{
		return m_nLiczbaElementow;
	}
}

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

Przykład 2.17 Definicja klasy TestInterfejsu

class TestInterfejsu
{
	public static void main(String args[])
	{
		Wektor wektor = new Wektor();
		
		// dodanie kilku elementów
		wektor.Dodaj("Element 1");
		wektor.Dodaj("Element 2");
		wektor.Dodaj(new Integer(123));
		
		// wyświetlenie elementów
		for (int i = 0; i < wektor.Rozmiar(); i++)
		{
			System.out.println("Element " + i + ": " + wektor.Pobierz(i));
		}
		
		// użycie referencji do interfejsu
		Kolekcja kolekcja = wektor;
		System.out.println("Rozmiar kolekcji: " + kolekcja.Rozmiar());
	}
}

Diagram

Ilustracja 2-2 Rezultat wykonania programu TestInterfejsu.

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 dziedziczenia wielokrotnego z C++.

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. grafika, z kolei pakiet ten jest częścią pakietu tyloch zawierającym pakiety zdefiniowane przeze mnie.

Poszczególne klasy publiczne definiujemy w pliku o nazwie:

NazwaKlasy.java

oprócz definicji jednej klasy publicznej w pliku tym mogą znajdować się definicje innych klas niepublicznych. I tak, klasę Punkt definiujemy w pliku Punkt.java:

package tyloch.grafika;

public class Punkt
{
	// definicja klasy
}

Podobnie jest dla klasy Kolo - definiujemy ją w pliku Kolo.java:

package tyloch.grafika;

public class Kolo extends Punkt
{
	// definicja klasy
}

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 tyloch.grafika muszą znajdować się w podkatalogach tyloch/grafika, 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=/usr/local/java/classes:/home/tyloch/klasy

to ostatecznie nasze pliki znajdą się w katalogu:

/usr/local/java/classes/tyloch/grafika/

lub

/home/tyloch/klasy/tyloch/grafika/

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 tyloch.grafika, program przyjmuje wtedy postać:

import tyloch.grafika.*;

class MojProgram
{
	public static void main(String args[])
	{
		Punkt punkt = new Punkt(10, 20);
		Kolo kolo = new Kolo(0, 0, 5);
		// ...
	}
}

Deklaracji import można użyć także dla importowania pojedynczej klasy:

import tyloch.grafika.Punkt;

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[] tablica;

lub

int tablica[];

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:

tablica = new int[10];

Gdy deklarujemy tablicę dla typów wewnętrznych (np. int, char, byte, itd.) możemy użyć listy inicjalizującej:

int[] tablica = {1, 2, 3, 4, 5};

Powyższe wyrażenie alokuje tablicę składającą się z pięciu liczb typu int z wartościami początkowymi odpowiednio: 1, 2, 3, 4, 5.

Gdy deklarujemy tablicę obiektów stosujemy operator new dla każdego z elementów tablicy osobno:

String[] tablicaStringow = new String[5];
tablicaStringow[0] = new String("Element 0");
tablicaStringow[1] = new String("Element 1");
// itd.

Przykład 2.18 Inicjalizacja wielowymiarowych tablic obiektów

class TestTablicWielowymiarowych
{
	public static void main(String args[])
	{
		// utworzenie tablicy dwuwymiarowej o nieregularnych wymiarach
		String[][] tablica = new String[5][];
		
		// alokacja podtablic o różnych rozmiarach
		for (int i = 0; i < tablica.length; i++)
		{
			tablica[i] = new String[i + 1];
			
			// inicjalizacja elementów
			for (int j = 0; j < tablica[i].length; j++)
			{
				tablica[i][j] = new String("Element[" + i + "][" + j + "]");
			}
		}
		
		// wyświetlenie tablicy
		for (int i = 0; i < tablica.length; i++)
		{
			for (int j = 0; j < tablica[i].length; j++)
			{
				System.out.print(tablica[i][j] + " ");
			}
			System.out.println();
		}
	}
}

W powyższym przykładzie utworzona zostaje pięcio-elementowa tablica, której elementami są tablice obiektów typu String. 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

class Tablice
{
	public static void main(String args[])
	{
		// różne typy tablic
		int[] tablicaInt = new int[10];
		double[][] tablicaDouble = new double[5][5];
		String[] tablicaString = new String[3];
		Object[] tablicaObject = new Object[2];
		
		// wyświetlenie informacji o tablicach
		System.out.println("Typ tablicaInt: " + tablicaInt.getClass().getName());
		System.out.println("Długość tablicaInt: " + tablicaInt.length);
		
		System.out.println("Typ tablicaDouble: " + tablicaDouble.getClass().getName());
		System.out.println("Długość tablicaDouble: " + tablicaDouble.length);
		
		System.out.println("Typ tablicaString: " + tablicaString.getClass().getName());
		System.out.println("Długość tablicaString: " + tablicaString.length);
		
		System.out.println("Typ tablicaObject: " + tablicaObject.getClass().getName());
		System.out.println("Długość tablicaObject: " + tablicaObject.length);
	}
}

Aplikacja Tablice ilustruje użycie własności obiektowych tablic. Na ekranie wypisywane są sygnatury (odpowiedniki nazw klas) poszczególnych tablic.

Diagram

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:

  • [I - tablica jednowymiarowa typu int
  • [[D - tablica dwuwymiarowa typu double
  • [Ljava.lang.String; - tablica jednowymiarowa obiektów typu String
  • [Ljava.lang.Object; - tablica jednowymiarowa obiektów typu Object

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.

Czy ten artykuł był pomocny? Podziel się nim z innymi!