Elementarz Java #1 – Podstawy Java
Wstęp
Ten artykuł rozpoczyna serię #ElementarzJava, poświęconej specyfikacji języka Java. Na początku chciałbym przybliżyć Ci, kilka podstawowych rzeczy związanych z Javą. W dzisiejszym materiale, poruszyłem takie zagadnienia jak kompilacja kodu źródłowego, widoczność zmiennych, struktura klasy, bloki inicjalizujące, użycie metody main
i wiele innych… Zachęcam do lektury całego wpisu od początku do końca oraz samodzielnego uruchomienia i przetestowania umieszonych tutaj przykładów. Pamiętaj, że najlepszy efekt w nauce osiągniesz, jeśli wszystko samodzielnie dokładnie sprawdzisz. W razie pytań, zachęcam do udzielenia się w sekcji komentarzy dostępnej pod artykułem. Tymczasem, życzę miłej lektury!
Seria #ElementarzJava składa się z następujących artykułów:
- Podstawy języka Java (kompilacja, zmienne, struktura klasy, pakiety),
- Typy danych w języku Java (deklaracja i inicjalizacja zmiennych, różnica między typami, garbage collection, typy opakowujące),
- Operatory i konstrukcje warunkowe w Java (użycie operatorów, porównywanie obiektów, instrukcje:
if
,if/else
,switch
), - Tablice (charakterystyka tablic, tablice jedno i wielowymiarowe),
- Pętle (tworzenie pętli, instrukcje
break
icontinue
, etykiety), - Metody oraz hermetyzacja (metody statyczne, przeciążanie metod, konstruktory, modyfikatory widoczności, hermetyzacja elementów klasy, parametry),
- Dziedziczenie (implementacja, przysłanianie metod, polimorfizm, rzutowanie),
- Obsługa wyjątków (kategorie wyjątków, łapanie wyjątków, klasy wyjątków),
- API Java (
String
,StringBuilder
, data i czas, kolekcje, wyrażenia lambda).
Jesteś tu pierwszy raz? Polecam rozpocząć lekturę od materiału: Elementarz Java #0 – wprowadzenie.
Kompilacja kodu źródłowego Javy
Języki programowania dzielą się na interpretowane oraz kompilowane. Java należy do tej drugiej kategorii. Aby uzyskać działający program kod napisany przez programistę musi zostać wpierw skompilowany do postaci kodu maszynowego.
Pracując przy projektach programistycznych opartych o Java, kod źródłowy (ang. source code) zapisywany jest w plikach z rozszerzeniem .java
. To jednak nie wszystko bowiem kiedy dokładnie przyjrzymy się strukturze plików i katalogów możemy również zauważyć pliki mające końcówkę .class
. Jest w nich zapisywany wykorzystywany do kompilacji programu tak zwany bytecode
.
Kod bajtowy Javy to nic innego jak lista instrukcji do wykonania przez wirtualną maszynę Javy (JVM). W ramach ciekawostki można dodać, że każdy kod operacji kodu bajtowego ma dokładnie jeden bajt długości.
Oczywiście programista Java w swojej codziennej pracy nie musi wiedzieć, jak działa kod bajtowy Javy. Warto jednak mieć choćby minimalną świadomość o tym jakie procesy zachodzą podczas kompilacji kodu źródłowego.
Plik z kodem źródłowym Java można skompilować bezpośrednio w IDE, ale też z poziomu konsoli systemowej. Korzystając z terminala możemy to zrobić na dwa sposoby:
- wywołując instrukcję
javac MyProgram.java
- wywołując instrukcję
java MyProgram
gdzie MyProgram
to nazwa pliku z kodem źródłowym, który chcemy skompilować.
Warto również dodać, że MyProgram
to nie tylko nazwa pliku, ale też nazwa publicznej klasy jaka w tym pliku została zapisana. Co jednak, kiedy w jednym pliku źródłowym stworzymy dwie klasy – jedną publiczną – a drugą bez żadnego modyfikatora dostępu?
Dwie klasy w jednym pliku
Mamy plik: MyFirstProgram.java
, którego zawartość wygląda następująco:
public class MyFirstProgram { public boolean hasName; } class MySecondClass { public String name; }
Po jego skompilowaniu zostaną wygenerowane dwa pliki bytecode: MyFirstProgram.class
oraz MySecondClass.class
.
Przykładu z dwoma klasami publicznymi w jednym pliku nie omawiam – po prostu dostaniemy błąd kompilacji.
Widoczność zmiennych
Nie do każdej zmiennej mamy dostęp z dowolnego miejsca kodu. Jest to chyba dość oczywiste. Pomijając jednak paradygmat programowania przy użyciu obiektów oraz cały szereg modyfikatorów widoczności (będzie o tym w dalszej części kursu) warto zwrócić uwagę na to co dzieje się, kiedy do akcji wchodzą różnego rodzaju bloki. Pisząc „bloki” mam tutaj na myśli instrukcje takie jak if
, while
, do-while
, for
, czy też try…catch
.
Zmienne, a bloki (if, while, …)
Tworząc zmienne wewnątrz bloków takich jak pętle czy też instrukcje warunkowe należy pamiętać o tym, że nie będziemy mieli do nich dostępu poza danym blokiem.
Mamy pętlę while
wewnątrz której stworzyliśmy zmienną variable
:
public static void main(String[] args) { while(true) { int variable; } variable = 1; }
Powyższy kod nie zostanie skompilowany. Do zmiennej variable
mamy dostęp tylko i wyłącznie wewnątrz pętli while
.
Zupełnie inna sytuacja będzie, kiedy zmienna zostanie utworzona poza pętlą, a dostęp do niej będziemy chcieli uzyskać wewnątrz bloku:
public static void main(String[] args) { int variable; while(true) { variable = 1; } }
Taki kod zostanie skompilowany i uruchomiony poprawnie.
Analogiczna sytuacja występuje w przypadku instrukcji warunkowej if
:
public static void main(Sting[] aros) { if(true) { int variable; } variable = 1; }
Powyższy kod nie zostanie skompilowany. Identycznie jak przy pętli while
poza instrukcją warunkową if
dostępu do zmiennej variable
nie mamy.
Te same reguły dotyczą również „gołych” bloków, które możemy sobie wstawić w dowolnym miejscu kodu.
public static void main(String[] args) { { } //jakiś blok { int variable; } variable = 1; }
Kod ten nie zostanie skompilowany.
Struktura klasy w Javie
Klasa w Javie ma swoją ściśle określoną strukturę. Składa się ona z pól, konstruktorów metod oraz statycznych i niestatycznych bloków inicjalizujących.
Konwencja JavaBeans
Tak na marginesie dodam, że przy większych projektach stosuje się nawet odpowiednie nazewnictwo metod, które zostało opracowane już w latach 90 przez firmę Sun Microsystem. Mowa tutaj o konwencji JavaBeans. Sprowadza się ona do tego, że tak zwane metody dostępowe – umożliwiające modyfikację bądź odczyt wartości z prywatnych pól – powinny mieć ściśle określone nazewnictwo, które prezentuje poniższy przykład:
public boolean isCanRun() { return canRun; } public int getNumber { return number; } public void setCanRun(boolean value) { canRun = value; }
Nie będę tutaj rozwodził się na ten temat. Jeśli interesują Ciebie szczegóły to w internecie można znaleźć sporo artykułów poświęconych temu zagadnieniu.
Przejdźmy zatem do struktury naszej klasy.
Klasa i bloki inicjalizujące
Zastanów się przez chwilę jaki rezultat otrzymamy po skompilowaniu i uruchomieniu poniższego kodu:
public class Haha { { System.out.print("Hello "); } public Haha { System.out.print("world"); } public static void main(String[] args) { Haha haha = new Haha(); { System.out.print("!"); } } }
Jeśli już wiesz to gratuluję! A jeśli nie to spieszę z wyjaśnieniem…
Ciało klasy Haha
zawiera takie elementy jak:
- blok inicjalizujący,
- publiczny konstruktor,
- statyczną metodę
main
.
Jest to klasa publiczna i zawierająca publiczną i statyczną metodę main(String[] args)
więc jak łatwo się domyślić po kompilacji zostanie uruchomiony znajdujący się w niej kod. Od niego więc zaczynamy całą analizę.
Na samym początku tworzony jest instancja obiektu Haha
. W pierwszej kolejności maszyna wirtualna uruchamia kod jaki znajduje się w bloku inicjalizującym. Na ekranie pojawia się napis „Hello
„. Dalej wywoływany jest bezargumentowy konstruktor. Nasza konsola wyświetla już tekst „Hello world
”. Na tym kończy się proces tworzenia instancji obiektu Haha
. Kolejno wywoływana jest następna instrukcja znajdująca się w metodzie main
. W tym przykładzie dodatkowo została ona opakowana nawiasami klamrowymi, czyli znajduje się w osobnym bloku. To jednak nie ma żadnego znaczenia dla działania naszego programu. Koniec końców na ekranie widzimy napis:
Hello world!
Tak więc, Witaj świecie!
Bloki inicjalizujące
Zabawa z blokami inicjalizującymi może być trochę bardziej ciekawsza niż takie proste ich wykorzystanie jak miało to miejsce w poprzednim przykładzie wypisującym „Hello world!
” na ekran komputera.
Co się stanie, jeśli do gry wejdą na przykład zmienne statyczne? Sprawdźmy to!
Statyczne bloki inicjalizujące
Mamy tutaj klasę Car
oraz Driver
. Klasa Car
zawiera publiczne i statyczne pole number
typu int
. Dodatkowo mamy statyczny blok inicjalizujący i statyczną metodę start()
.
Klasa Driver
jest już natomiast nieco prostsza w swojej budowie. Mamy tutaj statyczne wywołanie metody start()
klasy Car
, wywołanie tej samej metody tyle, że z poziomu obiektu oraz instrukcję wypisującą na ekran zawartość zmiennej statycznej number
.
class Car { public static int number = 1; static { number = 2; } public static void start() { System.out.println("start"); } } public class Driver { public static void main(String[] args) { Car.start(); new Car().start(); System.out.println(Car.number); } }
Zastanówmy się jaki będzie efekt uruchomienia powyższego kodu?
W pierwszej kolejności wywołujemy statyczną metodę start()
, na ekranie pojawia się napis „start
”. Dalej uruchamiamy tą samą metodę tyle, że z poziomu utworzonego wcześniej obiektu. Nie ma to jednak żadnego znaczenia i ponownie na konsoli pojawia się napis „start
”. Na samym końcu jest już nieco ciekawiej. Próba wypisania zawartości statycznej zmiennej number
powoduje uruchomienie statycznego konstruktora inicjalizującego. Tak więc do tego wszystkiego dokładamy liczbę „2
”. Żeby być jednak bardziej precyzyjnym trzeba napisać, że pole number
wpierw inicjalizowane jest przez liczbę 1
ale zaraz po tym zostaje to zmienione przez blok inicjalizujący.
Końcowy efekt:
start start 2
Inicjalizacja pól statycznych
Jeśli już zajmujemy się polami statycznymi to warto wspomnieć o pewnej ich własności, a w zasadzie sposobie w jaki funkcjonują. Posługując się nimi tak naprawdę operujemy na referencji do adresu w pamięci komputera, gdzie przechowywana jest ich zawartość. Oznacza to dla nas tyle, że dowolna modyfikacja zwartości takiego pola w dowolnym momencie oraz kontekście cały czas wpływa na tą samą wartość.
Prześledźmy sposób działania poniższego kodu:
class Test { static String name = ""; { name += "A"; } static { name += "B"; } { name += "C"; } } public class Example { public static void main(String[] args) { System.out.println(Test.name); System.out.println(Test.name); //statyczny blok inicjalizujący będzie uruchomiony tylko raz więc tutaj nic się nie stanie new Test(); new Test(); System.out.print(Test.name); } }
Mamy tutaj statyczne wywołanie pola name
z klasy Test
. Jak już wiemy taka konstrukcja w naszym przypadku spowoduje inicjalizację pola name
na wartość „”, a następnie zostanie do tego pustego stringa dodana litera „B
”. Na ekranie zobaczymy więc „B
”. Dalej mamy ponowne odwołanie się do tego samego pola. Tutaj jednak należy wiedzieć, że statyczny blok inicjalizujący będzie uruchomiony tylko raz więc nic się nie zmieni i zobaczymy znowu „B
”. Kolejno dwukrotnie tworzymy instancję klasy Test
. Co powoduje dwukrotne uruchomienie bloków inicjalizujących. Będą one wywoływane w takiej kolejności w jakiej znajdują się w klasie (od góry do dołu). Do naszego statycznego pola name
zostanie więc dodany ciąg „ACAC
”.
Tutaj może się pojawić kilka pytań.
Pierwsze, dlaczego nie został wywołany statyczny blok inicjalizujący? Stało się tak, ponieważ w tym przypadku nie odwoływaliśmy się statycznie do danego pola, ale po prostu tworzyliśmy instancję klasy. W takim wypadku statyczny blok inicjalizujący nie jest wywoływany.
Drugie, dlaczego blok inicjalizujący został uruchomiony dwa razy, a blok statyczny tylko jeden raz, mimo że dwukrotnie odwoływaliśmy się do pola name
? Wiąże się to ze wspominanym wyżej sposobem w jaki funkcjonują statyczne pola. Operując na nich tak naprawdę cały czas działamy na jednej „instancji”, jednej wartości, którą modyfikujemy niezależnie od kontekstu. Blok inicjalizujący jak sama nazwa wskazuje służy do inicjalizacji więc jeżeli już dana zmienna została zainicjalizowana to nie ma potrzeby uruchamiania go ponownie. Jeśli jednak tworzymy nowy obiekt klasy to tworzymy jej nową instancję. Niestatyczne pola inicjalizujące będą więc uruchomiane za każdym razem, kiedy taka instancja jest tworzona. Stąd w naszym przypadku zostały wywołane dwukrotnie.
Końcowy efekt:
B B BACAC
Konstruktor czy blok inicjalizujący. Co jest wywoływane wcześniej?
Co jest wywoływane pierwsze? Konstruktor czy blok inicjalizujący? Odpowiedź na to pytanie jest prosta. Zgodnie ze specyfikacją języka Java blok inicjalizujący wywoływany jest przed konstruktorem. Nie ma tutaj znaczenia kolejność umieszczenia tych elementów w klasie jak ma to miejsce w przypadku kilku bloków inicjalizujących. Przyjrzyjmy się prostemu przykładowi.
public class Question { String result = "A"; { result += "B"; } public Question() { result += "C"; } public static void main(String[] args) { Question q = new Question(); System.out.print(q.result); } }
Po uruchomieniu powyższego kodu na ekranie komputera zostanie wyświetlony napis „ABC
”.
UWAGA: W bloku inicjalizującym statycznym można modyfikować tylko statyczne (oczywiście nie finalne) pola. W bloku inicjalizującym (zwykłym) można edytować statyczne i niestatyczne pola.
Inicjalizacja pola w klasie innym polem
Popatrzmy na poniższy kod.
public class A { int a = 1; int b = a; public A() { a = 2; } public static void main(String[] args) { A a = new A(); System.out.print("a = " + a.a + ", b = " + a.b); } }
Mamy tutaj pole a
, które jest inicjalizowane jedynką oraz pole b
inicjalizowane wartością pola a
. Jaki będzie efekt działania takiego programu? Nie ma tutaj nic skomplikowanego, po utworzeniu instancji klasy A
, pole a
zostanie zmodyfikowane na 2
, natomiast pole b
dalej będzie miało wartość taką jaka została przypisana przed uruchomieniem konstruktora. Dla przypomnienie zaznaczam, że pola w klasie inicjalizowane są wartością, która przypisywana jest w momencie ich utworzenia, następnie blokiem inicjalizujący, a na końcu przez konstruktor.
O wartościach domyślnych oraz zmiennych globalnych, lokalnych i instancyjnych będzie w dalszej części kursu.
Użycie metody main
Stare strażackie porzekadło mówi, że nie ma dymu bez ognia. W świecie programisty Java można by powiedzieć, że nie ma aplikacji bez metody main
. W dzisiejszych czasach, nowoczesne IDE pozwalają nam zapomnieć o tym fragmencie kodu, generując go automatycznie. Warto jednak mieć świadomość tego w jaki sposób metoda ta może zostać zdefiniowana oraz jakie są jej różne „warianty”.
Definiowanie metody main
Metoda main
jest z definicji publiczna i statyczna jedyną dopuszczalną zmianą jest jedynie zamiana kolejnością słowa kluczowego public
ze słowem static
.
Możemy zrobić tak:
public static void main(String[] args) { }
lub tak:
static public void main(String[] args) { }
Definiowanie metody main (argumenty)
Ciekawsza jest zabawa argumentami. Zgodnie ze specyfikacją Javy metoda main przyjmuje tablice Stringów. Można ją jednak zadeklarować w dowolny sposób.
public static void main( ... ) { }
w miejsce „…” wstawić możemy przykładowo następujące kombinacje:
String[] _args
String args[]
String _args[]
String… $args
Więcej na temat definiowana tablic w Javie piszę w dalszej części kursu.
Przekazywanie danych jako argumenty metody main
Podczas kompilacji możemy przesyłać do aplikacji dodatkowe informacje. W kodzie programu możemy nimi operować odwołując się do tablicy stringów, którą przyjmuje metoda main
.
public class MyFirstClass { public static void main(String[] args) { System.out.println(args[0]); } }
Jeśli chcemy, aby powyższy program wypisał na konsoli napis „Hello World
” należy skompilować go używając następującej konstrukcji:
java MyFirstClass "Hello World"
Importowanie pakietów w Javie
Aplikacje napisane w języku Java to nie zawsze małe „studenckie” programy, składające się z dwóch lub trzech klas mających po parę linijek kodu. Dość często są to naprawdę spore projekty, na które składa się bardzo dużo zależności i wzajemnych powiązań. Aby to wszystko miało ręce i nogi, a programiści, nie musieli codziennie rano rozkładać na biurku „mapy”, wymyślono coś takiego jak pakiety.
Pakiet jest to nic innego jak pewny zbiór zawierający tematycznie powiązane ze sobą klasy czy też interfejsy. Sama zresztą Java dostarcza cały szereg różnych pakietów np. java.lang
, java.io
, java.util
. Mamy tego całkiem sporo. Warto więc wiedzieć jakie są zasady odnośnie ich importowania oraz tworzenia nowych.
Domyślne importy
Na początek warto wspomnieć o tak zwanych „domyślnych importach”. Kiedy tworzymy nową klasę lub też interfejs możemy ale nie musimy importować pakietu java.lang
, który to zawiera podstawowe klasy Javy. Przykładowo poniższe dwa importy są zupełnie zbędne:
import java.lang.*;
import java.lang.System;
To, że nie ma ich w kodzie nie oznacza dla programisty żadnych „problemów”. Zostaną one automatycznie dodane przez kompilator.
Import klas z innych pakietów
Przeanalizujmy poniższy kod:
package home.room; public class MyRoom { }
package guest; ... public class HomeGuest { public void guestSetting(MyRoom room) { } }
Co możemy wstawić w miejsce “…
”? Jak możemy zauważyć w klasie HomeGuest
znajdującej się w pakiecie guest
mamy metodę guestSetting
, która jako argument przyjmuje obiekt klasy MyRoom
. Musimy więc zaimportować klasę MyRoom
. Możemy to zrobić na dwa sposoby.
import home.room.MyRoom;
albo:
import home.room.*;
Pierwsze rozwiązanie importuje z pakietu home.room
tylko jedną klasę o nazwie MyRoom
. W drugiej wersji importujemy wszystkie klasy jakie znajdują się w pakiecie home.room
.
Niepoprawne importy:
import home.*;
import home.room.*.myRoom;
import home.room.*.myRoom.*;
Klasa o tej samej nazwie
Dzięki pakietom możemy w ramach jednego projektu stworzyć kilka klas o tej samej nazwie. W takiej sytuacji może się jednak pojawić pytanie jak poprawnie zaimportować odpowiedni obiekt? Zasada tutaj jest dość prosta, dokumentacja Javy jasno określa, że pierwszeństwo importu ma nazwa klasy. Zobaczmy, jak wygląda to w praktyce.
package home; public class Room { boolean isOpen = false; }
package home.myHome; public class Room { boolean isOpen = true; }
package guest; ... public class Guest { Room room; }
Co możemy wstawić w miejsce „…
”? Jak widać, w klasie Guest
mamy pole typu Room
. Musimy więc zaimportować tą klasę. Jest tutaj jedna dodatkowa trudność. Klasa Room
jest w dwóch pakietach. Możemy importować zarówno klasę Room
z pakietu Home
jak i klasę Room
z pakietu home.myHome
. Będzie to wyglądało tak.
Import z pakietu home
. Wersja pierwsza (dość oczywista):
import home.*;
Wersja druga (tutaj musimy pamiętać o tym, że pierwszeństwo będzie miał import, który wskazuje bezpośrednio nazwę klasy:
import home.room; //zaimportuje Room z home przez nazwę klasy co ma pierwszeństwo import home.myHome.*;
Import klasy Room
z pakietu home.myHome
:
import home.*; import home.myHome.Room;
Nazewnictwo pakietów
W poprzednich przykładach poza prostymi nazwami pakietów typu „Home
” używałem również takich konstrukcji jak „home.room
” czy też „home.myRoom
”. Skąd się one biorą? Otóż nie wymyśliłem sobie ich dla zabawy, a ma to większe znaczenie. Przenalizujmy poniższy przykład.
Załóżmy, że w katalogu, którego ścieżka jest podana niżej, mamy plik z kodem źródłowym Javy zawierający klasę Room
:
/my/directory/PartOne/HomeAssistant/Room.java
Cały projekt znajduje się w katalogu:
/my/directory
Jaką więc nazwę pakietu powinna mieć klasa Room
? Odpowiedź wygląda tak:
package partOne.homeAssistant;
Nazwa pakietu reprezentuje foldery pod bieżącą ścieżką, która w tym przypadku wygląda tak: partOne.homeAssistant
.
Import statycznych elementów
Java dopuszcza jeszcze jedną ciekawą funkcjonalność. Mając elementy statyczne w klasie, która znajduje się w innym pakiecie, możemy za pomocą odpowiedniego importu korzystać z nich tak jak by były one zdefiniowane bezpośrednio w klasie, na której pracujemy. Przeanalizujmy poniższy kod.
package car; public class Car { public static int number = 1; }
package driver; import static car.Car.*; public class Driver { public static void main(String[] args) { System.out.print(number); //możemy tak zrobić bo mamy import statycznych elementów z klasy Car pakietu car } }
Metoda main
umieszczona w klasie Driver
ma zaimplementowaną instrukcję, która wypisuje na ekran zawartość zmiennej statycznej number
odnosząc się do niej tak jak by była zdefiniowana w tej właśnie klasie. Mogliśmy tak zrobić dzięki importowi statycznemu.
Ogólna jego składnia wygląda następująco:
import static nazwaPakietu.nazwaKlasy.*; //import wszystkich elementów statycznych z klasy nazwaKlasy
Kolejność import, package, class
Ostatnią rzeczą jaką chciałem jeszcze poruszyć, a związaną z tematyką pakietów oraz importów jest to w jakiej kolejności należy umieszczać poszczególne elementy. Przyjrzyjmy się kilku dopuszczalnym formom.
package myPackage; import java.util.*; class MyClass { }
import java.util.*; class MyClass { }
package myPackage; class MyClass { }
Zamiana kolejnością package
i import
nie jest dopuszczalna.
Konstruktor czy blok inicjalizujący. Co jest wywoływane wcześniej?
Wydaje mi się, że masz tam błąd (Question, Example)
Hej Dawid, dzięki za czujność. Faktycznie był tam błąd w kodzie, pomyliłem nazwy obiektów. Mam nadzieję, że to drobne niedopatrzenie nie wpłynęło na Twoją naukę :)