Wstęp do Javy dla zaawansowanych

autor: Andrzej Gąsienica-Samek

Cechy Javy

Java jest prostym językiem obiektowym. Jej podstawowe cechy to:

Hello world

Program wypisujący na ekran "hello, i'm jan b." wygląda następująco:

    class Hello {
        public static void main(String[] args) {
            System.out.println("hello, i'm jan b.");
        }
    }
Kod źródłowy Javy zapisujemy w plikach z rozszerzeniem .java . Zazwyczaj w pliku źródłowym znajduje się jedna klasa i plik nosi nazwę tej klasy. W naszym przypadku może to być Hello.java . Aby go skompilować potrzebujemy Java Development Kit (JDK), który można ściągnąć ze strony java.sun.com. Następnie uruchamiamy kompilator za pomocą polecenia:
    javac Hello.java
Spowoduje to kompilację klasy do tzw. bajtkodu w pliku Hello.class . Teraz aby uruchomić nasz program należy użyć środowiska uruchomieniowego Javy (JRE), które znajduje się w JDK (jak również jest dostępne jako osobny, darmowy produkt). Nasz program uruchamiamy za pomocą polecenia:
    java Hello

Program

Nasz program składa się z deklaracji klasy Hello, która zawiera jedną statyczną (nie związaną z instancją klasy) metodę main. Każda metoda w Javie musi być zdefiniowana w pewnej klasie. Metoda main jako argument przyjmuje tablicę napisów. Następnie korzystamy z klasy System i jej pola statycznego out. Zawiera ono referencję do obiektu służącego do wypisywania komunikatów na konsolę. Obiekt ten posiada metodę println.

Typy proste i wyrażenia

Przyjrzyjmy się następującemu programowi:
    class Test1 {
        static void f(int a, int b, int c) {
            System.out.println("f: a="+a+" b="+b+" c="+c);
            a = a + b * c;
            System.out.println("f: a="+a+" b="+b+" c="+c);
        }

        public static void main(String[] args) {
            int a = 1, b = 2, c = 3;
            System.out.println("main: a="+a+" b="+b+" c="+c);
            f(a, b, c);
            System.out.println("main: a="+a+" b="+b+" c="+c);
        }
    }
Wynik działania tego programu to:
    main: a=1 b=2 c=3
    f: a=1 b=2 c=3
    f: a=7 b=2 c=3
    main: a=1 b=2 c=3
Program pokazuje podobieństwa składni ze składnią C. Jedynym niezrozumiałym elementem tego programu jest dodawanie liczb do napisów. Otóż w Javie wynikiem takiego dodawania jest przekształcenie liczby w napis, a następnie połączenie napisów. Program ten pokazuje, że typ int jest liczbą oraz, że jest przekazywany przez wartość. Wyrażenia zachowują się w naturalny sposób. Java posiada wbudowany zestaw typów prostych, którego nie można rozszerzać. Wszystkie zmienne w Javie są typu prostego. Wartości typów prostych (a więc wszystkich zmiennych i parametrów) są zawsze przekazywane przez wartość (przy przekazywaniu są kopiowane). Typy te to:

booleantyp logiczny
int32 bitowa liczba całkowita ze znakiem
long64 bitowa liczba całkowita ze znakiem
char16 bitowa liczba całkowita bez znaku
byte, shortodpowiednio 8 i 16 bitowa liczba całkowita
float, doubleodpowiednio 32 i 64 bitowa liczba zmiennoprzecinkowa
referencja do obiektureferencja zawiera adres obiektu w pamięci

Typy proste, poza typem referencyjnym nazywa się typami pierwotnymi. Zauważ, że zmienna nie może być typu obiektowego, a jedynie może zawierać referencję do obiektu. Tak więc przy przekazywaniu parametrów nigdy nie skopiujemy obiektu, a jedynie referencję do niego.

W Javie zdefiniowano następujące operatory, według priorytetu:

1.Postfiksowex.y f(x) a[x] x++ x--
2.Prefiksowe+ - ! ~ ++x --x (T)x
3.Multiplikatywne* / %
4.Addytywne+ -
5.Przesunięcia bitowe<< >> >>>
6.Relacyjne< > <= >= instanceof
7.Porównania== !=
8.Bitowe i logiczne I&
9.Bitowe i logiczne XOR^
10.Bitowe i logiczne LUB|
11.Warunkowe I&&
12.Warunkowe LUB||
13.Wyrażenie warunkowe?:
14.Przypisania= *= /= %= += -= <<= >>= >>>= &= ^= |=

Z podanych operatorów większość ma takie samo znaczenie jak w C. Nieznanym operatorem może być >>> , które oznacza przesunięcie bitowe w prawo bez powielania najbardziej znaczącego bitu, czyli przesunięcie bitowe na liczbach bez znaku w C. O operatorze . i instanceof powiemy później. Operatory te zazwyczaj przyjmują parę argumentów typu int, mogą również przyjmować argumenty typów long, float, double. W razie potrzeby wykonywane są niejawne promocje typów.

Stałe

Aby zadeklarować pewnien identyfikator jako stałą należy poprzedzić go słowem kluczowym final i zadeklarować podobnie do zmiennej. Pamiętaj przy tym, że stała jest jedynie wartość przechowywanego typu. Jeśli zadeklarujesz referencję jako stałą, to nie będzie możliwości zmiany tej referencji, ale obiekt będzie można zmieniać w dowolny sposób!

Konwersje typów pierwotnych

W Javie niejawna konwersja typu liczbowego może zostać wykonana, jeśli dziedzina typu podlegającego konwersji zawarta jest w dziedzinie typu wynikowego. Dodatkowo każdą liczbę typu całkowitoliczbowego można niejawnie przekształcić na liczbę zmiennoprzecinkową. Tak więc dostępne niejawne konwersje to:

    char -> int
    byte -> short -> int -> long -> float -> double

Poza nimi istnieją jawne konwersje między dowolnymi typami liczbowymi:

    (typ) wartość

W Javie nie istnieją konwersje między liczbami a typem logicznym, lub referencją.

Konstrukcje języka

W Javie można używać następujących konstrukcji:

InstrukcjaPrzykład
Instrukcja warunkowa
public static void main(String[] args) {
    if (args.length != 1)
        System.out.println("Wymagany jeden argument");
    else
        System.out.println("Argument: "+args[0]);
}
Pętla for
public static void main(String[] args) {
    for (int i=0; i<args.length; ++i)
        System.out.println(args[i]);
}
Pętla while
public static void main(String[] args) {
    int i = 0;
    while (i < args.length) {
        System.out.println(args[i]);
        ++i;
    }
}
Pętla do
public static void main(String[] args) {
    double d;
    do {
        d = Math.random();
    } while (d < 0.5);
    System.out.println("Liczba losowa: "+d);
}
Sterowanie pętlą
public static void main(String[] args) {
    int i = 0;
    while (true) {
        System.out.println(args[i++]);
        if (i < args.length)
            continue;
        break;
    }
}
    
switch
public static void main(String[] args) {
    switch (args.length) {
        case 0:
            System.out.println("Brak argumentów");
            break;
        case 1: case 2: case 3:
            System.out.println("Mało argumentów");
            break;
        default:
            System.out.println("Wiele argumentów");
    }
}
Wyjście z funkcji
static double f(int a) {
    return 1d / a;
}
public static void main(String[] args) {
    if (args.length == 0) {
        System.out.println("Brak argumentów");
        return;
    }
    System.out.println("1/liczbaArg = " + f(args.length));
}
Wyjątki
static void f() throws Exception {
    throw new Exception("ala");
}
public static void main(String[] args) {
    try {
        f();
    } catch (Exception e) {
        e.printStackTrace();
    }
}
Synchronizacja
public class Point {
    int x, y;
    synchronized public void wypisz() {
        System.out.println("Punkt "+x+","+y);
    }
    public static void przesun(Point p, int dx, int dy) {
        synchronized (p) {
            p.x += dx;
            p.y += dy;
        }
    }
}

Klasy, obiekty i referencje

Klasa deklaruje nowy typ. Każdy obiekt jest określonej klasy. Do obiektu odwołujemy się zawsze za pośrednictwem referencji. W klasie mogą znajdować się definicje pól dostępnych w obiekcie oraz metod jakie na obiekcie można wykonywać. Dodatkowo klasa może zwierać konstrutory, służące do inicjowania początkowego stanu obiektu. Aby zadeklarować pola i metody obiektu (inaczej pola i metody instancyjne) należy wyrzucić modyfikator static z definicji. Pola i metody obiektu będziemy nazywali po prostu polami i metodami.
    class Point {
        int x, y;
        Point(int x, int y) {
            this.x = x;
            this.y = y;
        }
        void print() {
            System.out.println("Point("+x+","+y+")");
        }
    }

Klasa Point zawiera dwa pola i jedną metodę oraz jeden konstruktor dwuargumentowy. Spójrzmy na przykład:

    class Test3 {
        public static void main(String[] args) {
            Point p = new Point(5, 3);
            p.y = 7;
            p.print();
        }
    }

Przykład ten demonstruje konstruowanie obiektu klasy za pomocą operatora new. Operator new tworzy nowy obiekt - rezerwuje dla niego miejsce w pamięci, a następnie wykonuje konstruktor w celu zainicjowania obiektu. Wynikiem działania operatora new jest referencja do stworzonego obiektu (jego adres). Pamiętaj, że w Javie wszystkie zmienne są typu prostego. Żadna zmienna nie jest obiektem, zmienna może jedynie wskazywać na pewnien obiekt w pamięci komputera.

Klasy podlegają jednokrotnemu dziedziczeniu. Każda klasa rozszerza niejawnie klasę Object, a więc każdy obiekt jest pośrednio obiektem klasy Object. Dzięki temu każdy obiekt ma metody takie jak:

    public boolean equals(Object);
    public String toString();
    public int hashCode();

Dodatkowo możemy deklarować interfejsy, czyli kontrakty, które obiekty mogą spełniać. Interfejsy mogą wielokrotnie dziedziczyć z innych interfejsów.

    interface Printable {
        void print();
    }

Taka deklaracja mówi, że jeśli obiekty klasy spełniają interfejs Printable to muszą zawierać bezparametrową metodę write. Jeśli obiekty klasy spełniają pewnien interfejs to trzeba to jawnie zadeklarować w definicji klasy. Przykład znajduje się niżej.

Pakiety i kontrola dostępu

W Javie wiele deklaracji może być poprzedzonych modyfikatorem dostępu (dostępności). Modyfikator ustala widoczność danej deklaracji.

Modyfikator Znaczenie
publicdostęp z każdego miejsca
protecteddostęp tylko z pakietu deklaracji oraz w klasie rozszerzającej
defaultdostęp tylko z pakietu deklaracji
privatedostęp tylko z klasy deklaracji

Modyfikator default nie występuje w języku jako słowo kluczowe, zamiast tego jest stosowany do wszystkich składowych klasy, dla których nie podano modyfikatora. W przypadku interfejsu jedynym dostępnym modyfikatorem dla jego składowych jest public.

Każda klasa i interfejs należą do pewnego pakietu. Nazwa pakietu jest ciągiem identyfikatorów oddzielonych kropkami. Pakiety tworzą drzewo w logiczny sposób, ale nie ma to odzwierciedlenia w żadnej konstrukcji języka. Dodatkowo istnieje dokładnie jeden pakiet bez nazwy.

Pliki należące do pewnego pakietu powinny znajdować się w podkatalogu o nazwie pakietu oraz powinny być opatrzone deklaratorem pakietu. Jeśli chcemy stworzyć pakiet pl.edu.mimuw.zpp to należy założyć podkatalog pl/edu/mimuw/zpp, a wszystkie pliki w tym katalogu rozpoczynać od deklaracji pakietu:

    package pl.edu.mimuw.zpp;

Jeśli chcemy używać klas znajdujących się w innych pakietach, to należy poprzedzać je pełną nazwą pakietu, np. java.io.InputStream lub do pliku dodać dyrektywę importu nazw z pewnego pakietu:

    import java.io.*;
lub
    import java.io.InputStream;

Referencje

Zmienne mogą być typu referencyjnego. Dla każdej zmiennej typu referencyjnego musi być zadeklarowana klasa obiektu na jaki ta zmienna wskazuje lub interfejs implementowany przez wskazywany obiekt. Dodatkowo każda zmienna referencyjna może zawierać referencję pustą ozanczaną przez null. Referencja pusta nie wskazuje na żaden obiekt. Operacje dostępne na referencjach:

OperacjaPrzykład
Porównanie
public static void main(String[] args) {
    Integer i1 = new Integer(1);
    Integer i2 = new Integer(1);
    Integer i3 = i1;
    System.out.println("i1==i2 " + (i1==i2)); // fałsz
    System.out.println("i1==i3 " + (i1==i3)); // prawda
}
Odwołanie do składowej obiektu
public static void main(String[] args) {
    Point p = new Point(3, 5);
    p.print();
    p.y = p.x;
}
Badanie obiektu i rzutowanie
static void wypisz(Object o) {
    if (o instanceof Integer) {
        Integer i = (Integer) o;
        System.out.println("Integer: " + i.intValue());
    } else
        System.out.println("Nie Integer " + o);
}
public static void main(String[] args) {
    wypisz(new Object());
    wypisz(new Integer(6));
}

Referencję do obiektu typu A można niejawnie zamienić na referencję do obiektu typu B, jeśli każdy obiekt typu A musi być jednocześnie obiektem typu B, np.:

    void f(A a, B b) {
        b = a;
    }

Jest prawidłowe, jeśli

Referencję do obiektu typu A można jawnie rzutować na referencję do obiektu typu B, jeśli obiekt typu A może być obiektem typu B, w przeciwnym przypadku jest to zabronione. Jeśli w czasie wykonania okaże się, że referencja nie wskazuje na obiekt typu B i nie jest to referencja pusta to zostanie rzucony wyjątek.

Przykład

Spójrzmy na bedziej zaawansowany przykład:
    interface Printable {
        void print();
    }
    class Point {
        int x, y;
        Point(int x, int y) {
            this.x = x;
            this.y = y;
        }
    }
    class PrintablePoint extends Point implements Printable {
        PrintablePoint(int x, int y) {
            super(x, y);
        }
        public void print() {
            System.out.println("Point("+x+","+y+")");
        }
    }
    class Test4 {
        public static void main(String[] args) {
            PrintablePoint prPoint = new PrintablePoint(7, 5);
            prPoint.print();
            Printable pr = prPoint;
            pr.print();
            Point point = prPoint;
            // nie można wykonać point.print()
            Printable pr2 = (Printable) point;
            pr2.print();
            point = new Point(8, 4);
            try {
                // poniższa instrukcja spowoduje błąd
                pr2 = (Printable) point;
            } catch (Throwable t) {
                t.printStackTrace();
            }
        }
    }

W powyższym przykładzie zdefiniowaliśmy interfejs Printable oraz klasę Point. Następnie rozszerzyliśmy klasę Point tworząc klasę PrintablePoint, tak aby obiekty tej klasy można było wypisywać. Zauważ, że jedynie obiekty klasy PrintablePoint można wypisywać. Obiekty klasy Point nadal nie posiadają metody print.

Wyrażenie (Printable) point zwraca referencję przechowywaną w point, jednocześnie sprawdzając, czy obiekt wskazywany przez tą referencję implementuje interfejs Printable.

Dodatkowo należy zwrócić uwagę, że metoda print() w klasie PrintablePoint została zadeklarowana jako public. Jest to konieczne, gdyż została ona zadeklarowana jako public w interfejsie Printable, a implementacja metody nie może osłabiać jest dostępności.

Tablice

W Javie istnieją tylko jednowymiarowe tablie. Tablice są obiektami. Tworzenie tablicy odbywa się z użyciem specjalnej formy operatora new lub podczas kompilacji. Tablica ma pole length oznaczające jej długość. Do elementów tablicy odwołujemy się za pomocą operatora indeksowania []. Elementy tablicy muszą być elementami typów prostych. Rozważmy następujący przykład:

    class Test5 {
        static void wypisz(int[] tab) {
            System.out.println(tab.length);
            for (int i=0; i<tab.length; ++i)
                System.out.println(tab[i]);
        }
        public static void main(String[] args) {
            int[] tab = { 34, 56, 78, };
            wypisz(tab);
            tab = new int[] { 23, 45 };
            wypisz(tab);
            tab = new int[4];
            wypisz(tab);
        }
    }

W przykładzie podano wszystkie trzy możliwości tworzenia tablic. Trzecia metoda tworzy tablicę o określonej wielkości z elementami początkowo równymi wartościom domyślnym typu. Druga metoda tworzy tablicę z określonymi elementami, a pierwsze jest skrótem drugiej, dostępnym podczas deklaracji zmiennej.

Jako, że tablice są obiektami więc zmienne tablicowe są zwykłymi zmiennymi referencyjnymi. Przy przekazywniu tworzona jest kopia samej referencji, a nie obiektu, itd. Referencje do tablic można również niejawnie zamienić na referencje do obiektu Object.

Napisy

Napisy w Javie są obiektami. Tak więc porównanie:

    "Ala" == new String("Ala")

z pewnością da wynik negatywny, gdyż jest to porównanie referencji do, a nie zawartości obiektów. Funkcjonalność napisów implementuje klasa String z pakietu java.lang . Implementacja ta opiera się na tablicy znaków, jednak dla użytkownika sprawia wrażenie niezmienialności - jeśli gdzieś w programie stworzymy obiekt klasy String to nikt nigdy na pewno nie zmieni jego zawartości, gdyż obiekty tej klasy nie udostępniają metod do modyfikacji wewnętrznej tablicy. Jeśli chcemy porównać dwa napisy należy użyć metody equals dostępnej dla dowolnych dwóch obiektów:

    "Ala".equals(new String("Ala"))

powinno już zwrócić wartość prawdziwą, gdyż dokona porównania znak po znaku. Dla napisów został udostępniony specjalny operator +. Jeśli po dowolnej stronie operatora + znajduje się referencja do napisu, to wartości po obu stronach tego operatora są zamieniane na obiekty klasy String, a następnie są sklejane. Wynikiem działania operatora + jest wtedy referencja do tak powstałego obiektu typu String:

    (2+"la").equals("2la")

Zwróci wartość prawdziwą.

That's all Folks!