Język Java: Podstawy, Programowanie, Zastosowania
Java
3
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. Jeżeli plik “nazwa.java” zawiera definicje wielu
klas, a wśród nich jedną klasę publiczną, to “nazwa” musi być nazwą tej 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.
Rysunek 4-1 ilustruje usytuowanie środowiska programowego języka Java, posadowionego na
dowolnej platformie sprzętowo- programowej komputera (platforma sprzętowo-programowa oznacza
sprzęt komputera i jego system operacyjny).
Program w języku Java
Java API
Maszyna wirtualna (JVM)
Platforma sprzętowo-programowa
a)
b)
Środowisko wykonawcze
Klasa System
MyProgram.java
Rys. 1-1. Usytuowanie systemu Java
Pokazany w części a) rysunku blok Java API (Application Programming Interface) reprezentuje klasy,
interfejsy i obiekty wchodzące w skład aktualnej maszyny wirtualnej, którą zwykle określa się jako
platformę języka Java (Java Platform). Umożliwiają one programom języka Java na dostęp do
zasobów komputera. Może to być dostęp (względnie) systemowo niezależny (implementowany przez
klasę System w pakiecie JDK) i na dostęp systemowo zależny (implementowany przez klasę Runtime,
reprezentującą środowisko wykonawcze w pakiecie JDK), jak pokazano w części b) rysunku 4-1.
Języki obiektowe
4
Tak więc programy użytkownika można kompilować na dowolnej platformie sprzętowo-programowej,
na której posadowiono kompilator języka Java. Otrzymany w wyniku kompilacji B-kod można
traktować jako zbiór instrukcji kodu dla dowolnej implementacji maszyny wirtualnej, jak pokazano na
rysunku 4-2.
PC - Windows 95/NT
SUN - Solaris
MyProgram.java
Edytor
Kompilator
Interpretator
Interpretator
MyProgram.class
Rys. 1-2. Przetwarzanie programów 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 zawiera publiczną funkcję klasy (funkcje takie są poprzedzane słowem
kluczowym static) main. Tekst źródłowy najprostszego programu może mieć postać:
//plik Hello.java
public class Hello {
public static void main(String args[])
{
System.out.print("Hello, World!\n");
} //end main
} // end Hello
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 (typem
zwracanym jest void), 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
Java
5
pisanie do standardowego strumienia wyjściowego. Klasa PrintStream zawiera szereg przeciążeń
metody o nazwie print; jedno z nich przyjmuje parametr typu String. Kompilator automatycznie
tłumaczy literał stały "Hello, World\n" na odpowiedni obiekt klasy String; odnośnik (referencja) do
tego obiektu jest przekazywana 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).
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.
Podklasa klasy abstrakcyjnej zawierającej metody abstrakcyjne może podawać definicje metod
abstrakcyjnych. Podklasa podająca te definicje staje się klasą konkretną, tj. może mieć wystąpienia.
Każda klasa, która odziedziczy metodę abstrakcyjną, ale nie dostarczy jej implementacji, sama staje
się klasą abstrakcyjną, a jej definicja także musi być poprzedzona słowem kluczowym abstract.
)
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) {/* ... */ }).
Języki obiektowe
6
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
} // end aMethod
} // end aSillyClass
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);
} // end aMethod
} // end ASillerClass
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.
Jeżeli dana klasa nie zawiera deklaracji konstruktorów, to kompilator dostarcza konstruktor domyślny
z pustym wykazem argumentów, który w swoim bloku wywołuje konstruktor super() jej bezpośredniej
nadklasy. Weźmy dla ilustracji definicję klasy Point:
public class Point { int x, ,y; }
Jest ona równoważna definicji
public class Point { int x, ,y; public Point() { super(); } }
z niejawnym wywołaniem dostarczanego przez kompilator konstruktora superklasy, od której
bezpośrednio dziedziczy klasa Point.
Podobne, niejawne wywołania konstruktora super() są wykonywane w drzewach dziedziczenia.
Rozpatrzmy następujący program:
//plik Super1.java
class Point { int x,y; Point() { x=1;y=2; } }
class CPoint extends Point { public int color = 0xFF00FF; }
public class Super1 {
public static void main(String args[]) {
CPoint cp = new CPoint();
System.out.println("cp.color= " + cp.color);
System.out.println("cp.x= " + cp.x);
}//end main
}//end Super1
Instrukcja CPoint cp = new CPoint(); tworzy nowe wystąpienie klasy CPoint. Najpierw
jest przydzielany obszar w pamięci dla obiektu cp, aby mógł przechowywać wartości x oraz y, po
Java
7
czym pola te są inicjowane do wartości domyślnych (tutaj zero dla każdego pola). Następnie jest
wołany konstruktor Cpoint(). Ponieważ klasa CPoint nie deklaruje konstruktorów, kompilator
automatycznie dostarczy konstruktor o postaci CPoint(){super();}, który wywoła konstruktor
klasy Point bez argumentów, tak jakby zamiast super() napisano:
Point(){super();x=1;y=2; }.
W hierarchii dziedziczenia konstruktor super() może być wywoływany jawnie, jak w przykładzie
poniżej:
//plik Super2.java
class Point { int x,y; Point(int x, int y)
{ this.x = x; this.y = y; } }
class CPoint extends Point {
static final int WHITE = 0, BLACK = 1;
int color;
CPoint(int x, int y) { this(x,y,WHITE); }
CPoint(int x, int y, int color) { super(x,y); this.color=color; }
}
public class Super2 {
public static void main(String args[]) {
int a = 10, b = 20;
CPoint cp = new CPoint(a,b);
System.out.println("cp.color= " + cp.color);
System.out.println("cp.x= " + cp.x);
}//end main
}//end Super2
Zmienna this w definicji konstruktora klasy Point jest odnośnikiem (referencją), identyfikującym
obiekt, na rzecz którego wywołuje się konstruktor. Tak więc lewa strona instrukcji this.x = x
identyfikuje pole x obiektu klasy Point, zaś prawa strona jest wartością argumentu x, którą inicjuje
się to pole. Natomiast słowo kluczowe this w definicji dwuargumentowego konstruktora klasy
CPoint służy do wywołania konstruktora trójargumentowego tej klasy w instrukcji
this(x,y,WHITE).
Zatem w instrukcji CPoint cp = new CPoint(a,b); konstruktor CPoint(int,int) wywołuje ze swojego
bloku (instrukcja this(x,y,WHITE);) drugi konstruktor, CPoint(int,int,int), dostarczając mu argument
WHITE. Drugi konstruktor woła konstruktor superklasy, przekazuje mu współrzędne x oraz y, a
następnie inicjuje pole color wartością WHITE.
)
Uwaga. instrukcja { this(argumenty);musi być pierwszą instrukcją w ciele konstruktora lub metody;
to samo dotyczy instrukcji super(argumenty);.
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);.
Gdyby w klasie ASillyClass zdefiniować statyczną funkcję klasy, np.
public static void bMethod(){/*instrukcje */}
to w takiej funkcji nie istnieje odnośnik this, a zatem żadna z jej instrukcji nie może się bezpośrednio
odwołać do niestatycznej składowej x; odwołanie byłoby możliwe jedynie przez zadeklarowanie
zmiennej odnośnikowej do klasy ASillyClass i zainicjowanie jej odnośnikiem do utworzonego za
pomocą operatora new nowego obiektu, jak pokazano w poniższej sekwencji instrukcji:
ASillyClass ob = new ASillyClass();
System.out.println(ob.x);.
1.3. Interfejsy
Konstrukcja o postaci
interface nazwa {
/* Deklaracje metod i definicje stałych */
}
Języki obiektowe
8
jest w języku Java typem definiowanym przez użytkownika. Deklaracja metody składa się z
sygnatury (sygnatura metody zawiera typ zwracany, nazwę metody 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żywania 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 1-3.
Java
9
Boat
Plane
SeaPlane
Vehicle
interface
PlaneLike
interface
BoatLike
Vehicle
Boat
Plane
SeaPlane
a)
b)
Rys. 1-3. a) Graf dziedziczenia mnogiego dla SeaPlane.
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 4-3):
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
Języki obiektowe
10
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 c:\mike (Win’95 DOS) umieszczono główny plik źródłowy
aplikacji Student.java o postaci:
import myprog.pakiet1.HiGrade;
import myprog.pakiet1.LoGrade;
Powyższe dwie deklaracje importu można zastapić jedną:
import myprog.pakiet1.*
public
class Student {
int i = 10;
public static void main(String args[])
{
System.out.println("Hello, I am here!");
HiGrade highgrade = new HiGrade();
highgrade.printgrade();
LoGrade lowgrade = new LoGrade();
lowgrade.printgrade();
}
}
Plik zawiera definicję klasy Student, poprzedzoną deklaracją importu klas HiGrade i LoGrade. Plik
ten może zostać skompilowany wywołaniem kompilatora javac z katalogu nadrzędnego w stosunku
do katalogu mike\myprog\pakiet1; jeżeli plik Student.java umieszczono w katalogu mike, to
wywołanie będzie miało postać: javac Student.java. Jeżeli pliki HiGrade.java i LoGrade.java
mają postać:
Java
11
package myprog.pakiet1;
public class HiGrade {
int i = 10;
public void printgrade()
{
System.out.println("My grades are higher than " + i);
}
}
class Empty{}
oraz
package myprog.pakiet1;
public class LoGrade {
int i = 3;
public void printgrade()
{
System.out.println("My grades are lower than " + i);
}
}
to zostaną utworzone cztery pliki wynikowe: Student.class w katalogu javaprog oraz HiGrade.class,
LoGrade.class i Empty.class, a wywołanie interpretatora java Student spowoduje wyprowadzenie
na ekran napisu:
Hello, I am here!
My grades are higher than 10
My grades are lower than 3
)
Uwaga. Deklaracja importu nie oznacza włączania do pliku Student.java tekstu zawartego w
plikach HiGrade.java i LoGrade.java. Natomiast pozwala ona użytkownikowi klasy Student używać
skrótowych nazw: np. zamiast pisać myprog\pakiet1\HiGrade highgrade = new
mike\myprog\pakiet1\HiGrade(); mogliśmy napisać krótko: HiGrade highgrade = new HiGrade();.
Gdybyśmy chcieli używać również klasy Empty (lub innych klas pakietu pakiet1), 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.
Zauważmy, że gdyby umieścić w pliku LoGrade.java definicję pakietowej funkcji wystąpienia, na
przykład
void msg() { System.out.print("This is a package method\n"); }
to funkcję tę można byłoby wywołać z ciała klasy HiGrade, na przykład w funkcji pf:
public void pf() {
LoGrade lg = new LoGrade();
lg.msg();
}
Fukcja pf jest publiczna, zatem jest dostępna nie tylko dla klas pakietu myprog.pakiet1, lecz także
poza tym pakietem. Z funkcji main klasy Student można ją, przykładowo, wywołać poleceniem:
highgrade.pf();.
Zauważmy też, że deklarując msg jako funkcję klasy LoGrade
static void msg(){System.out.print("This is a package method\n");}
moglibyśmy ją wywołać z ciała klasy HiGrade bez tworzenia referencji do obiektu klasy LoGrade, np.
public void pf() { LoGrade.msg(); }
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.
Języki obiektowe
12
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. Odnośnik (referencja)
b1 do klasy Base jest najpierw inicjowana obiektem klasy Base, a więc instrukcja b1.msg();
wywoła metodę tej klasy. Następnie odnośnikowi 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.5. Klasy zagnieżdżone
Klasy, jak już powiedziano, mogą należeć do pakietów nazwanych lub nienazwanych. Począwszy od
wersji 1.1 języka Java wprowadzono możliwość definiowania klas wewnętrznych (inner classes) jako
elementów składowych innych klas; można je definiować albo lokalnie wewnątrz ciała klasy
zewnętrznej, albo (anonimowo) jako wyrażenie w bloku. Takie zagnieżdżone klasy posiadają kilka
własności, które czynią je użytecznymi:
• Nazwy klasy wewnętrznej nie można użyć na zewnątrz jej zasięgu, za wyjątkiem nazwy
kwalifikowanej. Pomaga to w strukturalizacji klas w obrębie pakietu.
Java
13
• Kod klasy wewnętrznej może wykorzystywać proste nazwy z zasięgów otaczających, w tym
zarówno składowe klasy, jak i składowe wystąpienia klas otaczających oraz zmienne lokalne
otaczających bloków.
Klasy wewnętrzne są elementami kombinacji struktury blokowej i programowania opartego o klasy;
wprowadzono je po raz pierwszy w języku Beta. Wykorzystanie struktury blokowej oraz klas
wewnętrznych ułatwia programiście wiązać ze sobą obiekty Javy, ponieważ klasy mogą być
definiowane w pobliżu obiektów, którymi manipulują, i mogą bezpośrednio używać nazw, których
potrzebują. Ponadto programista może definiować klasę wewnętrzną jako statyczny element dowolnej
klasy otaczającej. Jednak klasa wewnętrzna nie może deklarować żadnej ze swoich składowych ze
słowem kluczowym static, ponieważ ciało klasy wewnętrznej jest w zasięgu klasy otaczającej. W
rezultacie zmienne statyczne dla klasy wewnętrznej muszą być umieszczane w klasie otaczającej. Z
tego samego powodu nie można w klasie wewnętrznej deklarować statycznych inicjatorów dla pól
klasy.
Klasy zewnętrzne oraz statyczne klasy wewnętrzne nazywa się klasami wysokiego poziomu. Różnią
się one od niestatycznych klas wewnętrznych tym, że mogą używać bezpośrednio jedynie swoje
własne zmienne wystąpienia.
Jeżeli klasa wewnętrzna posiada nazwę, może być deklarowana ze słowami kluczowymi private,
protected, final, lub abstract. Natomiast klasy anonimowe są prywatne w bloku, w którym są
zadeklarowane, ponieważ nie mogą być wykorzystane na zewnątrz tego bloku.
Zagnieżdżanie klas w ten właśnie sposób pozwala dowolnej klasie wysokiego poziomu – dla logicznie
powiązanych klas poziomu niższego – uzyskać podobną do pakietu organizację, w której wszystkie
klasy mają pełny dostęp do pól prywatnych.
W podanym niżej przykładzie klasa wewnętrzna została zadeklarowana jako klasa niestatyczna
Inner z własnym konstruktorem i własną zmienną składową y.
//Plik Outer1.java
public class Outer1 {
public int x;
public Outer1(int i) { x = i; }
class Inner {
public int y;
public Inner(String s, int j) {
y = j;
System.out.println(s+j);
};
}//end Inner
public static void main(String args[]) {
Outer1 ref1 = new Outer1(10);
System.out.println("ref1.x = " + ref1.x);
Outer1.Inner ref2 = ref1.new Inner("ref2.y = ", 20);
System.out.println("ref2.y = " + ref2.y);
}
}//end Outer1
Wprowadzenie klas zagnieżdżonych nie spowodowało zmian w przetwarzaniu plików z B-kodem
przez maszynę wirtualną Javy, tj. przez interpretator, ani w standardowych bibliotekach klas, a nowa
cecha języka została uwzględniona w nowych wersjach kompilatorów. Uwzględniono ją w ten sposób,
że nazwy klas wewnętrznych są przekształcane tak, aby uniknąć konfliktów z innymi nazwami w
różnych zasięgach. Nazwy są kodowane przez kompilator, który bierze ich postać źródłową,
kwalifikuje je kropkami, po czym zmienia każdą kropkę po nazwie klasy na znak dolara (‘$’). Tak
więc po kompilacji pliku źródłowego “Outer1.java” otrzymamy dwa pliki z B-kodem: “Outer1.class”
oraz “Outer1$Inner.class”. Oczywiście interpretacji poddamy plik “Outer1.class”.
Niestatyczną klasę wewnętrzną wykorzystuje się wtedy, gdy tworzony jest obiekt klasy zewnętrznej;
jak pokazano wyżej, argumentem operatora new jest jest konstruktor klasy wewnętrznej, wywoływany
na odnośniku do obiektu klasy zewnętrznej.
Następny przykład ilustruje deklarację statycznej klasy wewnętrznej:
Języki obiektowe
14
//plik Outer2.java
public class Outer2 {
public int x;
public Outer2(int i) { x = i; }
static class Inner {
public int y;
public Inner(int j) { y = j; };
}//end Inner
public static void main(String args[]) {
Outer2 ref1 = new Outer2(10);
System.out.println("ref1.x = " + ref1.x);
Outer2.Inner ref2 = new Outer2.Inner(20);
System.out.println("ref2.y = " + ref2.y);
}
}//end Outer2
W tym przypadku argumentem operatora new jest jest konstruktor klasy wewnętrznej, wywoływany
na składowej statycznej (Outer2.Inner) klasy zewnętrznej. Kompilator, podobnie jak w
poprzednim przykładzie, utworzy dwa pliki z B-kodem: “Outer2.class” oraz “Outer2$Inner.class”.
Jeżeli nie ma potrzeby tworzenia obiektu klasy zewnętrznej, wówczas zarówno klasę wewnętrzną, jak
i funkcje składowe klasy zewnętrznej można zadeklarować jako statyczne. Konstrukcje tego typu
wykorzystywane są w Javie prawie wszędzie tam, gdzie klasa wewnętrzna implementuje interfejs.
Ilustracją tego stwierdzenia jest kolejny przykład.
//Plik Outer3.java
interface InnerInt{
String wyswietl();
}//end of InnerInt interface
public class Outer3 {
public static int x=10;
static class Inner implements InnerInt{
private int y;
public Inner(int j) {
y=j;
System.out.println(wyswietl());
}//end of ctor
public String wyswietl(){
return (new String("Inner.y = "+y));
}
}//end Inner
public static InnerInt foo(int i){
return (new Inner(i));
}
public static void main(String args[]) {
System.out.println("Outer3.x = " + Outer3.x);
Outer3.foo(20);
}
}//koniec public class Outer3
W bloku main publiczna, statyczna funkcja foo() klasy zewnętrznej, wywoływana z argumentem
20, zwraca odnośnik typu interfejsowego InnerInt, inicjowany odnośnikiem do tworzonego w
instrukcji return obiektu klasy Inner. Mogliśmy tak uczynić, ponieważ klasa Inner jest nie
tylko odrębnym typem, lecz także implementuje typ InnerInt. W rezultacie kompilator utworzy
trzy pliki z B-kodem: “Outer3.class”, “Outer3$Inner.class” oraz “InnerInt.class”.
Java
15
Ponieważ typ Inner jest wykorzystywany w definicji klasy zewnętrznej tylko jeden raz, można, dla
uzyskania bardziej zwięzłego kodu, zrezygnować z wprowadzania jego nazwy do przestrzeni nazw
programu, a implementację interfejsu InnerInt umieścić bezpośrednio w wyrażeniu new instrukcji
powrotu z funkcji foo(). Konstrukcja taka, będąc implementacją interfejsu, jest oczywiście klasą;
ponieważ jednak nie występuje tutaj nazwa klasy, nazywamy ją klasą anonimową. Poniżej
zaprezentowano odpowiedni kod, w którym blok, zaczynający się nawiasem klamrowym po
InnerInt() jest ciałem klasy anonimowej, faktycznie identycznym z ciałem klasy Inner z
poprzedniego przykładu.
//plik Outer4.java
interface InnerInt{
StringBuffer wyswietl();
}//end InnerInt
public class Outer4 {
public static int x=10;
public static InnerInt foo(final int i) {
return new InnerInt() {
private int y = i;
public StringBuffer wyswietl() {
return (new StringBuffer("Outer4.foo(20).wyswietl()= " +y));
}//end wyswietl()
};//end anonymous
}//end foo
public static void main(String args[]) {
System.out.println("Outer4.x = " + Outer4.x);
System.out.println(Outer4.foo(20).wyswietl());
}//end main()
}//end Outer4
Zwróćmy uwagę na ostatnią instrukcję w funkcji main(). Argument metody println() jest wyrażeniem
otrzymanym w wyniku wywołania na klasie Outer4 funkcji foo(), która zwraca obiekt typu InnerInt.
Na obiekcie tym jest wywoływana funkcja wyswietl(), która zwraca wynik typu
StringBuffer, a więc typu akceptowanego przez metodę println().
Zdefiniowana tutaj klasa anonimowa ma następujące własności:
• Może mieć inicjatory pól, ale nie może mieć konstruktora.
• Jeżeli klasa anonimowa została wyprowadzona z interfejsu I, to faktyczną jej superklasą jest
Object, a klasa tylko implementuje interfejs I, bez żadnych rozszerzeń.
Przekształcanie nazw przez kompilator spowoduje teraz utworzenie następujących plików z B-kodem:
“Outer4.class”, “Outer4$1.class” oraz “InnerInt.class”. Plik “Outer4$1.class” odpowiada klasie
anonimowej. W jego nazwie liczba 1 (numer klasy anonimowej) została dodana przez kompilator dla
otrzymania unikatowej nazwy, która zostanie przekazana do konsolidatora.
Istnienie klas zagnieżdżonych wymaga pewnej zmiany w nazwach kwalifikowanych przy
definiowaniu zmiennych typu klasy wewnętrznej oraz hierarchii dziedziczenia. Istotne zmiany to:
– Inicjowanie zmiennych odnośnikowych typu klasy wewnętrznej słowem kluczowym this,
odpowiadającym bieżącemu obiektowi klasy zewnętrznej.
– Kwalifikowanie dziedziczenia od klasy wewnętrznej jej nazwą, umieszczaną po nazwie klasy
zewnętrznej i kropce.
Ilustracją tego sposobu kwalifikacji nazw jest podany niżej przykład.
class Vehicle {
class Wheel {
String hubcapType;
float radius;
}//end class Wheel
Wheel leftWheel = this. new Wheel();
Języki obiektowe
16
Wheel rightWheel = this. new Wheel();
Wheel extra;
static void thirdWheel(Vehicle car) {
if (car.extra == null) {
car.extra = car. new Wheel();
}//end if
}//end thirdWheel()
public static void main(String args[]) {
System.out.println("Vehicle = " );
}//end main()
}//end class Vehicle
//Koło ze szprychami
class WireRimWheel extends Vehicle.Wheel {
WireRimWheel(Vehicle car, float wireGauge) {
car. super();
}//end of constructor WireRimWheel()
}//end class WireRimWheel
class Auto extends Vehicle { }
W wyniku kompilacji zostaną utworzone cztery pliki z B-kodem: “Vehicle.class”,
“Vehicle$Wheel.class”, “WireRimWheel.class” oraz “Auto.class”.
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órej fraza catch 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 – odnośniki do tych klas.
Blok catch należy traktować jako ciało procedury obsługi wyjątku należącego do klasy argi.
Java
17
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:
//plik TestEx.java
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 TestEx
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: " +
Języki obiektowe
18
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 Windows’95)
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
Java
19
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 priorytet 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.
)
Uwaga. Ze względu na zdarzające się błędy przy blokowaniu i zwalnianiu zasobów oraz możliwe
zakleszczenia wątków w obowiązującej obecnie wersji JDK1.2 nie zaleca się stosowania metod
resume(), stop() i stop(Throwable obj).
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.
//plik Thread1.java
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
Języki obiektowe
20
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:
//plik TwoThreads
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.9 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ą, objętą 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:
//plik TestSynchro
Java
21
class TestSynchro {
public static void main(String[] args) {
TestSynchro t = new TestSynchro();
synchronized(t) {
synchronized(t) {
System.out.println("made it!");
}//end internal synchronized statement
}//end external synchronized statement
}//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++;
}//end synchronized method
}//end class Test
jest równoważny segmentowi
class BumpTest {
int count;
void bump(){
synchronized (this){
count++;
}//end synchronized statement
}//end bump()
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:
//plik Box.java
Języki obiektowe
22
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.10. 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
7
Aplikacji
Aplikacji: SMTP, HTTP, FTP, RPC, TELNET,
6
Prezentacji
RLOGIN, inne usługi
5 Sesji
DNS,
LDAP
4
Transportowa
Transportowa: TCP/UDP, IGMP
3 Sieciowa
Międzysieciowa: IP, ICMP, protokoły rutingu
2
Łącza danych
Interfejs sieciowy: ARP, RARP, LLC 802.2, Ethernet 802.3
1
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
Java
23
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/java/.
//plik URLTest.java
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/java/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/java/abc.
Języki obiektowe
24
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.
2. Aplety - programy Javy na stronach WWW.
Aplet jest niewielkim programem Javy przeznaczonym do uruchamiania w obrębie innej aplikacji -
przeglądarki WWW lub Java Applet Viewer. Najprostszy aplet drukujący jedno zdanie na ekranie
może mieć następującą postać:
HelloWorld.java
import java.applet.*;
import java.awt.*;
public class HelloWorld extends Applet {
public void init() {
add(new Label("Hello World!"));
}
}
Podobnie jak aplikację, kompilujemy aplet poleceniem:
javac
HelloWorld.java
w wyniku czego otrzymamy plik ze skompilowaną klasą o nazwie HelloWorld.class.
Następnie tworzymy prostą stronę HTML zawierającą znacznik <applet> wywołujący aplet Javy i
otwieramy tę stronę przeglądarką WWW.
HelloWorld.html
<html>
<head>
<title>HelloWorld</title>
</head>
<body>
<h1>Aplet HelloWorld</h1>
<applet code=HelloWorld.class width=200 height=50>
</applet>
</body>
</html>
Wynik działania apletu możemy również obejrzeć używając przeglądarki apletów dostarczanej przez
Suna razem ze środowiskiem JDK:
appletviewer
HelloWorld.html
Każdy aplet tworzony jest jako podklasa klasy java.applet.Applet, dostarczającej interfejs do
środowiska, w obrębie którego jest on uruchamiany.
Java
25
java.awt.Component
java.lang.Object
java.awt.Container
java.awt.Panel
java.applet.Applet
Rys. 2-1. Hierarchia dziedziczenia dla klasy Applet.
W cyklu życia apletu występują cztery istotne zdarzenia. Gdy one następują, wywoływane są
odpowiednie metody klasy Applet, które mogą być przesłonięte przez metody zdefiniowane w jej
podklasie, jak w poniższym przykładzie:
public class MyApplet extends Applet {
. . .
public void init() { . . . }
public void start() { . . . }
public void stop() { . . . }
public void destroy() { . . . }
. . .
}
Metody te są wywoływane przez środowisko, w którym uruchamiany jest aplet, w momentach
wystąpienia następujących zdarzeń:
init - po załadowaniu apletu (lub jego przeładowaniu) w celu jego zainicjowania,
start - w momencie uruchamiania apletu, po jego załadowaniu lub po ponownym odwiedzeniu
strony zawierającej aplet,
stop - w momencie zatrzymywania pracy apletu, gdy opuszczana jest strona lub następuje wyjście z
przeglądarki,
destroy - przed wyjściem z przeglądarki.
init
start
stop
destroy
Rys. 2-2. Cykl życia apletu.
Języki obiektowe
26
Aplet nie musi przesłaniać wszystkich z tych metod, może tylko niektóre albo żadnej. W metodzie
init powinno się zamieszczać kod, który normalnie powinien być zawarty w konstruktorze klasy.
Jednak w momencie wykonywania konstruktora klasy apletu nie ma gwarancji, że jest już
zdefiniowane całe jego środowisko. Dlatego takie zadania jak na przykład utworzenie wątków apletu
czy jednokrotne załadowanie obrazów wykorzystywanych przez aplet powinny być zawarte właśnie w
metodzie init. W metodach start i stop można na przykład odpowiednio uruchamiać i
zatrzymywać animację tak, żeby w czasie gdy użytkownik nie ogląda strony z apletem niepotrzebnie
nie zużywać zasobów komputera.
Aplet dziedziczy metody interfejsu użytkownika z nadrzędnych klas AWT (Abstract Window Toolkit)
Component, Container i Panel.
Z klasy Component dziedziczy metody paint i update, które odpowiadają za graficzną
reprezentację apletu na stronie przeglądarki. Metody te aplet może przesłonić:
class MyApplet extends Applet {
. . .
public void paint(Graphics g) { . . . }
. . .
}
Aplet jest podklasą klasy Container, może więc zawierać inne obiekty klasy Component, takie
jak klawisze, etykiety, listy wyboru itd. Jak w innych obiektach klasy Container wzajemne
rozłożenie komponentów kontrolowane jest poprzez klasę LayoutManager.
Z klasy Component aplet dziedziczy również metody obsługi zdarzeń. Zdarzenia są generowane
głównie przez komponenty interfejsu graficznego użytkownika. Komponenty będące źródłem zdarzeń
rejestrują jedną lub więcej klas delegowanych do obsługi zdarzenia:
someComponent.addActionListener(instanceOfMyClass);
W deklaracji klasy obsługującej zadarzenie specyfikujemy, że implementuje ona przynajmniej jeden z
interfejsów pochodnych od EventListner (np. ActionListner, MouseListner,
KeyListner, WindowListner itd.):
public class MyClass implements ActionListener {
a w jej definicji implementujemy metodę obsługującą zdarzenie:
public void actionPerformed(ActionEvent e) {
... // kod reagujący na zdarzenie typu ActionEvent
}
Każde źródło zdarzeń może mieć wiele klas delegowanych do jego obsługi. Jedna klasa może też
obsługiwać wiele różnych źródeł zdarzeń.
Aplety są uruchamiane w środowisku przeglądarki, która narzuca im pewne ograniczenia związane z
zachowaniem bezpieczeństwa. Różnią się one nieco pomiędzy różnymi przeglądarkami. Aplet
ściągnięty do przeglądarki poprzez sieć:
• nie może czytać i pisać do plików znajdujących się na komputerze, który go wykonuje,
• nie może tworzyć połączeń sieciowych do komputerów innych niż ten, z którego został ściągnięty,
• nie może uruchamiać żadnych programów na komputerze, który go wykonuje,
• może czytać tylko wybrane parametry systemowe,
• nie może ładować bibliotek ani definiować metod w kodzie maszynowym.
Ograniczeniem (choć nie związanym z bezpieczeństwem) jest również graficzny interfejs użytkownika
apletu. Aplet może przedstawiać swoją reprezentację graficzną tylko w obrębie prostokąta na stronie
WWW o z góry zadanych wymiarach, bądź w postaci generowanych okienek, które różnią się od tych
generowanych przez aplikacje.
Pliki składające się na aplet: klasy, pliki z grafikami, dźwiękami i inne pliki pomocnicze mogą być
połączone w jeden plik o formacie Java Archive (JAR), co pozwala uzyskać wiele korzyści:
• bezpieczeństwo - zawartość pliku JAR można podpisać cyfrowo; dzięki temu mogą być
złagodzone niektóre z wcześniej wymienionych ograniczeń związane z dostępem do lokalnych
zasobów dyskowych oraz z pełnym dostępem do sieci,
Java
27
• skrócenie czasu ładowania - cały aplet może ściągnięty przez przeglądarkę w jednej transakcji
HTTP bez konieczności otwierania nowego połączenia dla każdego pliku,
• kompresja - pliki są kompresowane zgodnie z formatem ZIP.
Do obsługi plików JAR służy standardowe narzędzie jar zawarte w pakiecie JDK. Sposób jego
użycia jest analogiczny do programu tar z systemu Unix.
Operacja Polecenie
Tworzenie pliku JAR
jar cf jar-file input-file(s)
Oglądanie zawartości pliku JAR
jar tf jar-file
Rozpakowywanie pliku JAR
jar xf jar-file
Uruchamianie apletu spakowanego w postaci
pliku JAR
<applet
code=AppletClassName.class
archive="JarFileName.jar"
width=width height=height>
</applet>
) Uwaga. Internet Explorer używa własnego standardu kompresji - CAB. Netscape Communicator
obsługuje format JAR..
Znacznik <applet> na stronie HTML może mieć więcej parametrów niż podane wcześniej w
prostym przykładzie. Bardziej złożone wywołanie może mieć następującą postać:
<applet code=AppletSubclass.class codebase=”directory/”
width=anInt height=anInt align=left>
<param name=parameter1Name value=aValue>
<param name=parameter2Name value=anotherValue>
Wymagana jest przeglądarka obsługująca aplety Javy!
</applet>
Domyślnie przeglądarka szuka klas apletu oraz plików skompresowanych w tym samym katalogu, z
którego został ściągnięty plik HTML zawierający znacznik <applet>. Można jednak wskazać inne
miejsce podając parametr codebase. Wartością parametru może być ścieżka względna wobec strony
HTML, ścieżka bezwzględna lub w ogólności dowolny URL. Klasy apletu mogą więc być ściągane z
innego serwera niż strona HTML. Opcje width i height określają rozmiary prostokąta na stronie
przeglądarki, w obrębie którego wizualizowany jest aplet. Opcja align określa położenie apletu,
podobnie jak dla znacznika <img>: left, right, top, texttop, middle,
absmiddle, baseline, bottom, absbottom. Do apletu można przekazywać parametry
definiując znaczniki <param> w obrębie znacznika <applet>. Wartości tych parametrów można
następnie odczytywać w kodzie apletu wywołując metodę klasy Applet
public String getParameter(String name)
Opcjonalny tekst w obrębie znacznika <applet> jest pisany w okienku przeglądarki, gdy ta nie
potrafi obsługiwać Javy lub obsługa została wyłączona.
Kod apletu wykonywany jest przez maszynę wirtualną Javy zaimplementowaną w przeglądarce.
Jednym ze stałych niedomagań tego modelu dystrybucji kodu jest to, że wersje Javy wspierane przez
przeglądarki nie nadążają za najnowszymi wersjami wprowadzanymi przez firmę Sun. Aby częściowo
rozwiązać ten problem Sun opracował moduł rozszerzający (ang. plug-in) przeglądarki (obecnie tylko
dla Netscape Navigator i Internet Explorer pod Windows), który zapewnia wykorzystanie najnowszej
wersji bibliotek języka Java. Na skutek niejednolitego sposobu wywołania takiego modułu ze strony
HTML i jego złożonej postaci najwygodniej jest skorzystać z programu HTMLConverter, który
zamienia wystąpienia zanacznika <applet> na odpowiadający mu kod wywołujący moduł
rozszerzający Javy.
Języki obiektowe
28
3. Standardowe klasy Javy.
W skład JDK 1.2 wchodzą następujące pakiety: java.applet, java.awt, java.beans,
java.io, java.lang, java.math, java.net, java.rmi, java.security,
java.sql, java.text, java.util, javax.accessibility, javax.swing,
org.omg. Zostaną one omówione pokrótce, najpierw te najczęściej wykorzystywane.
W pakiecie java.lang zdefiniowana jest klasa Object, która jest klasą nadrzędną wobec
wszystkich innych klas Javy.
Pakiet zawiera także klasy opakowujące (kopertowe) zmienne podstawowych typów języka Java takie
jak Boolean, Byte, Integer, Long, Double, Character itd., definiując użyteczne
metody operujące na zmiennych tych typów, metody konwersji pomiędzy typami, zamianę na
łańcuchy znaków i inne.
Klasa Math zawiera metody wykonujące podstawowe operacje numeryczne takie jak funkcje
eksponencjalne, logarytmiczne, trygonometryczne.
W pakiecie java.lang zawarte są dwie klasy obsługujące łańcuchy znaków: String i
StringBuffer. Klasa String używana jest do przechowywania i wykonywania operacji na
stałych łańcuchach; po utworzeniu obiektu tej klasy nie można zmienić jego wartości. Klasa ta zawiera
metody do sprawdzania poszczególnych znaków, porównywania łańcuchów, wyodrębniania
podłańcuchów, tworzenia kopii z zamianą na małe albo duże litery. Klasa StringBuffer
implementuje łańcuchy znaków, które mogą być zmieniane. Podstawowe metody tej klasy to append
dodająca znaki na końcu bufora i insert wstawiająca znaki w określonym miejscu. Każdy obiekt
przydziela bufor na przechowywany łańcuch znaków. Jeżeli całkowita długość łańcucha wzrośnie
powyżej rozmiaru bufora, automatycznie przydzielany jest większy.
Dostęp do zasobów systemowych można uzyskać poprzez niezależne od systemu API zdefiniowane w
finalnej klasie System oraz zależne od systemu API zawarte w Runtime. Można uzyskać dostęp do
parametrów systemowych, standardowych strumieni: wyjściowego, wejściowego i błędów, metod
ładowania bibliotek, odczytu czasu komputera, szybkiego kopiowania tablic itd.
Klasa Thread definiuje wątki programu Javy pozwalające na jego współbieżną pracę.
Pakiet java.io definiuje klasy implementujące operacje wejścia-wyjścia. W pakiecie tym (jak
również i w innych) występują klasy i metody pochodzące z JDK 1.0 równolegle z nowymi klasami
wprowadzonymi wraz z wejściem JDK 1.1. Pozostawiono stare klasy w celu zapewnienia zgodności
dla dawniej pisanych programów. Często przestarzałe (deprecated) konstrukcje są odradzane i
proponowane są ich udoskonalone odpowiedniki.
Podstawowymi klasami pozwalającymi czytać i pisać z plików, łańcuchów znaków i innych strumieni,
są:
Reader wraz z podklasami BufferedReader, CharArrayReader,
InputStreamReader, FileReader, StringReader oraz Writer wraz z podklasami
BufferedWriter, CharArrayWriter, OutputStreamWriter, FileWriter,
PrintWriter, StringWriter. Ich starsze odpowiedniki to InputStream i
OutputStream wraz z ich podklasami.
Strumień wejściowy znaków może być dzielony na jednostki logiczne - tokeny, poprzez
skonstruowanie na nim obiektu klasy StreamTokenizer. Można zdefiniować składnię, według
której wyróżniane będą tokeny typu słowo, liczba, znaczniki końca linii i pliku oraz pomijane
komentarze.
Zdefiniowane są również klasy wspierające obsługę struktury drzewa plików w systemie - File oraz
czytanie i pisanie do plików o dostępie swobodnym - RandomAccessFile.
W pakiecie java.util można znaleźć m.in. szereg klas definiujących różne struktury danych
przechowujące inne obiekty. Klasa Vector implementuje tablicę obiektów, która może rosnąć lub
zmniejszać się w miarę jak obiekty są dodawane lub usuwane. Tak jak w zwykłej tablicy dostęp do jej
elementów może odbywać się poprzez całkowity indeks, można również szukać wystąpienia obiektu
w wektorze, którego wartość jest równa podanemu. Wszystkie elementy wektora najwygodniej jest
przeglądać wykorzystując interfejs Enumeration:
Java
29
Vector v;
for(Enumeration e = v.elements(); e.hasMoreElements() ;)
System.out.println(e.nextElement());
Każdy wektor stara się optymalizować zarządzanie rozmiarem zajmowanej pamięci. Gdy wektor
zajmuje całą przydzieloną pamięć, przed dodaniem kolejnego elementu jego rozmiar jest
automatycznie zwiększany o wartość capacityIncrement. Program może jednak sam zwiększyć
rozmiar wektora przed wstawieniem dużej porcji danych, aby uniknąć wielu realokacji. Podklasą klasy
Vector jest Stack realizujący kolejkę LIFO obiektów z metodami push i pop.
Klasa Dictionary jest abstrakcyjnym rodzicem dowolnej klasy implementującej odwzorowanie
kluczy na wartości. Jej podklasą jest Hashtable, której konstrukor posiada dwa parametry
initialCapacity i loadFactor. Gdy liczba elementów w tablicy mieszającej (hash table)
osiąga wartość iloczynu tych parametrów, rozmiar tablicy jest automatycznie zwiększany i przeliczane
są pozycje elementów. Większy współczynnik wypełnienia pozwala oszczędniej gospodarować
pamięcią kosztem dłuższego czasu potrzebnego do wyszukiwania elementów. Efektywniej jest
również zawczasu przydzielić odpowiedniej wielkości tablicę, niż potem zdać się na automatyczne
przeładowywania. Poniżej przedstawiono przykład tablicy mieszającej obiektów typu Integer
posiadających nazwy jako klucze:
Hashtable numbers = new Hashtable();
numbers.put("one", new Integer(1));
numbers.put("two", new Integer(2));
numbers.put("three", new Integer(3));
Aby wydobyć element z tablicy można posłużyć się następującym kodem:
Integer n = (Integer)numbers.get("two");
if(n != null)
System.out.println("two = " + n);
Obiekty przechowywane w tablicy mieszającej muszą implementować metody hashCode i equals
dziedziczone z klasy java.lang.Object.
Przy użyciu klasy BitSet można tworzyć zbiory bitów i wykonywać na nich operacje logiczne.
Klasa StringTokenizer pozwalająca dzielić łańcuchy znaków na tokeny jest znacznie prostsza w
użyciu niż klasa java.io.StreamTokenizer. Określa się w niej tylko jakie znaki rozdzielają
kolejne tokeny, domyślnie są to znaki z łańcucha ” \t\n\r”, na przykład:
StringTokenizer st = new StringTokenizer("To jest tylko test");
while (st.hasMoreTokens())
System.out.println(st.nextToken());
Można również znaleźć klasy obsługujące daty - Date i Calendar, ustawiające parametry
specyficzne dla kraju, regionu lub języka - Locale, TimeZone.
W pakiecie java.util.zip znajdują się klasy pozwalające tworzyć i czytać pliki skompresowane
w formatach ZIP i GZIP.
Pakiet java.net zawiera klasy realizujące połączenia sieciowe zarówno na poziomie gniazd, jak i
adresów URL wskazujących zasoby w WWW. Podstawowe klasy to Socket, URL,
URLConection. Poniższy przykład pokazuje jak z apletu można w prosty sposób przejść do
wybranej strony WWW:
try {
URL task = new URL("http://www.task.gda.pl/");
getAppletContext().showDocument(task);
Języki obiektowe
30
} catch (MalformedURLException e) {} // new URL() failed
Aplety i aplikacje Javy komunikują się z użytkownikiem wykorzystując klasy z pakietu java.awt
składające się na graficzny interfejs użytkownika AWT (Abstract Window Toolkit). AWT dostarcza
typowe komponenty graficzne takie, jak klawisze, pola do wprowadzania tekstu, listy wyboru itd.
poprzez klasy Button, Checkbox, Choice, Label, List, Menu, Scrollbar,
TextArea, TextField będące pochodnymi klasy Component. Wykorzystując klasę Canvas,
można rysować dowolne obrazy graficzne na ekranie, a po dodaniu odpowiedniej obsługi zdarzeń
można zdefiniować dowolny własny komponent.
Wraz z JDK 1.1 został wprowadzony nowy model obsługi zdarzeń, który jest znacznie bardziej
elastyczny i wydajny w porównaniu z modelem z JDK 1.0. Stary model dla zachowania zgodności jest
również obsługiwany, choć jego użycie jest odradzane. W modelu 1.1 AWT zdarzenia są generowane
przez źródła, którymi mogą być komponenty interfejsu użytkownika, myszka, klawiatura itd. Może
być wydelegowany jeden lub więcej odbiorców zdarzenia pochodzącego od określonego źródła, który
jest obiektem dowolnej klasy implementującej przynajmniej jeden z interfejsów obsługi zdarzeń
takich, jak ActionListener, KeyListener czy MouseListener. Zasadę działania tego
modelu można prześledzić w poniższym przykładzie:
import java.applet.*;
import java.awt.*;
import java.awt.event.*;
public class URLButton extends Applet implements ActionListener {
public void init() {
Button button = new Button("TASK Home");
add(button);
button.addActionListener(this);
}
public void actionPerformed(ActionEvent event) {
. . .
}
}
W skład JDK 1.2 został włączony pakiet javax.swing nazywany również Java Foundation
Classes (JFC), znacznie rozszerzający możliwości funkcjonalne interfejsu graficznego użytkownika.
Pakiet ten zawiera znacznie więcej komponentów graficznych, pozwala dynamicznie imitować znane
środowiska graficzne (np. Windows, CDE/Motif), umożliwia korzystanie z urządzeń pomocniczych w
odczytywaniu informacji (jak np. czytniki ekranu, wyświetlacze Braille’a), zawiera bogatą bibliotekę
do tworzenia dwuwymiarowej grafiki, wspomaga technikę “przeciągnij i upuść” pomiędzy
aplikacjami Javy i aplikacjami w danym systemie operacyjnym.
Zastosowanie klas Applet i AppletContext z pakietu java.applet było już
demonstrowane we wcześniejszych przykładach. Definiują one środowisko, w którym uruchamiane są
aplety. Pozwalają również na komunikację pomiędzy apletami znajdującymi się na tej samej stronie.
Środowisko budowania niezależnych od platformy, modyfikowalnych wizualnych komponentów
JavaBeans API zawarte jest w pakiecie java.beans. Wykorzystując przeznaczone do tego
narzędzia programistyczne, można wizualnie z komponentów konstruować aplikacje, aplety, servlety
lub komponenty złożone. Narzędzie tego typu pozwala wybrać z zestawu potrzebny nam komponent,
wstawić go do okienka programu, zmodyfikować jego wygląd i zachowanie, zdefiniować interakcję z
innymi komponentami; wszystko to nie pisząc ani jednej linii kodu.
JDBC API (Java DataBase Conectivity) zdefiniowane w pakiecie java.sql wprowadza jednolity
standard dostępu do dowolnych relacyjnych baz danych. Klasy z tego pakietu implementują
połączenia z bazą danych, zapytania SQL, wyniki tych zapytań itp. Dostęp do bazy odbywa się za
Java
31
pośrednictwem drivera, który może być napisany całkowicie w Javie i wtedy może być np. ściągnięty
jako część apletu, może być też napisany z wykorzystaniem kodu maszynowego określonej platformy,
aby uzyskać dostęp do istniejących już bibliotek dostępu do baz danych. Drivery JDBC można
podzielić na cztery kategorie:
1. Pomost JDBC-ODBC, który zapewnia dostęp do bazy poprzez binarne drivery ODBC.
2. Driver w Javie zamieniający odwołania JDBC na odwołania do binarnego drivera dostępowego
konkretnej bazy danych znajdującego się na lokalnym komputerze.
3. Całkowicie w Javie napisany driver komunikujący się poprzez sieć z oprogramowaniem
pośredniczącym w dostępie do bazy (middleware). Protokół dostępowy zależy od producenta tego
oprogramowania, producent ten jest też najczęściej dostawcą odpowiedniego drivera JDBC.
4. Całkowicie w Javie napisany driver komunikujący się poprzez sieć bezpośrednio z serwerem bazy
danych. Dostawcą drivera często jest producent bazy danych.
RMI (Remote Method Invocation) zawarty w java.rmi umożliwia tworzenie rozproszonych
aplikacji w Javie. Aplikacja taka złożona jest z serwera, tworzącego obiekty, których metody mogą
być wywoływane zdalnie; po utworzeniu obiektów aplikacja udostępnia odnośniki do nich i czeka na
wywołania ich metod przez klientów; klient ściąga odnośniki do zdalnych obiektów i wywołuje ich
metody. RMI zapewnia mechanizm, poprzez który odbywa się komunikacja pomiędzy serwerem i
klientem oraz przesyłane są dane w obie strony.
W pakietach org.omg Javy 1.2 wsparto powszechnie przyjęty standard modelu rozproszonych
obiektów – CORBA (Common Object Request Brokerage Architecture). Pozwala on na komunikację
pomiędzy obiektami bez względu na platformę systemu operacyjnego, ani użyty języka
programowania.
W pakiecie java.math znajdują się klasy BigInteger i BigDecimal pozwalające na tworzenie
i operacje na liczbach całkowitych dowolnej precyzji. BigInteger implementuje operacje
arytmetyki modularnej, obliczanie największego wspólnego podzielnika, generator liczb pierwszych,
operacje na bitach itd. Z liczb zdefiniowanych w tej klasie korzysta pakiet java.security
wprowadzający jednolity model kryptografii i ochrony danych. Znajdują się tam abstrakcyjne klasy
definiujące kody zapewniające integralność danych (message digests) oparte na jednokierunkowych
funkcjach mieszających (one-way hash functions), podpisy cyfrowe oparte na parze kluczy jawnego i
tajnego, zarządzanie kluczami i certyfikaty. Jest to tylko ramowy szkielet koncepcji
kryptograficznych, pod który producenci oprogramowania mogą podłączać konkretne algorytmy i
metody. Razem z JDK Sun dostarcza jedynie klasy do cyfrowego podpisywania dokumentów
opartego na algorytmie DSA oraz obliczania kodów dokumentów metodami MD5 i SHA-1.
W pakiecie java.text znajdziemy klasy obsługujące różne formaty danych tekstowych, np.
DateFormat obsługujący daty w różnych standardach czy NumberFormat tworzący i analizujący
różne formaty liczb.
4. Servlety - programy Javy na serwerze WWW.
Servlety są modułami, które są uruchamiane wewnątrz serwerów przetwarzających zapytania i
generujących odpowiedzi, takich jak np. rozszerzone o obsługę Javy serwery WWW. Rozszerzają one
funkcjonalność tych serwerów. Można w uproszczeniu powiedzieć, że servlety dla serwerów są tym,
czym aplety dla przeglądarek.
Servlety stanowią alternatywę dla skryptów CGI, umożliwiając łatwą metodę dynamicznego tworzenia
dokumentów HTML. Są one łatwiejsze do pisania dzięki wykorzystaniu środowiska Javy. Są również
szybciej wykonywane, gdyż wywołanie servletu odbywa się nie poprzez uruchomienie nowego
procesu, co jest kosztowne ze względu na czas procesora i zasoby pamięciowe, lecz jako wątek. Co
więcej, kod wykonywalny dla servletu jest ładowany do serwera tylko raz, gdy po raz pierwszy żądana
jest usługa oferowana przez dany servlet lub automatycznie, gdy zostanie zmieniony kod servleta.
Potem servlet pozostaje w pamięci serwera i może równolegle obsługiwać wiele zapytań z
możliwością komunikacji pomiędzy nimi.
Języki obiektowe
32
Klasy implementujące funkcje servletów znajdują się w pakiecie javax.servlet dostępnego z
firmy Sun jako Java Servlet Development Kit. Zawierają one abstrakcyjną klasę GenericServlet
oraz interfejs Servlet, które definiują podstawowe własności ogólnych servletów, czyli modułów
przetwarzających zapytania i generujących odpowiedzi. Jednak najczęściej korzysta się z podklas
znajdujących się w pakiecie javax.servlet.http definiujących servlety HTTP. Klasa
HttpServlet posiada m.in. takie metody jak doGet i doPost obsługujące typowe zapytania
CGI, czy uniwersalną metodę service obsługującą wszystkie rodzaje zapytań. Najprostszy servlet
może wyglądać następująco:
HelloWorldServlet.java
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class HelloWorldServlet extends HttpServlet {
public void doGet (HttpServletRequest req,
HttpServletResponse res)
throws ServletException, IOException {
res.setContentType("text/html");
ServletOutputStream out = res.getOutputStream();
out.println("<html>");
out.println("<head><title>Hello World</title></head>");
out.println("<body>");
out.println("<h1>Hello World</h1>");
out.println("</body></html>");
}
}
Większość użytecznych servletów czyta parametry wejściowe przekazane im z przeglądarki poprzez
metody GET lub POST i na ich podstawie tworzy odpowiedni dokument HTML. Prosty przykład
takiego servletu przedstawiono poniżej.
Hello.java
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class Hello extends HttpServlet {
public void service(HttpServletRequest req,
HttpServletResponse res)
throws ServletException, IOException {
res.setContentType("text/html");
PrintWriter toClient = res.getWriter();
toClient.println("<html><head>");
toClient.println("<title>Hello</title>");
toClient.println("</head>");
toClient.println("<h1>Hello " +
req.getParameterValues("name")[0] + "!</h1>");
toClient.println("</body></html>");
toClient.close();
}
}
Powyższy servlet można wywołać na kilka sposobów. Można podać w przeglądarce URL
Java
33
http://www.task.gda.pl/servlet/Hello?name=Darek
i wywołać servlet przekazując parametry metodą GET. Można na stronie HTML umieścić formularz:
<form action=http://www.task.gda.pl/servlet/Hello method=POST>
Podaj swoje imię: <input type=text name=name>
<input type=submit value=Wyślij>
</form>
i wywołać servlet przekazując parametry metodą POST. Można również wykorzystać znacznik
<servlet> na stronie HTML zapisanej w pliku z rozszerzeniem shtml.
Hello.shtml
...
<servlet code=Hello>
<param name=name value=Darek>
</servlet>
...
Wtedy serwer WWW rozszerzony o funkcjonalność servletów zastąpi na stronie HTML powyższy
znacznik wynikiem generowanym przez servlet Hello i tak przetworzoną stronę prześle do
przeglądarki.
Dostępnych jest już wiele serwerów WWW obsługujących servlety Javy. Przykładami mogą być Java
Web Server firmy Sun lub darmowy dla celów niekomercyjnych JRun firmy Live Software. Ten
ostatni może pracować jako samodzielny serwer WWW, bądź jako moduł rozszerzający
funkcjonalność innych znanych serwerów WWW np. Apache, Microsoft IIS, Netscape.
5. Podsumowanie
Zalety języka Java:
• “ożywienie” WWW - swego rodzaju “lingua franca” Internetu
• język pozwala łatwo pisać aplikacje sieciowe
• przenośność - niezależność od platformy dzięki JVM; Java poprzez swój B-kod staje się najbliższą
aproksymacją (zbudowaną raczaej z software niż z hardware, chociaż anonsowane są już “Java
chips”) jednego z najstarszych marzeń przemysłu komputerowego: prawdziwie uniwersalna
maszyna wirtualna
• prawie czysta implementacja paradygmatu obiektowego
• usunięcie arbitralnego pojęcia wskaźnika jak w C++
• wsparcie dla zbierania nieużytków
• brak samodzielnych (tj. definiowanych poza klasami) funkcji zewnętrznych
• weryfikacja kodu w fazie kompilacji i w fazie wykonania na zgodność ze standardem języka
• dynamiczne ładowanie klas (w locie)
• biblioteki klas dla GUI, sieciowe i WWW
Wady:
• Brak definiowanych przez użytkownika przeciążonych operatorów
• złamanie zasad obiektowości w standardowych bibliotekach klas: w klasach opakowań
(kopertowych), w klasach kolekcji i w klasach łańcuchów
• Komunikaty są rzadko polimorficzne i rzadko trafiają na oczekiwaną strukturę dziedziczenia
• brak klas parametryzowanych (genericity, templates)
• dziedziczenie tylko pojedyncze
• usunięcie instrukcji assert znanej w C i C++
Języki obiektowe
34
• pogmatwana (zawikłana) struktura modularna z trzema wpływającymi wzajemnie na siebie
koncepcjami (klasy, pakiety zagnieżdżone, pliki źródłowe).
6. 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.2 Documentation. http://java.sun.com/products/jdk/1.2/docs/, 1998.
[8] Servlet Tutorial. http://www.task.gda.pl/java/ServletTutorial.html, 1998.
7. Programy - ćwiczenia
1. Plik Squares.java
class Squares {
/** Print out the squares of integers from 1 to 10 */
public static void main(String[] args) {
int x = 1;
System.out.println("Squares of integers from 1 to 10:");
while (x <= 10) {
System.out.println(x * x);
// print x squared
x = x + 1;
// add 1 to x
} // end while
} // end main
} // end Squares
2. Plik StringDemo.java
class StringsDemo {
static public void main(String[] args) {
String myName = "Archibald";
myName = myName + " Tuttle";
System.out.println("Name = " + myName);
}
}
3. Plik Child1.java
class Parent { //accessible within package
static int i = 10; //accessible within package
public static void fp()
{
System.out.println("fp of Parent class");
} //end fp
} //end Parent
Java
35
public class Child1 extends Parent {
private static String st = "I am Child1";
public static void main(String args[])
{
fp();//call class function; same result as
Parent.fp();//Only class name; same result as
System.out.println(Parent.i);
System.out.println(st);
// super.fp(); // Doesn't work. Why?
}//end main
}//end Child1
4. Plik Inherit1.java
class GrandFather {
private int i = 10;
void fp()
{
System.out.print("fp of Parent class\n"+"i= "+i);
}//end fp
}//end GrandFather
class Father extends GrandFather {
}//end Father
class GrandSon extends Father {
String st = "I am a grandson";
} // end GrandSon
public class Inherit1 {
public static void main(String args[])
{
GrandSon gs = new GrandSon();
gs.fp();
System.out.println("\n");
if(gs instanceof GrandSon)
System.out.println(gs.st);
}//end main
}//end Inherit1
5. Plik Inherit2.java
class Parent {
private int i = 10;
void fp()
{
System.out.print("fp of Parent class\n"+"i= "+i);
}//end fp
}//end Parent
class Descendant extends Parent {
public void fp()
{
System.out.println("fp of Descendant class");
} //end fp
public void fd()
Języki obiektowe
36
{
super.fp(); //call to Parent's fp
} // end fd
} // end Descendant
public class Inherit2 {
public static void main(String args[])
{
Descendant ds = new Descendant();
ds.fp();
ds.fd();
}//end main
}//end Inherit2
6. Plik Inherit3.java
class Parent {
private int i = 10;
void fp()
{
System.out.print("fp of Parent class\n"+"i= "+i);
}//end fp
}//end Parent
class Descendant extends Parent {
public void fp()
{
System.out.println("fp of Descendant class");
} //end fp
public void fd()
{
super.fp(); //call to Parent's fp
} // end fd
} // end Descendant
class Kin extends Descendant {
public void fp()
{
System.out.println("fp of Kin class");
} //end fp
public void fk()
{
super.fd(); // call to Descendant's fp
} // end fk
} // end Kin
public class Inherit3 {
public static void main(String args[])
{
Kin kn = new Kin();
kn.fp();
kn.fk();
}//end main
}//end Inherit2
7. Plik PascalTriangle.java
Java
37
import java.io.*;
public class PascalTriangle {
private int data[][];
/** Create a Pascal's triangle to specified depth. */
public PascalTriangle(int rows) {
data = new int[rows][];
for (int row = 0; row < rows; row++) {
data[row] = new int[row + 1];
if (row == 0)
data[row][0] = 1;
else
for (int col = 0; col <= row; col++) {
data[row][col] = 0;
// if not on right edge, add node up and right
if (col < row)
data[row][col] += data[row - 1][col];
// if not on left edge, add node up and left
if (col > 0)
data[row][col] += data[row - 1][col - 1];
}
}
}
/** Print this Pascal's triangle to given stream. */
public void print(PrintStream ps) {
for (int i = 0; i < data.length; i++) {
int[] row = data[i];
for (int j = 0; j < row.length; j++)
ps.print(row[j] + " ");
ps.println();
}
}
/** Create a Pascal's triangle of depth 12 and print it. */
public static void main(String[] args)
{
PascalTriangle pt = new PascalTriangle(12);
pt.print(System.out);
}
}
8. Plik EasyIn.java
// Simple input from the keyboard for all primitive types. ver 1.0
// Not thread safe, not high performance, and doesn't tell EOF.
// It's intended for low-volume easy keyboard input.
// An example of use is:
// EasyIn easy = new EasyIn();
// int i = easy.readInt(); // reads an int from System.in
// float f = easy.readFloat(); // reads a float from System.in
import java.io.*;
import java.util.*;
Języki obiektowe
38
class EasyIn {
static InputStreamReader is = new InputStreamReader( System.in );
static BufferedReader br = new BufferedReader( is );
StringTokenizer st;
StringTokenizer getToken() throws IOException {
String s = br.readLine();
return new StringTokenizer(s);
}
boolean readBoolean() {
try {
st = getToken();
return new Boolean(st.nextToken()).booleanValue();
} catch (IOException ioe) {
System.err.println("IO Exception in EasyIn.readBoolean");
return false;
}
}
byte readByte() {
try {
st = getToken();
return Byte.parseByte(st.nextToken());
} catch (IOException ioe) {
System.err.println("IO Exception in EasyIn.readByte");
return 0;
}
}
short readShort() {
try {
st = getToken();
return Short.parseShort(st.nextToken());
} catch (IOException ioe) {
System.err.println("IO Exception in EasyIn.readShort");
return 0;
}
}
int readInt() {
try {
st = getToken();
return Integer.parseInt(st.nextToken());
} catch (IOException ioe) {
System.err.println("IO Exception in EasyIn.readInt");
return 0;
}
}
long readLong() {
try {
st = getToken();
return Long.parseLong(st.nextToken());
} catch (IOException ioe) {
Java
39
System.err.println("IO Exception in EasyIn.readLong");
return 0L;
}
}
float readFloat() {
try {
st = getToken();
return new Float(st.nextToken()).floatValue();
} catch (IOException ioe) {
System.err.println("IO Exception in EasyIn.readFloat");
return 0.0F;
}
}
double readDouble() {
try {
st = getToken();
return new Double(st.nextToken()).doubleValue();
} catch (IOException ioe) {
System.err.println("IO Exception in EasyIn.readDouble");
return 0.0;
}
}
char readChar() {
try {
String s = br.readLine();
return s.charAt(0);
} catch (IOException ioe) {
System.err.println("IO Exception in EasyIn.readChar");
return 0;
}
}
String readString() {
try {
return br.readLine();
} catch (IOException ioe) {
System.err.println("IO Exception in EasyIn.readString");
return "";
}
}
// This method is just here to test the class
public static void main (String args[]){
EasyIn easy = new EasyIn();
System.out.print("enter char: "); System.out.flush();
System.out.println("You entered: " + easy.readChar() );
System.out.print("enter String: "); System.out.flush();
System.out.println("You entered: " + easy.readString() );
System.out.print("enter boolean: "); System.out.flush();
Języki obiektowe
40
System.out.println("You entered: " + easy.readBoolean() );
System.out.print("enter byte: "); System.out.flush();
System.out.println("You entered: " + easy.readByte() );
System.out.print("enter short: "); System.out.flush();
System.out.println("You entered: " + easy.readShort() );
System.out.print("enter int: "); System.out.flush();
System.out.println("You entered: " + easy.readInt() );
System.out.print("enter long: "); System.out.flush();
System.out.println("You entered: " + easy.readLong() );
System.out.print("enter float: "); System.out.flush();
System.out.println("You entered: " + easy.readFloat() );
System.out.print("enter double: "); System.out.flush();
System.out.println("You entered: " + easy.readDouble() );
}
}
9. Plik Heritage.java
/**Program krotko demonstruje mechanizm korzystania z interfejsow*/
/**Glowna klasa programu, tworzy pozostale*/
public class Heritage
{
public static void main (String[] args) {
/*tworzymy obiekty i wywolujemy odpowiednie metody*/
Client our_client = new Client();
our_client.startingReport();
Server our_server= new Server (our_client);
our_server.execute(5);
}
}
/**Klasa klienta implementuje interfejs Message*/
class Client implements Message {
int counter=0;
/**zeruje licznik i wyswietla komunikat poczatkowy*/
public void startingReport()
{
counter=0;
System.out.println ("To jest raport poczatkowy klienta");
System.out.println ("Counter="+counter);
}
/**Zwieksza licznik i wyswietla jego wartosc - podajac, ze klasa implementuje interfejs Message;
zobowiazalismy sie, ze metoda o nazwie giveReport bedzie istniala*/
public void giveReport()
{
counter++;
System.out.println ("Counter="+counter);
Java
41
}
}
/**Klasa serwera korzysta z obiektu majacego zaimplementowany interfejs Message*/
class Server
{
/*Zwrocmy uwage, ze typem obiektu jest nazwa interfejsu*/
Message object;
/**Konstuktor. Jako parametr podajemy dowolny obiekt majacy zaimplementowany interfejs
Message; w naszym przypadku bedzie to Client*/
Server (Message object)
{
this.object=object;
}
/** Wywolujemy metode giveReport obiektu z interfejsem Message times razy */
public void execute (int times)
{
for (int k=0; k<times; k++)
object.giveReport();
}
}
/**Interfejs Message, kazda klasa ktora go zaimplementuje musi posiadac metode giveReport*/
interface Message
{
/**Tylko zobowiazanie, ze kazda klasa ktora implementuje ten interfejs bedzie miala metode
giveReport - jej dzialanie bedzie zalezec od danej klasy*/
void giveReport();
}
10. Plik Compare.java
/**Program krotko demonstruje mechanizm wyjatkow w Java*/
import java.io.*;
/**Klasa porownuje dwa pliki i zwraca rezultat porowanania na ekranie*/
public class Compare {
/**glowna i jedyna metoda aplikacji*/
public static void main (String[] args) {
/*nazwy plikow*/
String first_file = args[0];
String second_file = args[1];
/*obiekty reprezentujace pliki*/
RandomAccessFile first =null;
RandomAccessFile second =null;
/*Zawartosc plikow bedzie umieszczona w tych buforach*/
byte[] first_buffer;
byte[] second_buffer;
/*Dlugosci plikow*/
int first_size = -1;
int second_size = -1;
try {
System.out.println ("Ten program porowna dwa pliki");
Języki obiektowe
42
System.out.println ("plik 1: "+first_file+" plik2: "+second_file);
try {
first = new RandomAccessFile(first_file, "r");
second = new RandomAccessFile(second_file, "r");
/*blok, w ktorym porownujemy*/
prawie_koniec:{ //etykieta
first_size = (int)first.length();
second_size = (int)second.length();
/*najpierw porownujemy dlugosci*/
if (first_size!=second_size)
{
System.out.println ("Rozne pliki");
break prawie_koniec;
}
/*jesli dlugosci identyczne, wczytujemy pliki do pamieci*/
first_buffer = new byte [first_size];
second_buffer = new byte [second_size];
first.readFully (first_buffer, 0, first_size);
second.readFully (second_buffer, 0, second_size);
/*porownujemy zawartosc plikow*/
for (int i=0; i<first_size; i++)
{
if (first_buffer[i]!=second_buffer[i])
{
System.out.println ("Rozne pliki");
break prawie_koniec;
}
}
System.out.println ("Pliki identyczne");
}
/*tutaj przechodzi break prawie_koniec*/
System.out.println ("---------------");
}
/*przechwytujemy wyjatki zwiazane z operacjami I/O*/
catch (IOException e)
{
System.out.println ("Blad I/O");
}
/*to sie zawsze wykona*/
finally
{
try
{
first.close();
second.close();
}
/*podczas zamykania plikow ignorujemy wyjatki*/
catch (Exception ignored) { }
System.out.println ("Koniec czesci wewnetrznej");
}
}
/*tu przechwytujemy wyjatki nie obsluzone w czesci wewnetrznej*/
catch (Exception e)
{
Java
43
System.out.println ("Wystapil nastepujacy wyjatek przechwycony w main");
System.out.println (e);
System.out.println ("Uzycie programu: Compare pierwszy_plik drugi_plik");
}
/*na koniec zawsze wyswietlamy krotki komunikat*/
finally
{
System.out.println ("Koniec dzialania programu");
}
} //end main
}// end Compare
11. Plik FileInfo.java
import java.io.*;
public class FileInfo {
public static void main(String[] args) {
for (int i = 0; i < args.length; i++)
{
String path = args[i];
if (args.length > 1) System.out.println("Regarding \"" + path + "\":");
File file = new File(path);
if(file.exists())
{
System.out.println("This file exists.");
System.out.println("It can" + (file.canRead() ? "" : "not") +
" be read.");
System.out.println("It can" + (file.canWrite() ? "" : "not") +
" be written.");
System.out.println("It is " + file.length() + " bytes long.");
if(file.isFile())
{ System.out.println("It is a normal file."); }
else
{
System.out.println("It is a directory.");
String[] contents = file.list();
if (contents.length == 0) System.out.println("It is empty.");
else
{
System.out.println("It contains the following files:");
for (int j = 0; j < contents.length; j++)
System.out.println("\t" + contents[j]);
}//end else that belongs to if(contents.length==0)
}// End else that belongs to if(file.isFile())
}// End if(file.exists())
else {
System.out.println("This file does not exist.");
}
// End else that belongs to if(file.exists())
System.out.println();
}// End for
}//End main
}// End FileInfo