3
Java
Język Java: Podstawy, Programowanie, Zastosowania
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ę:
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).
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.
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.
Rys. 1-2. Przetwarzanie programów użytkownika
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ę
(W języku Java każda instrukcja kończy się średnikiem, który pełni rolę symbolu terminalnego).
Słowo System jest nazwą klasy w standardowym środowisku języka. Klasa System zawiera statyczny obiekt składowy typu PrintStream o nazwie out; wywołanie System.out oznacza pisanie do standardowego strumienia wyjściowego. Klasa PrintStream zawiera szereg przeciążeń metody o nazwie print; jedno z nich przyjmuje parametr 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.
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.
F Uwaga. W języku Java każda klasa dziedziczy od predefiniowanej klasy Object. Zatem, jeżeli w definicji klasy nie występuje fraza extends, to jest to równoważne niejawnemu wystąpieniu w tej definicji frazy ‘extends Object’.
Zauważmy, że oprócz słowa kluczowego class i nazwy klasy wszystkie pozostałe elementy w deklaracji klasy są opcjonalne. Jeżeli nie umieścimy ich w deklaracji, to kompilator przyjmie domyśnie, że klasa jest niepubliczną, nieabstrakcyjną i niefinalną podklasą predefiniowanej klasy Object.
Ciało klasy jest zamknięte w nawiasy klamrowe i może zawierać zmienne składowe (to jest pola lub zmienne wystąpienia), zmienne klasy (statyczne, tj. poprzedzone słowem kluczowym static), konstruktory i metody oraz funkcje klasy (statyczne). Nazwa każdej zmiennej składowej, zmiennej klasy, metody lub funkcji klasy musi być poprzedzona nazwą typu (np. boolean, double, char, float, int, long, void). Przed nazwą typu może wystąpić jeden ze specyfikatorów dostępu: private (dostęp tylko dla elementów klasy, np. private double d;), protected (dostęp tylko w podklasie, nawet jeśli podklasa należy do innego pakietu; nie dotyczy zmiennych klasy) lub public (dostęp publiczny). Brak specyfikatora oznacza, że dany element jest dostępny tylko dla klas w tym samym pakiecie. Po specyfikatorze dostępu może wystąpić słowo kluczowe final. Słowo final przed nazwą typu zmiennej wystąpienia lub zmiennej klasy deklaruje jej niemodyfikowalność (np. public static final int i = 10;), zaś w odniesieniu do metody oznacza, że nie może ona być redefiniowana w podklasie (np. public final void f(int i) {/* ... */ }).
Dostęp do elementów klasy uzyskuje się za pomocą operatora kropkowego. Jeżeli element danej klasy (zmienna lub metoda) przesłania (overrides) jakiś element swojej superklasy, to można się do niego odwołać za pomocą słowa kluczowego super, jak w poniższym przykładzie:
class ASillyClass /* Deklaracja klasy */
static final int MAX = 100; /** Definicja stałej */
boolean aVariable;/* Deklaracja zmiennej wystąpienia */
static public int x = 10; //Definicja zmiennej klasy
void aMethod() { //Definicja metody
aVariable = true;// Instrukcja przypisania
} // 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 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 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 {
int a = 10, b = 20;
CPoint cp = new CPoint(a,b);
}//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.
F 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);.
Konstrukcja o postaci
interface nazwa {
/* Deklaracje metod i definicje stałych */
jest w języku Java typem definiowanym przez użytkownika. Deklaracja metody składa się z sygnatury (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();
...
dziadekNeo