Personal tools

PO Wstęp do Javy

From Studia Informatyczne

<<< Powrót

Spis treści

Wprowadzenie

Od tego wykładu zaczynamy poznawanie programowania obiektowego na przykładzie konkretnego języka programowania. Będzie nim Java. Java jest jednym ze współczesnych, nowoczesnych języków obiektowych. Jest też bardzo popularna. Wszystkie te cechy uzasadniają wybór tego języka jako narzędzia do pokazania mechanizmów obiektowych. Należy jednak podkreślić, że nie jest to jedyna możliwość. Niniejszy wykład z bardzo małymi zmianami można by było oprzeć na C#. Można też z bardzo dobrym skutkiem skonstruować wykład z programowania obiektowego zestawiając ze sobą czysto obiektowy Smalltalk z hybrydowym C++ (choć wymaga to już pewnej dojrzałości programistycznej od słuchaczy). Możliwości jest wiele, twórcy każdego wykładu muszą dokonać jakiegoś wyboru. Ważne jest to, by słuchaczom wykładu nie umknął podstawowy fakt: wybrany język programowania jest tylko narzędziem służącym do pokazania na przykładach na czym polega programowanie obiektowe. Wybór języka, aczkolwiek niezbędny, jest rzeczą wtórną.

W trakcie wykładu omówimy wiele cech i konstrukcji Javy. Chcemy jednak mocno podkreślić, że nie jest naszym celem stworzenie podręcznika Javy. Na szczęście firmowa dokumentacja Javy jest bardzo obszerna i powszechnie, darmowo dostępna. Dotyczy to tak definicji języka jak i opisu standardowych bibliotek. Nie czujemy się więc zobowiązani do omawiania tych cech Javy, które z punktu widzenia tego wykładu mają małe znaczenie (na przykład zupełnie pomijamy kwestie związane ze współbieżnością). Java mimo młodego wieku przeszła już wiele zmian, my będziemy bazować na jej dystrybucji J2SE 1.6 i opisie języka podanym w trzecim wydaniu jego specyfikacji (z roku 2005).

Standardowe biblioteki Javy są bardzo rozbudowane - instalując podstawową dystrybucję Javy (J2SE) dostajemy do dyspozycji prawie cztery tysiące klas zgrupowanych w około dwustu pakietach. Oczywiście nie będziemy próbowali ich wszystkich omawiać w ramach naszego wykładu, natomiast będziemy czerpać z nich przykłady rozbudowanych i złożonych hierarchii klas. Zajmiemy się na przykład kolekcjami i strumieniami oraz klasami do tworzenia graficznych interfejsów użytkownika.

Po tych wyjaśnieniach pora zaczynać!

Zaczynamy

Java jest wysokopoziomowym, kompilowanym, obiektowym językiem programowania z silną kontrolą typów. Składnia Javy jest wzorowana na C/C++. Jednym z założeń projektowych Javy było stworzenie języka wzorowanego na C++ ale bezpieczniejszego, dlatego w Javie nie ma na przykład wskaźników. Inne założenie projektowe dotyczyło przenośności programów. Ponieważ Java m.in. została zaprojektowana z myślą o uruchamianie programów pobieranych przez sieć, język musiał być tak zaprojektowany i wyspecyfikowany, by efekt działania programu nie zależał od tego, jakiej implementacji języka użyto (oczywiście przy założeniu jej zgodności ze specyfikacją języka). Ciekawą cechą Javy jest to, że kompilator nie generuje kodu maszynowego, lecz kod pośredni (tak zwany bajtkod), który jest wykonywany przez wirtualną maszynę Javy. Takich wirtualnych maszyn Javy stworzono wiele dla różnych systemów operacyjnych i komputerów. Dzięki temu rozwiązaniu i wspomnianej wcześniej ścisłej specyfikacji języka, można napisać program w Javie na przykład na komputerze domowym, skompilować go, przesłać skompilowaną jego postać na drugi koniec świata na duży komputer w centrum obliczeniowym i tam go wykonać. Efekt działania będzie taki sam.[1]

Od napisania przez Kernighana i Ritchiego znakomitej książki o C stało się zwyczajem, by pierwszy prezentowany w danym języku program wypisywał pozdrowienie dla świata, nie wypada nam więc postąpić inaczej:

public class HelloWorld { 
  public static void main(String[] args) {
    System.out.println("Hello world!");
  }
}

Programy w Javie składają się z klas. Na razie przyjmijmy - w wielkim uproszczeniu - że klasa jest takim lepszym typem rekordowym z Pascala (czy typem struktur z C), zawierającym oprócz zwykłych pól także funkcje. W językach obiektowych funkcje zdefiniowane w klasach nazywamy metodami.

Przedstawiony program składa się z jednej klasy o nazwie HelloWorld. Definicja klasy rozpoczyna się od słowa class poprzedzonego być może dodatkowymi modyfikatorami dostępu. Tu mamy jeden: public, informujący kompilator, że nasza klasa jest dostępna także poza swoim pakietem.[2] O pakietach powiemy dalej Przedstawiona klasa zawiera tylko jedną składową, jest nią metoda o nazwie main. Ta nazwa metody nie jest przypadkowa. Wykonanie całego programu w Javie polega na wykonaniu metody main z głównej klasy programu (w naszym programie jest tylko jedna klasa, więc wybór klasy głównej nie nastręcza tu problemów).

Metoda main musi być przygotowana na przyjęcie jako argumentu tablicy napisów zawierającej argumenty podane w wierszu polecenia przy wywołaniu programu, nawet jeśli, tak jak przykładowa metoda, ignoruje te argumenty. Parametry metody deklaruje się bardzo podobnie jak w większości języków programowania. Podaje się sekwencję deklaracji poszczególnych parametrów (u nas ta sekwencja ma długość 1), a poszczególne deklaracje oddziela się jakimś separatorem (w Javie jest to akurat przecinek). Każda deklaracja określa nazwę i typ parametru. W niektórych językach programowania, np. Pascalu czy C#, deklaracja określa również sposób przekazywania parametrów, ale w Javie nie ma takich dodatkowych informacji. W naszym przykładzie parametr nazywa się args, a typem jego wartości jest String[], czyli tablica napisów, przy czym długość tablicy może być dowolna. Zwróćmy uwagę, że w Javie najpierw podaje się typ, a potem nazwę parametru, choć to parametr, a nie typ, jest w tym miejscu definiowany.

Jeżeli w Javie mamy metodę, która wylicza jakąś wartość, czyli metodę będącą funkcją, to w jej nagłówku musimy określić typ wyniku. Jeśli metoda jest procedurą, czyli nie daje żadnej wartości, to zamiast typu wyniku podajemy słowo kluczowe void.[3] Metoda main w Javie nie daje żadnego wyniku. Dlatego w jej nagłówku pojawiło się słowo void. Typ wyniku metody podaje się w Javie (i wielu innych językach programowania) przed nazwą metody (niezgodnie niestety z tradycją matematyczną).

Przed typem metody podane są dodatkowe specyfikatory. Słowo public oznacza, że metoda jest widoczna poza klasą. Słowo static oznacza, że jest to metoda klasowa, czyli taka, do wywołania której nie jest potrzebny egzemplarz obiektu tej klasy. Tak jak wspominaliśmy wcześniej klasa odpowiada typowi, zaś wartości typu klasowego - egzemplarze tego typu, to obiekty. Zwykle wywołujemy metody obiektów, ale można też wywoływać metody klas. Jest to szczególnie istotne w naszym programie, gdzie nie utworzyliśmy żadnego obiektu.

Treść metody main składa się z jednego wywołania metody o nazwie println, wypisującej na konsoli tekstowej (czyli na standardowym wyjściu programów działających w trybie tekstowym) tekst zadany jako parametr. Wywołując metodę w Javie musimy podać obiekt, na rzecz którego tę metodę wywołujemy. W tym przypadku jest to obiekt będący wartością zmiennej out[4] reprezentujący standardowy strumień wyjściowy (jest to obiekt klasy PrintStream). Zmienna out jest atrybutem klasowym (polem w terminologii rekordów z Pascala) klasy System. W tej klasie zebrano podstawowe operacje i obiekty związany ze standardowym wejściem i wyjściem oraz z systemem operacyjnym, pod kontrolą którego działa program. Zwróćmy uwagę, że obiekt out jest atrybutem klasowym, to znaczy dostępnym bezpośrednio w klasie System, a nie jej egzemplarzach (których zresztą dla tej akurat klasy nie daje się tworzyć). W celu odwoływania się do metody lub atrybutu klasy w Javie stosuje się (typową dla większości języków obiektowych) notację kropkową:

obiekt.składowa

gdzie składowa może być atrybutem lub metodą. W tym drugim przypadku podajemy jeszcze listę (być może pustą) jej parametrów otoczoną nawiasami okrągłymi. Oczywiście możemy się dalej odwoływać do składowych atrybutu lub wyniku metody, pisząc na przykład:

osoba.adres.ulica

Tej samej składni używa się przy odwoływaniu się do atrybutów i metod klasowych

klasa.składowa

Komentarze

Komentarze jednowierszowe zaczynają się od dwu ukośników i rozciągają aż do końca wiersza.

// To jest komentarz jednowierszowy

Komentarze wielowierszowe są ograniczone przez /* na początku i */ na końcu. Oczywiście komentarz wielowierszowy może mieścić się całkowicie w jednym wierszu, a nawet może być kilka takich komentarzy w jednym wierszu.

/* To jest 
     komentarz 
     wielowierszowy */
  /* A */ /* to */ /* kilka */ /* takich */ /* komentarzy */

Komentarzy wielowierszowych nie można zagnieżdżać.

/* To jest komentarz /* to nadal jest komentarz */ a_to_już_nie

Elementy leksykalne Javy

Nie będziemy tu zbytnio wchodzić w szczegóły, odsyłając bardziej zainteresowanych czytelników do raportu języka. Pozwolimy sobie jedynie podsumować najważniejsze elementy leksykalne.

Identyfikatory

  • Identyfikatory składają się z liter i cyfr, znak "_" jest traktowany jako litera.[5]
  • Duże i małe litery są rozróżniane, czyli identyfikator X jest różny od identyfikatora x.
  • Ponieważ Java używa kodowania znaków Unicode[6], to identyfikatory mogą zawierać znaki narodowe (w tym polskie).
  • Nie ma ograniczenia na długość identyfikatorów.
  • Identyfikatory nie mogą być słowami kluczowymi Javy ani literałami true, false i null.

Kwestia zasadności używania w programamch identyfikatorów pisanych z użyciem znaków narodowych, czyli nie po angielsku, budzi dużo kontrowersji. Autor tego tekstu, doceniając inne argumenty, dotyczące na przykład pracy w międzynarodowych zespołach, pozwala sobie wyrazić w tym miejscu radość z faktu, że rozwiązując w Javie np. zadanie o wilku, kozie i kapuście może nazwać zmienną łódź zamiast lodz czy boat.

Słowa kluczowe

Oto lista słów kluczowych Javy

abstract continue for new switch
assert default if package synchronized
boolean do goto private this
break double implements protected throw
byte else import public throws
case enum instanceof return transient
catch extends int short try
char final interface static void
class finally long strictfp volatile
const float native super while

Literały

W Javie mamy 6 rodzajów literałów:

  • Liczby całkowite (np. 13 czy -2627), mogą być dziesiętne, szesnastkowe (0xC) lub ósemkowe (np. 015[7]). Największa dozwolona wartość literału całkowitego to 2147483647 czyli 2^31-1[8], zaś literału będącego długą liczbą całkowitą (long) to 9223372036854775807L czyli 2^63-1. Literały typu long oznaczamy pisząc po ich ostatniej cyfrze duże L. Dokładne wartości tych stałych nie mają dla nas oczywiście znaczenia, natomiast bardzo ważne jest zwrócenie uwagi na to, że specyfikacja Javy nie pozwala implementacjom tego języka na jakąkolwiek dowolność w tym względzie.
  • Liczby rzeczywiste (np. 1.0 czy -4.9e12), mogą być zapisanie w systemie dziesiętnym lub szesnastkowym[9].
  • Literały logiczne false i true.
  • Literały znakowe (np. 'a' czy '\n').
  • Literały napisowe (np. "Ala ma kota"). Na uwagę zasługuje fakt, że napisy nie są w Javie wartościami typu pierwotnego, lecz wartościami klasy String. Mamy tu zatem sytuację, w której składnia języka jest związana z jedną ze standardowych klas.
  • Literał null.

Operatory

Zestaw operatorów w Javie jest dość bogaty[10]:

= > < ! ~  ? :
== <= >= != && || ++ --
+ - * / & | ^ % << >> >>>
+= -= *= /= &= |= ^= %= <<= >>= >>>=


Elementy składniowe Javy

Typy

Jak już wspominaliśmy Java jest językiem programowania z silnym systemem typów. To oznacza, że każda zmienna, atrybut czy parametr ma zadeklarowany typ. Kompilator wylicza typy wszystkich wyrażeń w programie i sprawdza, czy wszystkie operatory i metody są używane zgodnie z ich deklaracjami, czyli z argumentami odpowiednich typów. Także elementy każdej instrukcji muszą mieć właściwe typy, np. warunek w pętli while musi być wyrażeniem o wartości typu logicznego.

Specyficzną cechą Javy jest to, że typy w tym języku są podzielone na dwie kategorie:

  • typy pierwotne,
  • typy referencyjne.

Typy pierwotne to grupa ośmiu typów zawierających wartości proste. Tymi typami są:

  • typ wartości logicznych: boolean,
  • typy całkowitoliczbowe: byte, short, int, long, char[11],
  • typy zmiennopozycyjne: float, double.

Typy referencyjne dzielą się z kolei na następujące kategorie:

  • typy klas,
  • typy interfejsów,
  • typy tablic.

Wartościami typów referencyjnych są referencje (w pewnym uproszczeniu można o nich myśleć jako o wskaźnikach) do obiektów lub wartość null.

Ponadto istnieje typ o pustej nazwie, będący typem wyrażenia null. Ponieważ nie ma on nazwy, nie da się nawet zadeklarować zmiennej tego typu. Wartość tego typu można rzutować na dowolny typ referencyjny, więc można myśleć o wartości null jako o wartości każdego typu referencyjnego i nie przejmować się istnieniem tego typu.

Obiekty

Przez obiekt rozumie się w Javie dynamicznie stworzony egzemplarz jakiejś klasy lub dynamicznie stworzoną tablicę[12]. Żeby to zrównanie egzemplarzy klas i tablic uprawomocnić zadbano, by zarówno egzemplarze klas jak i tablice rozumiały wszystkie metody z klasy Object (jak zobaczymy w dalszych wykładach efekt ten dla egzemplarzy klas jest w naturalny sposób osiągany dzięki dziedziczeniu).

Zmienne

Zmienne są (zwykle) nazwanymi pojemnikami na pojedyncze wartości typu z jakim zostały zadeklarowane. Zmienne typów pierwotnych przechowują wartości dokładnie tych typów, zmienne typów referencyjnych przechowują wartość null albo referencję do obiektu typu będącego zadeklarowanym typem zmiennej bądź jego podklasą.

Wyróżniamy siedem rodzajów zmiennych:

  • zmienne klasowe,
  • zmienne egzemplarzowe,
  • zmienne lokalne,
  • elementy tablic (te zmienne są anonimowe),
  • parametry metod,
  • parametry konstruktorów,
  • parametry obsługi wyjątków.

Każda zmienna musi być zadeklarowana. Z każdą zmienną związany jest jej typ podawany przy deklaracji zmiennej. Typ ten jest używany przez kompilator do sprawdzania poprawności operacji wykonywanych na zmiennych. Jeśli kompilator wykryje w programie użycie zmiennej niezgodne z jej typem, to taki program zostanie odrzucony jako niepoprawny. Sprawdzanie zgodności typów jest bardzo ważną i użyteczną własnością Javy. Dzięki niej wiele prostych błędów w programach (w rodzaju omyłkowej próby pomnożenia liczby przez napis) jest wykrywanych już w fazie kompilacji. Oczywiście błędów logicznych (to jest działań programu niezgodnych z jego specyfikacją) kompilator nie jest w stanie wykrywać. A szkoda.

W Javie każda zmienna musi być zainicjowana przed pierwszym pobraniem jej wartości. Wymusza to kompilator Javy. W przypadku parametrów zachowanie tego wymogu jest oczywiste. Dla pozostałych czterech rodzajów zmiennych jest to już trudniejsze. W Javie przyjęto, że takie zmienne albo będą jawnie inicjowane przez programistę albo będą miały nadaną automatycznie wartość początkową. Zasady są tu następujące: zmienne klasowe, egzemplarzowe i elementy tablic są inicjowane wartościami domyślnymi, zaś zmienne lokalne muszą być jawnie zainicjowane przez programistę.

Przy deklaracji czterech rodzajów zmiennych nie będących parametrami można jawnie podawać ich wartości początkowe. To bardzo dobra praktyka. Jeśli w deklaracji zmiennych klasowych, egzemplarzowych lub tablic nie podano ich wartość początkowej, to taka zmienna zostanie automatycznie zainicjowana wartością domyślną. Ta wartość zależy od typu zmiennej, ogólnie można by powiedzieć, że odpowiada wartości 0 (dla wartości logicznych będzie to false, a dla typów referencyjnych wartość null).

W przypadku zmiennych lokalnych programista musi sam zadbać o zainicjowanie zmiennej przed jej odczytaniem. Może to zrobić albo od razu przy deklaracji zmiennej lokalnej (najbezpieczniejsze rozwiązanie) albo później - byleby tylko przed pierwszym użyciem tej zmiennej. Przy czym w tym drugim przypadku musi to zrobić tak, by kompilator miał pewność, że zmienna rzeczywiście została zainicjowana. Wbrew pozorom problem stwierdzenia, czy w danym miejscu programu zmienna miała wcześniej przypisaną wartość wcale nie jest prosty. Wyobraźmy sobie na przykład, że między deklaracją zmiennej a owym miejscem programu były zagnieżdżone instrukcje warunkowe i instrukcje pętli. Okazuje się, że nie ma i nigdy nie będzie algorytmu, który mógłby zawsze rozstrzygnąć, czy zmiennej przypisano uprzednio wartość. Zatem kompilator Javy, jeśli nie ma pewności, czy zmiennej nadano wartość przed pierwszym odczytaniem jej wartości, odrzuca program. Na szczęście w większości sytuacji występujących w praktyce, zdolność kompilatora do wykrywania, czy zmienna została zainicjowana, jest wystarczająca.

Konwencja nazewnicze

Konwencje kodowania w Javie zalecają pisanie nazw klas i interfejsów z wielkiej litery, nazw zmiennych, parametrów i metod z małej. W nazwach wielosłowowych kolejne słowa piszemy z wielkiej litery (bez podkreślników, jest to tzw. notacja CamelCase). Nazwy pakietów piszemy zawsze wyłącznie małymi literami, nazwy stałych (static final) wyłącznie wielkimi.

Instrukcje

Zestaw instrukcji Javy jest dość standardowy dla C-podobnych języków. Dlatego ograniczymy się tu jedynie do wymienienia tych instrukcji:

  • instrukcja pusta
;
  • instrukcja deklaracji zmiennej lokalnej
int j = 13;
int[] tab = new int[10];
char zn;
  • instrukacja etykietowana
koniec: return 0;
  • instrukcja wyrażeniowa
i = 0;
o.wypisz(i);
  • instrukcja warunkowa
if (i > 0) i--;
if (i > j)
  max = i; 
else 
  max = j;
  • instrukcja asercji
assert i>0;
assert i>=0: "i (" + i + ") mniejsze od zera";
  • instrukcja wyboru
switch (i){
  default: System.out.println("Wartość spoza zakresu"); break;  // Tak, nie musi być ostatnia!
  case 1: case 2: System.out.println("Dodatnie"); break;
  case -1: case -2: System.out.println("Ujemne"); break;
  case 0: System.out.println("Zero"); break;
}
  • pętla dopóki
while (i>0)
   i--;
  • pętla wykonuj
do 
  i++ 
while ( i < 0 );
  • pętla dla (wersja podstawowa)
for(int j = 0; j<tab.length; j++)
  tab[j] = 0;
  • pętla dla (wersja rozszerzona)
for(int elt: tab)
  System.out.print(elt+", ");
  • instrukcja break
break;
break dalej;
  • instrukcja kontynuuj
continue;
continue dalej;
  • instrukcja powrotu
return;
return 0;
  • instrukcja zgłoszenia wyjątku
throw new Wyjątek();
  • instrukcja synchronizująca (zagadnienia współbieżności pomijamy)
  • instrukcja try
try{
  i = i/k;
}
catch (ArithmeticException e){
  System.out.println("Dzielenie przez zero");
}
finally{
  System.out.println("Kończymy");
}
  • instrukcja bloku (umożliwia deklarowanie zmiennych i klas lokalnych)
{
  int i = 1; 
  i++;
  int j = i + 1;
  return i+j;
}

Jak widać deklaracje zmiennych lokalnych można mieszać z instrukcjami bloku. Zmienna lokalna jest widoczna w bloku od miejsca deklaracji do końca bloku. Nie można deklarować w bloku zmiennej o nazwie już widocznej w bloku (np. zdefiniowanej na zewnątrz bloku).


Przypisy

  1. Zdarzają się wprawdzie także w Javie problemy z przenoszeniem oprogramowania, ale są one zwykle spowodowane nie niezgodnością samych maszyn wirtualnych, lecz specyficznymi cechami środowisk, w jakich te programy się uruchamia. Ale należy mocno podkreślić, o ile w przypadku większości języków programowania normą jest to, że uruchomienie programu na maszynie o innej architekturze wymaga co najmniej ponownego skompilowania programu, o tyle w przypadku Javy regułą jest to, że program działa na każdym komputerze tak samo, bez żadnych dodatkowych zabiegów.
  2. W tym przykładzie słowo public przed deklaracją klasy można pominąć.
  3. Składniowo wygląda to tak samo jak deklaracja funkcji nie dającej wyniku w C (skąd wzięła się ta składnia), ale w C słowo void nie jest słowem kluczowym, lecz nazwą typu mającego zero wartości).
  4. W dalszej części zamiast pisać "obiekt będący wartością zmiennej x" będziemy pisać po prostu "obiekt x", co wprawdzie jest nieco mniej precyzyjne, ale za to znacznie krótsze.
  5. Także znak "$", ale nie powinno się go używać we własnych identyfikatorach ze względu na specjalną funkcję, którą pełni.
  6. Dokładniej UTF-16, czyli 16-bitowej wersji Unicodu.
  7. Zauważmy, że 015 oznacza liczbę 13. Ta paskudnie wybrana notacja do zapisu liczb ósemkowych pochodzi z języka C (jak chyba wszystkie nieeleganckie konstrukcje w Javie oraz, co wypada przyznać, spora część pozostałych konstrukcji składniowych Javy).
  8. Formalnie rzecz biorąc największą wartością jest 2147483648, ale można jej użyć tylko jako argumentu jednoargumentowego operatora -, czyli nieco nieformalnie można powiedzieć, że zakres literałów całkowitoliczbowych to -2147483648..2147483647. Analogiczna sytuacja ma miejsce dla literałów typu long, czyli możemy przyjąć, lekko upraszczając, że ich zakresem jest -9223372036854775808..9223372036854775807
  9. Największa skończona, dodatnia wartość literału rzeczywistego to 3.4028235e38f, zaś literału podwójnej precyzji to 1.7976931348623157e308. Najmniejsze dodatnie wartości to odpowiednio 1.40e-45f i 4.9e-324. Znajomość tych liczb nie będzie potrzebna w dalszej części tego wykładu. Warto tylko dodać, ze specyfikacja Javy określa nawet standard reprezentowania tych liczb w pamięci (jako IEEE 754).
  10. Pojęcie operatora nie jest do końca ściśle zdefiniowane w specyfikacji języka Java. Na przykład lista operatorów z rozdz. 3.12 specyfikacji języka oddzielnie wymienia ? i :, które stanowią jeden operator, nie wymienia zaś instanceof, które w dalszej części specyfikacji konsekwentnie jest nazywane operatorem. Na szczęście dla nas te subtelności nie mają znaczenia.
  11. Kwestia zaliczenia typu char do typów liczbowych budzi (słusznie) sporo kontrowersji. Kto ma wątpliwości, niech napisze w programie w Javie na przykład tak: System.out.println('a'*5+3);. Sposób traktowania wartości znakowych został przeniesiony do Javy z języka C.
  12. Tablice są w Javie traktowane jak obiekty, mimo że typy tablicowe nie są typami klasowymi. To dość subtelna kwestia, standard języka dzieli typy referencyjne na klasowe i tablicowe (oraz interfejsy), ale egzemplarze tablic są traktowane np. przez operator instanceof jak obiekty klasy Object, zaś tablice jako podklasy klasy Object.