Java potrafi nieźle namieszać w kodzie, gdy nie zna się jej specyfiki. W pracy komercyjnej używam jej w wersji 8 i równolegle przygotowuję się do zdania certyfikatu OCA8. Dzięki temu zdążyłem poznać wiele smaczków działania Javy. Chciałbym, w krótkiej serii podzielić się z Tobą jej kilkoma ciekawymi aspektami. Potrafią naprawdę zakręcić w głowie 😵. Zapraszam!

Lista wszystkich wpisów dotyczących Zawiłości języka Java:
#1 - Zawiłości języka Java #1
#2 - Zawiłości języka Java #2
#3 - Zawiłości języka Java #3
#4 - Zawiłości języka Java #4

Główne wejście aplikacji

Każdy z nas wie, że aplikacja napisana w Javie powinna mieć główne wejście. Oklepane jest pisanie public static void main(String[] args)na początku. Zachęcam do eksperymentowania ze zmianą poszczególnych elementów sygnatury tej metody, można dowiedzieć się wiele ciekawych rzeczy:

  • Zwiększenie restrykcyjności modyfikatora dostępu (np. z public na protected) poprawnie się skompiluje, ale w trakcie wykonywania otrzymamy Error, który rzuci JVM - punkt wejściowy do programu musi być widoczny globalnie
  • Usunięcie specyfikatora static także spowoduje rzucenie wyjątku podczas uruchomienia - JVM musi mieć możliwość wywołania tej metody bez tworzenia nowego obiektu (nie trzeba deklarować niepotrzebnej instancji klasy, aby wywołać main)
  • Zmiana zwracanego typu nie przypadnie JVM do gustu - program, kiedy kończy swoje działanie to nie ma już potrzeby, aby obsłużyć zwrotkę (w przypadku nieprawidłowego zamknięcia aplikacji zostaniemy poinformowani o tym fakcie przez wyjątek)
  • Użycie innej nazwy niż main także spowoduje wyrzucenie błędu przez program - jest to po prostu przyjęta konwencja, taki identyfikator wejścia do programu
  • Zmiana typu parametru wejściowego, bądź ich ilości osiągnie ten sam negatywny rezultat co powyższe podpunkty - jest to miejsce na przekazanie parametrów, w postaci literałów, niezbędnych do działania programu. Stąd właśnie taka konwencja, która musi pozostać niezmienna (jedyna zmiana jaka jest możliwa to String[] na String...)
  • Istnieje możliwość zmiany nazwy parametru wejściowego na taką, którą dopuszcza kompilator (pierwszym znak musi być literą bądź $ czy _, dalej możemy również korzystać z cyfr, oczywiście nie mogą to być słowa kluczowe 😉)
  • Możliwe jest dodanie opcjonalnego specyfikatora w postaci final

Kolejność modyfikatorów i specyfikatorów w metodach

Modyfikatory dostępu

Tak wygląda przykładowa sygnatura metody. Przedstawiona kolejność musi zostać zachowana. Nie można jej dowolnie modyfikować, trzeba trzymać się tej konwencji. Każdy z punktów może za to przyjmować inne wartości, które poniżej zostaną w skrócie przedstawione:

  1. Modyfikator dostępu - decyduje o widoczności metody, jaki inny komponent będzie mógł z niej korzystać:
    1. public - dostępna dla wszystkich
    2. protected - tylko w obrębie pakietu oraz klas dziedziczących
    3. default (oznacza brak modyfikatora) - tylko w obrębie pakietu
    4. private - tylko w obrębie danej klasy
  2. Opcjonalny specyfikator - w odróżnieniu od modyfikatorów można stosować kilka specyfikatorów w jednej metodzie i ustawić je w dowolnej kolejności (UWAGA: jednak nie wszystkie kombinacje są dozwolone)
    1. static - sprawia, że metoda przynależy do klasy a nie do jej instancji
    2. abstract - metoda nie potrzebuje implementacji (ciała)
    3. final - nie zezwala na nadpisanie bądź przesłanianie metody przez podklasę
    4. synchronized - synchronizuje daną metodę pomiędzy wątkami
    5. native - służy do interakcji z kodem napisanym w innym języku takim jak np. C++ (nigdy się z tym nie spotkałem)
    6. strictfp - wymusza równe traktowanie liczb zmiennoprzecinkowych na różnych platformach systemowych, aby zapewnić przenośność programu (nigdy się z tym nie spotkałem)
  3. Zwracany typ - może to być typ prymitywny (boolean, byte, short, int, long, floar, double, char), void (brak zwracanego wyniku) bądź też dowolna klasa
  4. Nazwa metody - dowolna nazwa, które spełnia wymogi (nie może to być słowo kluczowe Javy, pierwsza znak musi być literą, _ lub $, a dalej możemy dopisywać jeszcze liczby)
  5. Lista parametrów - można nie podawać żadnego parametru bądź też dowolną ich ilość, mogą one być dowolnych typów, jedynie na ostatnim miejscu można umieścić tzw. varargs, czyli parametr określający zmienną ilość argumentów danego typu np. String…)
  6. Opcjonalny wyjątek - dzięki niemu można poinformować jaki wyjątek może rzucić metoda (chyba, że jest to nieobsłużony wyjątek typu checked, który trzeba oznaczyć w sygnaturze metody)
  7. Ciało metody - kawałek kodu, który determinuje jakie zadanie ma do wykonania metoda

Odpowiednie komentarze

Dopuszczalne jest pisanie zdań w kodzie, które nie będą podlegały kompilacji. Mają one za zadanie informować o tym co autor miał na myśli. Potrafią pomóc, jednak częściej szkodzą. Trzeba się ich strzec, ponieważ bardzo szybko się deaktualizują. Najgorsze są “martwe kody”, czyli kod, który ktoś kiedyś zakomentował. Najczęściej istnieje tłumaczenie ich trzymania “bo może się jeszcze kiedyś przyda”. Do tego służy kontrola wersji a nie komentarze ✋. Mamy 3 możliwości do komentowania kodu:

  • // … - komentarz jednolinijkowy
  • /* … */ - komentarz wielolinijkowy
  • */*** … */ - specjalny komentarz Javadoc do tworzenia dokumentacji (znanej chociażby z oficjalnej strony dokumentacji Javy)

Bloki inicjalizacyjne

Zacznijmy od przykładu (oczywiście to kiepskiej jakości kod, ale nie w tym rzecz 🤫):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class InitializationBlocks {

  private int number; 
  private static String kind; 

  {
    System.out.println("Instance block");
    number = 2; 
    name = "Instance"; 
    System.out.println("Instance block number: " + number);
  }

  public InitializationBlocks() { 
    number = 5; 
  }

  static { 
    System.out.println("Static block"); 
    name = "Class"; 
    kind = "First static block"; 
  } 

  private static String name; 

  public static void main(String[] args) { 
    InitializationBlocks object = new InitializationBlocks(); 
    System.out.println(object.name); 
    System.out.println(object.number); 
    System.out.println(object.kind); 
  } 

  static { 
    kind = "Second static block"; 
  } 
}

Zaznaczono w przykładzie jeden blok inicjalizacyjny instancji oraz dwa statyczne. Wynikiem tej aplikacji jest:

Static block 
Instance block 
Instance block number: 2 
Instance 
5 
Second static block

Dzięki takiemu rezultatowi można wysnuć następujące wnioski:

  1. Na początku obsługiwane są zmienne statyczne oraz instancyjne (ich deklaracja i opcjonalna inicjalizacja). Gdyby kod był wykonywany linijka po linijce to w pierwszym bloku statycznym nie mielibyśmy dostępu do zmiennej statycznej name.
  2. Następnie wykonywany jest blok inicjalizacyjny statyczny co można zaobserwować po pierwszej linijce wyniku “Static block”. Są one wykonywane po kolei przez co zmienna statyczna kind przyjęła na koniec wartość “Second static block”. Uwaga: nie ma możliwości w blokach inicjalizacyjnych przypisywania wartości do zmiennych instancji.
  3. Kolejnym etapem jest wykonywanie bloków inicjalizacyjnych instancji. Ich wywoływanie także zależy od kolejności w klasie. W nich mamy prawo przypisywać wartości do zmiennych statycznych.
  4. Na samym końcu wywoływany jest konstruktor, który ma prawo obsługiwać wszystkie rodzaje zmiennych.

Należy zwrócić uwagę na jeszcze jeden aspekt:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class StaticBlock {
  
  private static int counter;
  
  static {
    System.out.println("I'm static block");
    counter = 0;
  }
  
  public StaticBlock() {
    counter++;
    System.out.println("Creating " + counter + " object");
  }
  
  public static void main(String[] args) {
    new StaticBlock();
    new StaticBlock();
    new StaticBlock();
    new StaticBlock();
  }
}

Wyniki aplikacji zamieszczony został poniżej:

I'm static block 
Creating 1 object 
Creating 2 object 
Creating 3 object 
Creating 4 object

Blok statyczny wykonuje się tylko raz. Jest to moment, kiedy klasa zostaje załadowana. Następnie już tylko wywoływany jest kilka razy konstruktor, który informuje o ilości stworzonych obiektów. Warto eksperymentować i poznawać mechanizmy narzędzia, z którym ma się styczność na co dzień 👌.

Kolejność instrukcji w klasie

Pierwszym elementem w pliku klasy (np. ClassName.java) powinien być package. Jest to opcjonalna linijka (nie jest konieczna, gdy jesteśmy w folderze, gdzie znajduje się plik java). Ma za zadanie informować o tym, gdzie dokładnie znajduje się dana klasa. Przykładem może być package x.y.z. Ważne jest umieszczając klasę w danym pakiecie, aby pamiętać o przeniesieniu jej pliku do odpowiednich folderów o tych samych nazwach co w deklaracji (klasa z deklaracją package x.y.z musi znaleźć się w ścieżce /x/y/z/).

Następnie umieszczane są linijki odpowiedzialne za import. Są to instrukcje dla kompilatora z jakich pakietów powinien on skorzystać, aby uzyskać dostęp do niezbędnych klas czy metod albo zmiennych. Kolejność ich pisania jest dowolna. Używa się do tego słowa kluczowego import. Dzięki niemu możemy zaimportować:

  • wybraną klasę - import java.util.List
  • wszystkie klasy z pakietu - import java.util.*
  • daną metodę statyczną - import static org.testng.Assert.assertNotNull
  • daną zmienną statyczną - import static pl.jakis.pakiet.Clazz.ZMIENNA
  • wszystkie zmienne i metody statyczne z klasy - import static pl.jakis.pakiet.Clazz.*

Kolejnym elementem są klasy i interfejsy. Ich kolejność w pliku jest dowolna. Jednak należy pamiętać, że maksymalnie jedna z nich może mieć modyfikator dostępu public (wtedy plik musi nosić taką nazwę jak ta klasa bądź interfejs). Jeśli mamy kilka klas bądź interfejsów w pliku to plik musi nazywać się jak jedna z nich.

Dalej są zmienne, metody, konstruktory itd. znajdujące się w klasie, które mogą być w niej w dowolnej kolejności (co zaprezentowano w akapicie Bloki inicjalizacyjne).

Podsumowanie

Powyższe zagadnienia pokazują kilka zawiłości oraz zasad jakie wymaga od nas Java. Istnieją pewne standardy pisania kodu źródłowego (ogólne i takie, które ustaliliście sobie w zespole), których należy przestrzegać. Jednak można prowadzić eksperymenty z językiem w zaciszu domowym i sprawdzić jak zachowuje się on w pewnych, niestandardowych sytuacjach. Zachęcam do tego. Masz może także jakieś ciekawe aspekty Javy, którymi chciałbyś się podzielić? Napisz o tym w komentarzu lub podziel się tym ze mną poprzez maila.