Informacje o nowych artykułach oraz akcjach edukacyjnych prosto na Twojej skrzynce e-mail!

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!

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:

  1. wywołując instrukcję javac MyProgram.java
  2. 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:

  1. blok inicjalizujący,
  2. publiczny konstruktor,
  3. 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.

Spodobało się?

Jeśli tak, to zarejestruj się do newslettera aby otrzymywać informacje nowych artykułach oraz akcjach edukacyjnych. Gwarantuję 100% satysfakcji i żadnego spamowania!

, , , , , , , , , , , , , ,

Dodaj komentarz

Komentarze (2)

  • Dawid pisze:

    Konstruktor czy blok inicjalizujący. Co jest wywoływane wcześniej?
    Wydaje mi się, że masz tam błąd (Question, Example)

Odpowiedz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Pin It on Pinterest