autor: Andrzej Gąsienica-Samek
Java jest prostym językiem obiektowym. Jej podstawowe cechy to:
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.javaSpowoduje 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
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.
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=3Program 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:
boolean | typ logiczny |
int | 32 bitowa liczba całkowita ze znakiem |
long | 64 bitowa liczba całkowita ze znakiem |
char | 16 bitowa liczba całkowita bez znaku |
byte, short | odpowiednio 8 i 16 bitowa liczba całkowita |
float, double | odpowiednio 32 i 64 bitowa liczba zmiennoprzecinkowa |
referencja do obiektu | referencja 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. | Postfiksowe | x.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.
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ą.
W Javie można używać następujących konstrukcji:
Instrukcja | Przykł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; } } } |
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.
W Javie wiele deklaracji może być poprzedzonych modyfikatorem dostępu (dostępności). Modyfikator ustala widoczność danej deklaracji.
Modyfikator | Znaczenie |
public | dostęp z każdego miejsca |
protected | dostęp tylko z pakietu deklaracji oraz w klasie rozszerzającej |
default | dostęp tylko z pakietu deklaracji |
private | dostę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;
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:
Operacja | Przykł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.
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.
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 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ą.