JAVA - kurs (cz.1)

 

JAVA  -  kurs  (cz.1)


SPIS TEMATÓW


1. Język Java
  1.1. Elementarny program: tekst źródłowy, kompilacja, interpretacja
  1.2. Klasy: definicja, dziedziczenie, tworzenie obiektów
  1.3. Interfejsy
  1.4. Pliki źródłowe i pakiety
  1.5. Polimorfizm
  1.6. Obsługa wyjątków
  1.7. Zarządzanie pamięcią
  1.8. Współbieżność
  1.8.1 Synchronizacja wątków
  1.9. Obiekty sieciowe



W drugiej część przeczytacie

2. Aplety - programy Javy na stronach WWW
3. Standardowe klasy Javy
4. Servlety - programy Javy na serwerze WWW
5. Podsumowanie
6. Programy - ćwiczenia



1. Język Java

Początki języka Java sięgają roku 1990, gdy Bill Joy napisał dokument pod tytułem "Further", w którym sugerował inżynierom Sun Microsystems stworzenie obiektowego środowiska w oparciu o C++. Dokument ten miał pewien wpływ na twórców projektu Green (James Gosling, Patrick Naughton i Mike Sheridan). W roku 1991 w ramach projektu Green opracowano w języku C kompilator oraz interpretator wynalezionego przez Goslinga języka OAK (Object Application Kernel), który miał być narzędziem do oprogramowania "inteligentnych" konsumenckich urządzeń elektronicznych. Ponieważ nazwa "OAK" okazała się zastrzeżona, zmieniono ją na "Java".
Obecnie należy raczej mówić o środowisku Java, na które składa się:

  1. Obiektowy język Java, którego składnia wykazuje znaczne podobieństwo do składni języka C++. Nazwa pliku z programem źródłowym w języku Java, ma postać "nazwa.java", gdzie "nazwa" musi być nazwą zdefiniowanej w tym pliku klasy publicznej.
     

  2. Kompilator, który przetwarza program "nazwa.java" na tak zwany B-kod (bytecode, J-code), zapisywany automatycznie w plikach z rozszerzeniem nazwy".class". B-kod jest przenośną postacią programu, która może być zinterpretowana przez odpowiednią maszynę wirtualną, to jest "urządzenie logiczne", na którym będzie wykonywany program binarny.
     

  3. Specyfikacje maszyny wirtualnej Java (JVM Java Virtual Machine) i plików klas. JVM można uważać za abstrakcyjny komputer, który wykonuje programy, zapisane w plikach z rozszerzeniem nazwy ".class". Maszyna wirtualna może być implementowana na rzeczywistych komputerach na wiele sposobów, na przykład jako interpretator wbudowany w przeglądarkę WWW (np. Netscape), lub jako oddzielny program, który interpretuje pliki "nazwa.class". Może to być także implementacja polegająca na przekształceniu tuż przed rozpoczęciem fazy wykonania pliku z B-kodem na program wykonalny, specyficzny dla danej maszyny. Mechanizm ten można określić jako tworzenie kodu wykonalnego w locie (ang. Just-In-Time, np. kompilator JIT firmy Symantec). Interpretatory B-kodu, tj. różne maszyny wirtualne, są także często napisane w języku Java.
     

  4. Biblioteka Javy. Środowisko języka Java zawiera bogatą bibliotekę, a w niej zbiór składników dla prostego, niezależnego od platformy graficznego interfejsu użytkownika.


1.1. Elementarny program: tekst źródłowy, kompilacja, interpretacja

Java wprowadza swoistą terminologię dla swoich konstrukcji syntaktycznych i jednostek (modułów) kompilacji. Programem w języku Java jest aplikacja (application) lub aplet (applet). Aplikacja jest programem samodzielnym, zaś aplet jest programem wbudowanym (np. w przeglądarkę WWW). Każda aplikacja musi zawierać dokładnie jeden moduł źródłowy nazywany modułem głównym aplikacji, którego klasa publiczna zawiera publiczną funkcję klasy (funkcje takie są poprzedzane słowem kluczowym static) main.
Tekst źródłowy najprostszego programu może mieć postać:

public
class Hello {
    public static void main(String args[])
    {
     System.out.print("Hello, World!\n");
    }
}

Dla skompilowania powyższego programu jego tekst źródłowy należy umieścić w pliku o nazwie Hello.java. Zakładając, że dysponujemy systemem JDK z kompilatorem javac, program skompilujemy poleceniem:

    javac Hello.java

Udana kompilacja wygeneruje plik z B-kodem o nazwie Hello.class, zawierający sekwencję instrukcji dla interpretatora JVM. Kod ten wykonujemy przez wywołanie interpretatora o nazwie java poleceniem:

   
java Hello


Interpretator wyszuka plik o nazwie Hello.class, ustali, czy klasa Hello zawiera publiczną metodę statyczną main i wykona instrukcje zawarte w bloku main. Zauważmy przy okazji, że w języku Java wszystkie stałe, zmienne i funkcje są elementami składowymi klas; nie ma wielkości globalnych, definiowanych poza klasą. Ponadto nie deklaruje się metod (funkcji) składowych jako rozwijalnych (inline) bądź nie decyzja należy do kompilatora.

W przykładowym programie do metody main jako parametr jest przekazywana (z wiersza rozkazowego) tablica obiektów (łańcuchów) klasy String; metoda main nie zwraca wyniku, zaś wartością parametru arg[0] jest pierwszy po nazwie programu spójny ciąg znaków. Ciało main zawiera jedną instrukcję

   
System.out.print("Hello, World!\n");


(W języku Java każda instrukcja kończy się średnikiem, który pełni rolę symbolu terminalnego).

Słowo System jest nazwą klasy w standardowym środowisku języka. Klasa System zawiera statyczny obiekt składowy typu PrintStream o nazwie out; wywołanie System.out oznacza pisanie do standardowego strumienia wyjściowego. Klasa PrintStream zawiera szereg przeciążeń metody o nazwie print; jedno z nich przyjmuje parametr String.
Kompilator automatycznie tłumaczy literał stały "Hello, World\n" na odpowiedni obiekt klasy String; odniesienie (referencja) do tego obiektu jest przekazywane do metody System.out.print(). Metoda print generuje jeden wiersz wyjściowy i powraca do metody main, która kończy wykonanie.

1.2. Klasy: definicja, dziedziczenie, tworzenie obiektów

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.

1.3. Interfejsy

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.

 

1.4. Pliki źródłowe i pakiety

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.

1.5. Polimorfizm

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

1.6. Obsługa wyjątków

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


1.7. Zarządzanie pamięcią

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.

1.8. Współbieżność

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

1.8.1 Synchronizacja wątków

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.

1.9. Obiekty sieciowe

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.

 

C.D.  w drugiej części kursu


D.F.



Copyright © by MiniMax 1997/2007. All rights reserverd!