Klasę Javy można traktować jako wzorzec i jednocześnie generator obiektów.
Jako wzorzec klasa zapewnia hermetyzację (zamknięcie w jednej jednostce
syntaktycznej) danych i metod oraz ukrywanie informacji, które nie powinny być
widoczne dla użytkownika. Jako generator zapewnia tworzenie obiektów za pomocą
operatora new, którego argumentem jest konstruktor klasy.
Definicja klasy ma postać:
Deklaracja klasy
{
Ciało klasy
}
Deklaracja klasy składa się w najprostszym przypadku ze słowa kluczowego class i
nazwy klasy. Przed słowem kluczowym class może wystąpić jeden ze specyfikatorów:
abstract, public, final, lub dwa z nich, np. public abstract, public final.
Specyfikator abstract odnosi się do klas abstrakcyjnych, które nie mogą mieć
wystąpień, zaś final deklaruje, że dana klasa nie może mieć podklas. Brak
specyfikatora oznacza, że dana klasa jest dostępna tylko dla klas zdefiniowanych
w tym samym pakiecie. Specyfikator public mówi, że klasa jest dostępna
publicznie. Klasa abstrakcyjna może zawierać metody abstrakcyjne (bez
implementacji, poprzedzone słowem kluczowym abstract; w miejscu ciała metody
abstrakcyjnej występuje średnik); wówczas każda jej podklasa musi podawać
implementacje tych metod. Każda klasa, która odziedziczy metodę abstrakcyjną,
ale nie dostarczy jej implementacji, sama staje się klasą abstrakcyjną.
Po nazwie klasy mogą wystąpić frazy: extends nazwa_superklasy oraz implements
nazwy_interfejsów. Fraza extends nazwa_superklasy mówi, że klasa dziedziczy
(zawsze publicznie) od klasy nazwa_superklasy, zaś implements nazwy_interfejsów
deklaruje, że w danej klasie zostaną zdefiniowane metody, zadeklarowane w
implementowanych interfejsach. Jeżeli dana klasa implementuje więcej niż jeden
interfejs, wtedy nazwy kolejnych interfejsów oddziela się przecinkami.
Uwaga! - W języku Java każda klasa dziedziczy od predefiniowanej klasy Object.
Zatem, jeżeli w definicji klasy nie występuje fraza extends, to jest to
równoważne niejawnemu wystąpieniu w tej definicji frazy extends Object.
Zauważmy, że oprócz słowa kluczowego class i nazwy klasy wszystkie pozostałe
elementy w deklaracji klasy są opcjonalne. Jeżeli nie umieścimy ich w
deklaracji, to kompilator przyjmie domyśnie, że klasa jest niepubliczną,
nieabstrakcyjną i niefinalną podklasą predefiniowanej klasy Object.
Ciało klasy jest zamknięte w nawiasy klamrowe i może zawierać zmienne składowe
(to jest pola lub zmienne wystąpienia), zmienne klasy (statyczne, tj.
poprzedzone słowem kluczowym static), konstruktory i metody oraz funkcje klasy
(statyczne). Nazwa każdej zmiennej składowej, zmiennej klasy, metody lub funkcji
klasy musi być poprzedzona nazwą typu (np. boolean, double, char, float, int,
long, void). Przed nazwą typu może wystąpić jeden ze specyfikatorów dostępu:
private (dostęp tylko dla elementów klasy, np. private double d;), protected
(dostęp tylko w podklasie, nawet jeśli podklasa należy do innego pakietu; nie
dotyczy zmiennych klasy) lub public (dostęp publiczny). Brak specyfikatora
oznacza, że dany element jest dostępny tylko dla klas w tym samym pakiecie. Po
specyfikatorze dostępu może wystąpić słowo kluczowe final. Słowo final przed
nazwą typu zmiennej wystąpienia lub zmiennej klasy deklaruje jej
niemodyfikowalność (np. public static final int i = 10;), zaś w odniesieniu do
metody oznacza, że nie może ona być redefiniowana w podklasie (np. public final
void f(int i) {/* ... */ }).
Dostęp do elementów klasy uzyskuje się za pomocą operatora kropkowego. Jeżeli
element danej klasy (zmienna lub metoda) przesłania (overrides) jakiś element
swojej superklasy, to można się do niego odwołać za pomocą słowa kluczowego
super, jak w poniższym przykładzie:
class ASillyClass /* Deklaracja klasy */
{
static final int MAX = 100; /** Definicja stałej */
boolean aVariable;/* Deklaracja zmiennej wystąpienia */
static public int x = 10; //Definicja zmiennej klasy
void aMethod() { //Definicja metody
aVariable = true;// Instrukcja przypisania
}
class AsillerClass extends ASillyClass {
boolean aVariable;
void aMethod() {
aVariable = false;
super.aMethod(); /* Wywołanie metody superklasy */
System.out.println(aVariable);
System.out.println(super.aVariable);
}
Dostęp do zmiennych składowych klasy (statycznych) jest możliwy bez tworzenia
obiektów tej klasy. Np. dla klasy ASillyClass możemy napisać instrukcję:
System.out.println(ASillyClass.x);
Klasy i omawiane niżej interfejsy są typami referencyjnymi (odnośnikowymi).
Wartościami zmiennych tych typów są odnośniki do wartości lub zbiorów wartości
reprezentowanych przez te zmienne. Np. instrukcja
ASillyClass oob;
jedynie powiadamia kompilator, że będziemy używać zmiennej oob, której typem
jest ASillyClass. Do zmiennej oob możemy przypisać dowolny obiekt typu
ASillyClass utworzony za pomocą operatora new:
oob = new ASillyClass();
W powyższej instrukcji argumentem operatora new jest generowany przez kompilator
konstruktor ASillyClass() klasy ASillyClass, który inicjuje obiekt utworzony
przez operator new. Operator new zwraca odnośnik do tego obiektu, po czym
przypisuje go do zmiennej oob.
Konstrukcja o postaci
interface nazwa {
/* Deklaracje metod i definicje stałych */
}
jest w języku Java typem definiowanym przez użytkownika. Deklaracja metody
składa się z sygnatury (sygnatury metod zawierają typ zwracany, nazwy metod i
typy argumentów) i terminalnego średnika. Ponieważ interfejs może zawierać
jedynie deklaracje metod i definicje stałych, odpowiada on klasie abstrakcyjnej
z zadeklarowanymi publicznymi polami danych i metodami abstrakcyjnymi. W związku
z tym w definicji interfejsu zabrania się użycia specyfikatorów private i
protected, zaś użycie specyfikatorów abstract i public jest zbyteczne.
Weźmy dla przykładu dwa interfejsy PlaneLike i BoatLike
interface PlaneLike {
void takeOff();
float kmph();
}
interface BoatLike {
void swim();
float knots();
}
i zdefiniujmy ich implementacje w klasach Plane i Boat, które dziedziczą od
wspólnej superklasy Vehicle:
class Vehicle {}
class Plane extends Vehicle implements PlaneLike {
/* Plane must implement kmph(), takeOff() */
public void takeOff() { System.out.println("Plane is taking off"); }
public float kmph() { return 600; }
}
class Boat extends Vehicle implements BoatLike {
/* Boat must implement knots(),swim() */
public void swim() { System.out.println("Boat is swimming"); }
public float knots() { return 20; }
}
Poprawne będą wówczas deklaracje
Plane biplane = new Plane();
biplane.takeOff();
Boat vessel = new Boat();
vessel.swim();
a także deklaracje
PlaneLike aircraft = new Plane();
aircraft.takeOff();
BoatLike motorboat = new Boat();
motorboat.swim();
Załóżmy teraz, że chcielibyśmy skonstruować klasę SeaPlane, której obiekty
powinny się zachowywać w pewnych okolicznościach jak pojazdy wodne, zaś w innych
jak pojazdy powietrzne. W języku, który wyposażono w mechanizm dziedziczenia
mnogiego (np. C++) klasa SeaPlane miałaby dwie superklasy: Plane i Boat, jak
pokazano w części a) rysunku.

Rys. a) Graf dziedziczenia mnogiego dla SeaPlane
Rys. b) Graf dziedziczenia pojedynczego dla SeaPlane
W języku Java podobny efekt można osiągnąć poprzez implementację w klasie
SeaPlane obu interfejsów, t.j. PlaneLike i BoatLike (część b rysunku):
class SeaPlane extends Vehicle implements
PlaneLike, Boatlike {
/** SeaPlane must implement kmph(), takeOff(),
knots(), swim() */
}
Dla osiągnięcia pożądanego zachowania się obiektów klasy SeaPlane moglibyśmy
umieścić w głównym module źródłowym Multi1.java następujący kod:
public
class Multi1 {
public static void main(String args[])
{
Boat vessel = new Boat();
Plane biplane = new Plane();
System.out.println("Let's starting!");
PlaneLike ref1 = new SeaPlane(biplane);
ref1.takeOff();
System.out.println(ref1.kmph());
BoatLike ref2 = new SeaPlane(vessel);
ref2.swim();
System.out.println(ref2.knots());
}
}
Jednak ze względu na wielokrotną używalność kodu lepszym rozwiązaniem będzie
taka definicja klasy SeaPlane, która wykorzystuje mechanizm delegacji, tj.
bezpośredniej współpracy z klasami Plane i Boat:
class SeaPlane extends Vehicle implements
PlaneLike, BoatLike {
// define a Plane and Boat instance variables
// i.e. collaborate with Plane and Boat classes
private Plane itAsPlane;
private Boat itAsBoat;
//Konstruktor
SeaPlane(Plane itAsPlane)
{
this.itAsPlane=itAsPlane;
}
//Konstruktor
SeaPlane(Boat itAsBoat)
{
this.itAsBoat=itAsBoat;
}
// forward the messages to the appropriate collaborator
public float kmph() { return itAsPlane.kmph(); }
public void takeOff() { itAsPlane.takeOff(); }
public float knots() { return itAsBoat.knots(); }
public void swim() { itAsBoat.swim(); }
}
Interfejs nie może dziedziczyć klas, ale może dziedziczyć dowolnie wiele
interfejsów. Np. korzystając z podanych wyżej definicji moglibyśmy utworzyć
interfejs
interface SeaPlaneLike extends PlaneLike, BoatLike{
public long SPEED_LIMIT = 1000;
}
i wykorzystać go w klasie SeaPlane, implementując metody zadeklarowane w
interfejsach PlaneLike i BoatLike.
Program języka Java może się składać z wielu niezależnie kompilowalnych
modułów źródłowych, w których umieszcza się definicje klas oraz interfejsów.
Moduły źródłowe są przechowywane w plikach o nazwie Nazwa.java, gdzie Nazwa jest
nazwą klasy publicznej; pliki te stanowią jednostki kompilacji. Jeżeli w pliku
Nazwa.java zdefiniowano tylko jedną klasę, to w wyniku kompilacji tego pliku
powstaje plik wynikowy Nazwa.class. Jeżeli program jest aplikacją, to w zestawie
modułów źródłowych musi się znaleźć dokładnie jeden moduł źródłowy (moduł główny
aplikacji) z klasą publiczną, która zawiera publiczną i statyczną funkcję main
(każdy inny moduł źródłowy może zawierać klasę z funkcją main, jeżeli nie jest
to klasa publiczna).
Moduł źródłowy, w którym definicje klas oraz interfejsów poprzedzono deklaracją
pakietu o postaci package nazwa_pakietu; staje się pakietem. Deklaracja pakietu
rozszerza przestrzeń nazw programu i pozwala na lepsze zarządzanie programem
wielomodułowym. Jeżeli moduł źródłowy nie zawiera deklaracji pakietu, to należy
on do tzw. pakietu domyślnego (pakietu bez nazwy). Np. zadeklarowana wcześniej
klasa Hello, umieszczona w pliku Hello.java należy do pakietu domyślnego.
Pakiety są ściśle powiązane z katalogami, w których umieszcza się moduły
źródłowe i pliki wynikowe.
Załóżmy np., że w katalogu mike\myprog\pakiet1
(Win95 DOS) umieszczono główny plik źródłowy aplikacji Student.java o postaci:
package mike.myprog.pakiet1;
import mike.myprog.pakiet1.Grade;
public
class Student {
int i = 10;
public static void main(String args[])
{
System.out.println("Hello, I am here!");
Grade mygrade = new Grade();
mygrade.printgrade();
}
}
Plik zawiera definicję klasy Student, poprzedzoną deklaracją pakietu oraz
deklaracją importu klasy Grade. Plik ten może zostać skompilowany (wywołaniem
kompilatora javac z katalogu nadrzędnego w stosunku do katalogu mike: javac mike\myprog\pakiet1\Student.java).
Jeżeli np. plik Grade.java ma postać:
package mike.myprog.pakiet1;
class Grade {
int i = 10;
public void printgrade()
{
System.out.println("My grades are higher than " + i);
}
}
class Empty {}
to zostaną utworzone dwa pliki wynikowe: Student.class i Grade.class, a
wywołanie interpretatora java mike\myprog\pakiet1\Student spowoduje
wyprowadzenie na ekran napisu:
Hello, I am here!
My grades are higher than 10
Uwaga. Deklaracja importu nie oznacza włączania do pliku Student.java tekstu
zawartego w pliku Grade.java.Natomiast pozwala ona użytkownikowi klasy Student
używać skrótowych nazw: np. zamiast pisać mike\myprog\pakiet1\Grade mygrade =
new mike\myprog\pakiet1\Grade(); mogliśmy napisać krótko Grade mygrade = new
Grade();. Gdybyśmy chcieli używać również klasy Empty, to deklaracja importu
miałaby postać: import mike.myprog.pakiet1.*. W języku Java ważna jest także
kolejność deklaracji:najpierw deklaracja pakietu, po niej deklaracje importu, po
czym definicje klas.
Polimorfizm możemy określić jako wirtualizację operacji; jest to możliwość
dynamicznego (późnego, realizowanego w fazie wykonania) wiązania nazwy operacji
do wielu implementacji (metod) tej operacji w różnych klasach pozostających w
relacji dziedziczenia. Wiązaniu towarzyszy mechanizm wyboru konkretnej
implementacji. Wybór implementacji zależy od nazwy metody oraz od typu
dynamicznego tego obiektu, dla którego została wywołana operacja, a nie od typu
zmiennej, wskazującej ten obiekt.
Zauważmy, że przeciążanie operacji nie prowadzi do polimorfizmu; w tym przypadku
wiązanie, dopasowanie parametrów wywołania operacji do określonej sygnatury i
wybór implementacji są statyczne, ponieważ są wykonywane w fazie kompilacji.
Muszą wówczas istnieć różnice w sygnaturach operacji (w typie wyniku i/lub w
typach parametrów wejściowych), a kryterium wyboru implementacji zależy od tych
różnic.
W języku Java wszystkie deklaracje metod, które nie są metodami klasy, ani
metodami finalnymi, można traktować jako operacje wirtualne. Tak więc definicja
metody o danej sygnaturze w superklasie może być przesłonięta przez definicję
metody o tej samej sygnaturze w podklasie. W rezultacie kompilator nie może
związać nazwy metody z jej implementacją (istnieją co najmniej dwie
implementacje o takich samych sygnaturach). Wiązanie może się odbyć dopiero w
fazie wykonania; wówczas interpretator określa typ dynamiczny obiektu na którym
zostaje wywołana operacja i wiąże tę operację z właściwą dla danego obiektu
implementacją. Ilustrację wywołania polimorficznego pokazuje poniższy prosty
przykład:
Plik Polimorf.java
class Base {
public void msg()
{
System.out.println("Base class method");
}
}
class Derived extends Base {
public void msg()
{
System.out.println("Derived class method");
}
}
public class Polimorf {
int i = 10;
public static void main(String args[])
{
Base b1 = new Base();
b1.msg();
b1 = new Derived();
if(b1 instanceof Derived)
System.out.println("Derived");
b1.msg();
}
}
Metoda msg() ma dwie implementacje: w klasie Base i w klasie Derived.
Odniesienie (referencja) b1 do klasy Base jest najpierw inicjowane obiektem
klasy Base, a więc instrukcja b1.msg(); wywoła metodę tej klasy. Następnie
odniesieniu b1 zostaje przypisany obiekt klasy Derived, co spowoduje, że
następna instrukcja b1.msg(); wywoła implementację metody msg() zdefiniowaną w
klasie Derived. Instrukcja if wykorzystująca operator instanceof służy do
sprawdzenia, jaki jest typ dynamiczny obiektu b1. Wynikiem wykonania programu
będzie następujący wydruk:
Base class method
Derived
Derived class method
Wyjątki pozwalają zachować kontrolę nad przebiegiem wykonania funkcji
(metod), a także pojedynczych instrukcji zawartych w funkcjach. Wyjątek jest
zdarzeniem, które pojawia się podczas wykonania i rozrywa normalną kolejność
wykonania instrukcji.
W języku Java istnieje bardzo rozbudowana hierarchia (drzewo) predefiniowanych
klas wyjątków, których superklasą jest klasa Throwable, a głównymi gałęziami
drzewa są klasy Error i Exception. Część wyjątków należy do grupy tzw. wyjątków
weryfikowalnych (checked exceptions): kompilator sprawdza, czy program zawiera
procedury obsługi dla każdego wyjątku z tej grupy. Natomiast wyjątki klasy Error
i jej podklas należą do grupy wyjątków nieweryfikowalnych (unchecked exceptions),
ponieważ mogą one wystąpić w wielu punktach programu i powrót z nich jest trudny
lub wręcz niemożliwy. Do grupy wyjątków nieweryfikowalnych należą też wyjątki
klasy RuntimeException (podklasa Exception) i jej podklas, ponieważ
zadeklarowanie w programie takich wyjątków nie mogłoby znacznie pomóc w
ustaleniu (przez kompilator) poprawności programów.
Dla obsługi wyjątków weryfikowalnych wprowadzono cztery słowa kluczowe: throw,
throws, try, catch i finally. Słowo kluczowe throw służy do jawnego zgłaszania
wyjątków nieweryfikowalnych i występuje w instrukcji throw o składni throw
wyrażenie;gdzie wyrażenie musi oznaczać zmienną lub wartość typu referencyjnego
do klasy Throwable lub jej podklas. Zgłoszenie wyjątku w instrukcji throw
spowoduje natychmiastowe opuszczenie bloku lub funkcji zawierającego instrukcję
throw i znalezienie instrukcji try, która przechwyci zgłoszony wyjątek. Jeżeli
nie ma takiej instrukcji try, zostanie wywołana metoda UncaughtException i
wykonanie programu (lub wątku) zostanie zakończone.
Fraza:throws klasa_wyjątków
może wystąpić w nagłówku funkcji (metodzie wystąpienia, konstruktorze, funkcji
klasy), np.
public staic void main(String args[]) throws Exception {/*...*/}
void printNumber(int number) throws WrongNumberException {/*...*/}
Fraza throws klasa_wyjątków oznacza, że dana funkcja może zgłaszać jedynie
wyjątki podanej klasy.
Jeżeli wykonanie pewnej instrukcji programu może spowodować powstanie
wyjątkowego zdarzenia, to musi ona być ujęta w blok instrukcji try, po którym
muszą wystąpić procedury obsługi wyjątku mające postać frazy catch i
bezpośrednio po catch (opcjonalnie) frazy finally.
Składnia tej konstrukcji ma
postać:
try {I}catch(arg1 e1) {I} catch(arg2 e2) {I} ... catch(argn en) {I} ...
finally {I}
gdzie I oznacza instrukcje, arg1 .. argn klasy wyjątków, e1 ... en zmienne
odniesienia do tych klas.
Blok catch należy traktować jako ciało procedury obsługi wyjątku należącego do
klasy argi.
Jeżeli funkcja/metoda zawiera instrukcję try, to wyjątki mogą być zgłaszane
wyłącznie w bloku try. Po zgłoszeniu wyjątku sterowanie opuszcza kod, który
zgłosił wyjątek i przechodzi do pierwszej w kolejności procedury catch; jeżeli
ta nie obsługuje wyjątku zgłoszonej klasy, jest on przekazywany do następnej.
Blok frazy finally (jeśli występuje) jest wykonywany zawsze gdy kończy się
wykonanie instrukcji try i to nawet wtedy, gdy wykonanie try zostaje gwałtownie
przerwane.
Prosty program, który jedynie ilustruje składnię zgłaszania i obsługi wyjątków
może wyglądać następująco:
import java.io.*;
public class TestEx {
public TestEx() {}
final int CONSTANT = 10;
void ff() throws IOException
{
try
{
System.out.println("I was here, within try statement.");
if(CONSTANT < 0)
throw new IOException("CONSTANT should be negative");
}
catch(IOException exc)
{
System.err.println("Caught IOException: "+ exc.getMessage());
}
finally
{
System.out.println("And now I am in finally blok");
}
}//end ff
public static void main(String args[]) throws IOException
{
TestEx te = new TestEx();
te.ff();
}//end main
}//end class
Podany niżej przykład definiuje klasę ListOfNumbers, wywołującą z pakietów Javy
dwie metody klas, które mogą zgłosić wyjątki.
import java.io.*;
import java.util.Vector;
public class ListOfNumbers {
private Vector victor;
private static final int size = 10;
public ListOfNumbers () {
victor = new Vector(size);
for (int i = 0; i < size; i++)
victor.addElement(new Integer(i));
}
public void writeList() {
PrintWriter p = null;
try {
System.out.println("Entering try statement");
p = new PrintWriter(new FileOutputStream("OutFile.txt"));
for (int i = 0; i < size; i++)
p.println("Value at: " + i + " = " + victor.elementAt(i));
} catch (ArrayIndexOutOfBoundsException e) {
System.err.println("Caught ArrayIndexOutOfBoundsException: " +
e.getMessage());
} catch (IOException e) {
System.err.println("Caught IOException: " + e.getMessage());
} finally {
if (p != null) {
System.out.println("Closing PrintWriter");
p.close();
} else {
System.out.println("PrintWriter not open");
}
}
}
public static void main(String args[]) {
ListOfNumbers lst = new ListOfNumbers();
lst.writeList();
}//end main
}//end ListOfNumbers
Konstruktor ListOfNumbers() tworzy obiekt klasy Vector o dziesięciu
elementach będących wartościami od 0 do 9. W klasie ListOfNumbers zdefiniowano
także metodę writeList(), która zapisuje ten ciąg liczb do pliku tekstowego
OutFile.txt.
Metoda writeList() wywołuje dwie metody, które mogą zgłosić
wyjątki:
konstruktor klasy FileOutputStream, który zgłasza IOException jeżeli z
jakiegoś powodu nie może otworzyć pliku:
p = new PrintWriter(new FileOutputStream("OutFile.txt"));
metodę elementAt() klasy Vector, która zgłasza wyjątek
ArrayIndexOutOfBoundsException jeżeli przekazana do niej wartość indeksu jest
zbyt mała (liczba ujemna) lub zbyt duża (większa niż liczba elementów zawartych
aktualnie w obiekcie klasy Vector).
Instrukcje System.err.println wykorzystują komunikaty generowane przez metodę
getMessage klasy Throwable (lub jej podklasę), która podaje dodatkowe informacje
o zaistniałym błędzie.
Jeżeli na dysku jest wystarczająco dużo miejsca na zapisanie pliku OutFile.txt,
to program założy taki plik z zawartością:
Value at: 0 = 0
...
Value at 9 = 9
oraz wyprowadzi na ekran dwa wiersze tekstu:
Entering try statement
Closing PrintWriter
Język Java jest wyposażony w mechanizm zbierania nieużytków (garbage
collection). System wykonawczy Javy automatycznie zwraca przydzielony obiektowi
obszar pamięci gdy stwierdzi, że do danego obszaru nie odnosi się już żadna
referencja.
Program Javy jest z reguły wykonywany w obrębie jednego procesu, którego
stos może być wykorzystywany przez wiele współbieżnych wątków programu (wątkiem
nazywa się sekwencyjny przepływ sterowania w procesie, który wykonuje dany
program). Nawet najprostsza aplikacja, która wyświetlała napis "Hello, World!",
miała dwa wątki wykonania: wątek główny (main thread) wykonujący kod tej
aplikacji oraz kolektor nieużytków.
Jeżeli maszyna wirtualna jest wieloprocesorowa, wątki mogą być wykonywane
współbieżnie; na komputerze jednoprocesorowym współbieżność może być emulowana (np.
w systemie Windows95) przez przydzielanie poszczególnym wątkom pewnej liczby
kwantów czasu procesora. W tym drugim przypadku jest realizowana tzw.
wielowątkowość wywłaszczeniowa (preemptive), która nie dopuszcza do
"zagłodzenia" (starving) wątków o niskich priorytetach. Priorytet jest liczbą z
przedziału 1..10; jeżeli priorytet nie zostanie ustawiony jawnie (funkcją
setPriority klasy Thread), to nowy wątek przejmie prirytet wątku, który go
utworzył.
Uwaga. Zagłodzenie może się zdarzyć wtedy, gdy jeden lub więcej wątków w
programie jest blokowanych przed dostępem do pewnego zasobu i wskutek tego nie
mogą biec dalej. Krańcową postacią zagłodzenia jest zakleszczenie lub impas (deadlock).Impas
pojawia się wtedy, gdy dwa lub więcej wątków czeka na warunek, który nie może
być spełniony; typowym przykładem jest sytuacja, gdy istnieją dwa wątki i każdy
z nich czeka na wykonanie czegoś przez partnera.
Wykonanie metody start (public synchronized void start()) na rzecz utworzonego
wcześniej obiektu klasy Thread powoduje utworzenie wątku. Przebieg wykonania
wątku zależy od implementacji metody run (public void run()), wywoływanej
niejawnie przez system tuż po utworzeniu wątku. Zakończenie wykonania metody
run powoduje niejawne wywołanie metody stop (public static final void stop()),
która niszczy wątek. Stanami wątków można sterować przez wywołania finalnych
metod wystąpienia suspend i resume (zawieszenie i wznowienie wątku
zawieszonego), wait i notify (wstrzymanie i uwolnienie wątku wstrzymanego),
wywołania finalnych metod synchronizowanych join() i join(long millisec), które
powodują wstrzymanie wykonywania wątku aż do zniszczenia go przez inny wątek
oraz przez wywołania metod klasy: public static void sleep(long millisec), która
powoduje uśpienie wątku na podany okres czasu i public static void yield(),
która oddaje dostęp do procesora innemu wątkowi (o ile taki istnieje). Ponadto
można wywołać metodę public final boolean isAlive() dla stwierdzenia, czy wątek
istnieje.
Wątek Javy może być tworzony na dwa sposoby: albo jako obiekt podklasy, która
dziedziczy od klasy Thread (klasa java.lang.Thread zawiera metody, które
kontrolują i synchronizują poszczególne wątki), albo jako obiekt klasy, która
implementuje interfejs Runnable (java.lang.Runnable). W obu przypadkach należy
podać implementację metody run() zadeklarowanej w interfejsie Runnable. Pierwszy
sposób ilustruje poniższy przykład.
public class Thread1 extends Object {
public static void main(String args[]) {
Thread x = new MyThread("Fastthread");
Thread y = new MyThread("Slow thread");
x.setPriority(Thread.MAX_PRIORITY);
x.start();
y.start();
}//end main()
}//end Thread1
class MyThread extends Thread {
protected String name = "not initialized";
public MyThread(String nameString)
{ name = nameString; }
public void run() {
for(int i =1;i<=10;i++) {
try {
Thread.currentThread().sleep(300);
}//end try
catch(InterruptedException e) {
} //end catch
System.out.println(name+"continues... "+i);
}//end for
System.out.println(name+" is DONE!! ");
}//end run()
}//end MyThread
W przykładzie utworzono dwa wątki, x i y, przy czym jeden z nich (x )ustawiono
na najwyższy osiągalny priorytet. Każdy wątek będzie wykonywany oddzielnie, ale
wątek x zakończy działanie wcześniej.
Drugi sposób implementuje interfejs Runnable:
public class TwoThreads extends Object {
public static void main(String args[]) {
MyClass xx = new MyClass();
MyClass yy = new MyClass();
Thread x = new Thread(xx);
Thread y = new Thread(yy);
x.setPriority(Thread.MAX_PRIORITY);
x.start();
y.start();
}//end main()
}//end TwoThreads
class MyClass implements Runnable {
public void run()
{
for(int I=0;I<=10;I++)
{ System.out.println("Thread progress="+I); }
System.out.println("Thread completed");
}
}
W podanych wyżej przykładach wątki były niezależne i asynchroniczne. Inaczej
mówiąc, każdy wątek zawierał wszystkie dane i metody potrzebne dla jego
wykonania i nie wymagał żadnych zewnętrznych zasobów lub metod. Ponadto przebieg
każdego wątku miał własny rytm, niezależny od stanu, czy aktywności drugiego,
biegnącego współbieżnie.
Istnieje jednak wiele sytuacji, gdy oddzielne, współbieżnie wykonywane wątki
współdzielą pewne dane i muszą uwzględniać stany i aktywności innych wątków.
Powszechnie znanym modelem programistycznym takich sytuacji jest model
producent/konsument, w którym producent generuje strumień danych pobieranych
następnie przez konsumenta. Przykładem praktycznym może być program, w którym
jeden wątek (producent) zapisuje dane do pliku, podczas gdy drugi wątek czyta
dane z tego samego pliku. Innym przykładem może być pisanie znaków na
klawiaturze: wątek producenta umieszcza naciśnięcia klawisza w kolejce zdarzeń,
a wątek konsumenta odczytuje zdarzenia z tej samej kolejki. Jeszcze innym
przykładem może być wysyłanie znaków do tego samego strumienia (np. do
System.out) z kilku współbieżnie wykonywanych wątków. Widzimy, że podane
przykłady wykorzystują współbieżne wątki, które dzielą wspólny zasób: w
pierwszym współdzielą plik, a w drugim kolejkę zdarzeń, w trzecim ten sam
strumień. Ponieważ wątki współdzielą wspólny zasób, muszą być w jakiś sposób
synchronizowane (np. w trzecim przykładzie przy braku synchronizacji łańcuchy
znaków pochodzące z różnych źródeł mogą być bezsensownie przemieszane).
Segmenty kodu programu, które żądają dostępu do tego samego obiektu z dwóch
oddzielnych, współbieżnych wątków, nazywa się sekcjami krytycznymi. W języku Java sekcją krytyczną może być blok lub metoda; identyfikuje się je słowem
kluczowym synchronized.
W przypadku bloku mamy instrukcję synchronized o składni
synchronized ( Wyrażenie ) Blok
w której Wyrażenie musi być typu referencyjnego, a Blok jest instrukcją
grupującą, objetą nawiasami klamrowymi.
Instrukcja synchronized przejmuje wzajemnie wykluczającą blokadę na rzecz
wykonywanego wątku, wykonuje Blok, po czym zwalnia blokadę. Inaczej mówiąc,
wykonanie instrukcji synchronized powoduje przydzielenie wątkowi podanego Bloku
jako sekcji krytycznej, a po wykonaniu go na rzecz obiektu identyfikowanego
przez Wyrażenie, zwolnienie sekcji.
Ilustracją wykorzystania instrukcji synchronized może być poniższy prosty
program:
class TestSynchro {
public static void main(String[] args) {
TestSynchro t = new TestSynchro();
synchronized(t) {
synchronized(t) {
System.out.println("made it!");
}
}//end synchronized
}//end main
}//end TestSynchro
który wydrukuje napis
made it!
Metoda jest synchronizowana jeżeli w jej nagłówku umieszczono słowo kluczowe synchronized. Synchronizowana metoda operująca na obiekcie pewnej klasy
automatycznie nakłada blokadę na ten obiekt przed wykonaniem jego ciała
(funkcji, metod) i automatycznie zwalnia blokadę przy powrocie, podobnie jak
instrukcja synchronized. Tak więc segment kodu
class Test {
int count;
synchronized void bump() { count++; }
static int classCount;
static synchronized void classBump() {
classCount++;
}
}
jest równoważny segmentowi
class BumpTest {
int count;
void bump(){
synchronized (this){
count++;
}//end synchronized statement
}
static int classCount;
static void classBump() {
try {
synchronized (Class.forName("BumpTest")) {
classCount++;
} end synchronized statement
}//end try
catch (ClassNotFoundException e) {
...
}end catch
}//end classBump
}//end BumpTest
Wymieniona "blokada" (lock) jest w programie wielowątkowym związana z obiektem,
na którym wątek ma wykonywać pewne operacje; jest ona często nazywana monitorem
obiektu. Do czasu zwolnienia monitora, zostanie zablokowane wykonanie każdego
innego wątku, który podejmie próbę wywołania (na rzecz tego samego obiektu),
dowolnej metody sychronizowanej danej klasy.
Zauważmy, że w klasie Test metoda classBump() jest statyczna (jest metodą
klasy). Zatem, mimo iż metoda ta jest synchronizowana, może być jednocześnie
wywoływana na rzecz wielu obiektów, a więc blokada nie będzie efektywna. Dla
uniknięcia możliwości jednoczesnego wykonywania pewnej operacji na tym samym
obiekcie (w szczególności zawierającym metody statyczne) przez dwa różne wątki
dobrą praktyką jest definiowanie klas tak, aby były przygotowane na użycie
współbieżne, jak ilustruje to poniższy kod:
public class Box {
private Object boxContents;
public synchronized Object get() {
Object contents = boxContents;
boxContents = null;
return contents;
}//end get
public synchronized boolean put(Object contents) {
if (boxContents != null)
return false;
boxContents = contents;
return true;
}//end put
}
Każde wystąpienie klasy Box ma pewną zawartość zmiennej wystąpienia, która
utrzymuje referencję do dowolnego obiektu. W rezultacie można włożyć obiekt do
pudełka (Box) wywołaniem metody put(), która zwróci false jeżeli pudełko jest
już pełne. Podobnie można wyjąć obiekt z pudełka wywołaniem metody get(), która
zwróci null jeżeli pudełko jest puste. Gdyby put() i get() nie były
synchronizowane i dwa wątki wykonywałyby te metody na tym samym obiekcie klasy
Box w tym samym czasie, wtedy wykonanie programu dwuwątkowego mogłoby przebiegać
w sposób przez nas nieoczekiwany.
Komputery w sieci Internet komunikują się ze sobą albo poprzez TCP (Transport
Control Protocol) albo poprzez User Datagram Protocol (UDP).
Uwaga. Nazwą TCP określa się zwykle protokół (zbiór zasad według których
odbywa się komunikacja w sieci komputerowej); TCP/IP (gdzie IP jest akronimem od
Internet Protocol) jest warstwowym zestawem protokołów i odpowiada
siedmiowarstwowemu modelowi ISO/OSI (Open Systems Interconnection) z warstwami:
fizyczną(1), łącza danych(2), sieciową(3), transportową(4), sesji(5),
prezentacji(6) i aplikacji(7). Np. Protokół IP jest implementacją warstwy (3),
zaś TCP i UDP implementują warstwę (4) modelu ISO/OSI, jak pokazano w tabeli
1.1.
Tabela 1.1
Zależność pomiędzy modelem ISO/OSI a TCP/IP
|
Warstwa
|
ISO/OSI
|
TCP/IP
|
|
1
|
Aplikacji |
Aplikacji: SMTP, HTTP, FTP, RPC, TELNET,RLOGIN, inne usługi |
|
2
|
Prezentacji |
|
|
3
|
Sesji |
DNS, LDAP |
|
4
|
Transportowa |
Transportowa: TCP/UDP, IGMP |
|
5
|
Sieciowa |
Międzysieciowa: IP, ICMP, protokoły rutingu |
|
6
|
Łącza danych |
Interfejs sieciowy: ARP, RARP, LLC 802.2, Ethernet 802.3 |
|
7
|
Fizyczna |
Fizyczna dla różnych mediów |
Pisząc program "sieciowy" w języku Java nie musimy deklarować, czy żądamy
komunikacji via TCP czy UDP, ponieważ zdefiniowane w pakiecie java.net klasy
dostarczają środki dla niezależnej od systemu komunikacji w sieci; klasy URL,
URLConnection, Socket i ServerSocket korzystają z TCP, a klasy DatagramPacket,
DatagramSocket i MulticastSocket korzystają z UDP.
Tym niemniej warto pamiętać, że powszechnie używane protokoły: Telnet i
wirtualnego terminala odpowiadają warstwie 5 i częściowo 6, zaś FTP (File
Transfer Protocol) odpowiada warstwom 6 i 7 modelu ISO/OSI. Zauważmy też, że
HTTP (Hypertext Transfer Protocol), jest aplikacją (warstwa (7) modelu ISO/OSI.
Gdy używamy HTTP do czytania z pewnego lokalizatora URL (Uniform Resource
Locator) danych z pliku w formacie HTML (HyperText Markup Language),
otrzymywane dane muszą wystąpić w tej samej kolejności, w której były
przesyłane, co wymaga niezawodnego kanału komunikacji punkt-punkt, jaki zapewnia
protokół połączeniowy TCP. Z kolei UDP, bezpołączeniowy protokół przesyłania
niezależnych pakietów danych (datagramów) pomiędzy dwoma aplikacjami w sieci,
jest wystarczający dla takich aplikacji, jak np. program ping, czy też programu
podającego aktualny czas.
Komputery w sieci są identyfikowane przez 32-bitowe adresy IP, zaś biegnące na
danym komputerze sieciowym procesy � poprzez 16-bitowe numery portów (port można
uważać za utrzymywaną przez system kolejkę danych, które mają być dostarczane do
danego procesu wykonującego pewien program). Na przykład programowi
wykorzystującemu protokół HTTP przydziela się zwyczajowo port o numerze 80.
Programy odbierają lub wysyłają dane poprzez wiązane z numerami portów gniazda
(sockets). Gniazdo, definiowane przez warstwę transportową modelu ISO/OSI,
reprezentuje punkt końcowy połączenia pomiędzy programami wykonywanymi na
komputerach sieciowych w systemie dostawca-odbiorca (klient-server); jest to
interfejs programowy umożliwiający aplikacjom dostęp do protokołów TCP i UDP i
wymianę danych poprzez sieć pracującą pod kontrolą protokołów TCP/IP. Program
serwera wykonywany na konkretnej maszynie ma przypisane do pewnego numeru portu
gniazdo, poprzez które "nasłuchuje" ewentualnego żądania nawiązania łączności
przez klienta. Jeżeli klient zna adres komputera sieciowego, na którym jest
wykonywany serwer oraz numer portu, do którego serwer jest dołączony, to może
przesłać takie żądanie. Serwer akceptuje połączenie, a następnie tworzy dla
klienta nowe gniazdo, związane z innym numerem portu, ponieważ na pierwotnym
gnieździe musi nadal prowadzić "nasłuch" żądań połączenia. Po stronie klienta,
gdy uzyska potwierdzenie połączenia, tworzone jest odpowiednie gniazdo (związane
z lokalnym numerem portu na maszynie klienta), poprzez które może prowadzić
komunikację z serwerem.
W pakiecie java.net klasy Socket i ServerSocket służą do komunikacji w oparciu
o protokół połączeniowy TCP, zaś klasy DatagramSocket, DatagramPacket oraz
MulticastSocket są wykorzystywane do komunikacji UDP. Klasy: URL oraz
URLConnection i URLEncoder służą do nawiązania łączności z World Wide Web (WWW).
Komunikacja, która wykorzystuje te klasy, reprezentuje wyższy poziom abstrakcji,
ponieważ korzystają one między innymi z implementacji gniazd.
Poniżej pokazano przykład aplikacji, która wykorzystuje klasy URL i
URLConnection dla dostępu do zasobu sieciowego (pliku tekstowego "abc") WWW
zlokalizowanego pod adresem www.task.gda.pl/~dark/.
import java.net.*;
import java.io.*;
//import java.util.*;
public class URLTest {
public static void main(String[] args) {
URL url;
try {
url = new URL("http://www.task.gda.pl/~dark/abc");
URLConnection uc = url.openConnection();
BufferedReader d = new BufferedReader(new
InputStreamReader(uc.getInputStream()));
//DataInputStream dis = new DataInputStream(uc.getInputStream());
String line = d.readLine();
System.out.println(line);
}//end try
catch (Exception e)
{ e.printStackTrace(); }
}// end main
}// end URLTest
W programie wykorzystano konstruktor URL(String). Klasa URL jest wyposażona w
cztery publiczne konstruktory:
-
public URL(String spec) throws MalformedURLException tworzy obiekt URL z
podanej reprezentacji obiektu klasy String, lub zgłasza wyjątek, jeżeli łańcuch
"spec" podaje nieznany protokół. Dla danego protokołu (http, file, ftp) jest
przyjmowany domyślny numer portu.
Numer portu można też podać jawnie, np.
http://www.task.gda.pl:80/~dark/abc
-
public URL(URL context, String spec) throws MalformedURLException tworzy
obiekt URL przez parsing (wydzielenie) specyfikacji "spec" w obrębie zadanego
kontekstu.
-
public URL(String protocol, String host, int port, String file) throws
MalformedURLException tworzy obiekt URL z podanego protokołu, nazwy komputera,
numeru portu i nazwy pliku.
-
public URL(String protocol, String host, String file) throws
MalformedURLException tworzy absolutny obiekt URL z podanego protokołu, nazwy
komputera i nazwy pliku; przyjmuje port domyślny dla danego protokołu.
Bibliografia
[1] Cargill T. An Overview of Java for C++ Programmers. C++ Report, vo. 8,
No. 2, pp. 46-49, 1996.
[2] Lorenz M. Java as an Object-Oriented Language. SIGS Books & Multimedia, New
York 1996.
[3] Martin R. C++ and Java: A Critical Comparison. C++ Report, vo. 9, No. 1, pp.
42-49, 1997.
[4] Jain P. and Schmidt D.C. Experiences Converting a C++ Communication Software
Framework to Java. C++ Report, vo. 9, No. 1, pp. 51-66, 1997.
[5] Gosling J., Joy B, Steele G. The Java Language Specification. Addison-Wesley,
1996, http://java.sun.com/docs/books/jls/.
[6] The Java Tutorial. http://java.sun.com/docs/books/tutorial/, 1998.
[7] JDK1.1.6 Documentation. http://java.sun.com/products/jdk/1.1/docs/, 1998.
[8] Servlet Tutorial. http://www.task.gda.pl/java/ServletTutorial.html, 1998.