Wpis z dedykacją dla D, który postanowił nauczyć się nowego języka programowania i, chcąc utworzyć stałą (constant – słowo najważniejsze niedostępne w Javie), zapytał mnie o relację pomiędzy terminami: final, static i obiekt niemodyfikowalny (immutable).
final
Słowo final zmienia znaczenie w zależności od kontekstu, ale zawsze oznacza, iż coś jest “ostateczne” i po utworzeniu nie da się już tego zmienić.
Klasa final
Jeśli klasę oznaczymy słowem final, nie będzie można jej rozszerzać, czyli tworzyć jej podklas. Zatem będzie to “ostateczna” wersja danej klasy.
public final class MojaOstatecznaKlasa {} // Błąd kompilacji: public class Proba extends MojaOstatecznaKlasa {}Metoda final
Z metodami jest podobnie jak z klasami. jeżeli słowem final oznaczymy metodę, nie będzie można jej przesłaniać w klasach pochodnych – czyli mamy do czynienia z “ostateczną” wersją danej metody.
public class Klasa { public void m1() {} public final void m2() {} } public class Podklasa extends Klasa { public void m1() {} // OK public void m2() {} // Błąd kompilacji }Zmienna final
Jeśli jako final opiszemy zwykłą zmienną w środku kodu, wartość (a dokładniej: wartość w przypadku typów prostych lub referencję w przypadku obiektów) będzie można jej przypisać maksymalnie raz. Na przykład:
final int x; // OK x = 3; // OK System.out.println(x); x = 4; // błąd kompilacjiPole klasy final
Jeśli słowem final oznaczymy pole klasy, będzie to oznaczało, iż wartość (lub referencję, jak powyżej) można mu nadać tylko raz – będzie ona ostateczna. Wartość tę można przypisać na dwa sposoby – albo “w miejscu”, przy definiowaniu klasy, albo w konstruktorze. Nie da się tego zrobić później. jeżeli pole final nie otrzyma wartości w żadnym z tych dwóch miejsc, kompilator zaprostesuje.
Za przykład niech posłuży fragment definicji klasy String z Javy 8:
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { // inicjalizacja w konstruktorze private final char value[]; // inicjalizacja w miejscu private static final long serialVersionUID = -6849794470754667710L; // konstruktor inicjalizujący pole final public String(String original) { this.value = original.value; this.hash = original.hash; }Parametr final
Jeśli jako final oznaczymy parametr metody, zablokujemy możliwość zmiany jego wartości. I tak nie powinniśmy tego robić – to jedna z dobrych praktyk. Użycie słowa final oficjalnie nas do niej zobowiązuje.
public void dontMessWithParams(final int p1, int p2){ p2 = p1; // OK! p1 = p2; // Błąd kompilacji. }static
Słowo najważniejsze static nie musi być łączone ze stałymi. Chcę jednak rozebrać je na czynniki pierwsze, ponieważ często spotyka się je razem z final w następujących konstrukcjach (ta akurat znów klasy String):
private static final long serialVersionUID = -6849794470754667710L;Słowo “statyczny”, jako przeciwieństwo “dynamiczny” można w uproszczeniu rozumieć w ten sposób, iż opisany nim byt do życia nie potrzebuje obiektów (tworzonych dynamicznie).
Pole klasy static
Jeśli pole klasy zostanie oznaczone jako static (jak w przykładzie bezpośrednio powyżej), oznacza to, iż jest współdzielone przez wszystkie obiekty danej klasy. Można uzyskać do niego dostęp odwołując się do nazwy obiektu lub do nazwy klasy – efekt będzie ten sam. Pamięć jest przydzielana tylko raz, podczas ładowania klasy.
W ten sposób można definiować ważne stałe albo różnego rodzaju liczniki (np. liczba egzemplarzy danej klasy – wówczas w konstruktorze zwiększamy wartość tego pola o 1).
Metoda static
Metoda static jest współdzielona przez wszystkie obiekty, można ją także wywołać odnosząc się bezpośrednio do nazwy klasy, bez potrzeby tworzenia obiektu.
Metoda ta ma dostęp jedynie do statycznych atrybutów (pól i metod) danej klasy. Co za tym idzie, nie może też używać słów this ani super.
W ten sposób często definiuje się różne metody pomocnicze – na przykład większość metod w klasy Math.
Najbardziej znana metoda statyczna to oczywiście main.
public static void main(String[] args){ OtherClass myObject = new OtherClass("Hello World!"); System.out.print(myObject); }Blok kodu static
Wewnątrz definicji klasy można umieścić statyczny blok kodu, w którym, na przykład, zainicjalizujemy jakieś statyczne pola. Zostanie on wywołany w trakcie ładowania klasy.
Mimo iż bloki statyczne rzadko pojawiają się w kodzie, kolejność ich wykonywania to konik wielu profesorów informatyki na egzaminach z programowania obiektowego.
class Klasa{ static{ System.out.println("blok statyczny"); } }Zagnieżdżona klasa static
Jeśli klasę zagnieżdżoną opiszemy słowem static, będzie ona mogła odwoływać się jedynie do statycznych atrybutów swojej klasy zewnętrznej.
Obiekty niemodyfikowalne
Obiekt niemodyfikowalny (immutable) to taki obiekt, w którym po użyciu konstruktora nie można już dokonywać żadnych zmian.
Przekazując taki obiekt, mamy pewność, iż nie zostanie on zmieniony przez żaden kod – ani nasz, ani znajdujący się pod kontrolą kogoś innego. Obiekty niemodyfikowalne są wybawieniem podczas pracy z wątkami, ponieważ nie trzeba synchronizować ich stanu.
Niemutowalne są na przykład obiekty klasy Integer.
Oto fragment definicji tej klasy:
private final int value; public Integer(int value) { this.value = value; } public Integer(String s) throws NumberFormatException { this.value = parseInt(s, 10); }Wartość value (czyli liczbę typu prostego int opakowywaną przez klasę Integer) można ustawić jedynie przy użyciu któregoś z konstruktorów. Nie da się jej zmienić później. Klasa Integer ma szereg metod pozwalających uzyskać dostęp do tej wartości, ale sama wartość wewnątrz obiektu tej klasy nigdy nie zostanie zmieniona. Nie jest do tego potrzebne słowo final (które zostało tu użyte) – wystarczyłaby sama enkapsulacja. final daje nam jednak niezbitą pewność, iż po inicjalizacji nikt już tej wartości nie zmieni.
Co ma zrobić osoba, która potrzebuje innej liczby Integer? Będzie musiała po prostu utworzyć nowy obiekt.
Integer result = new Integer(1); // obiekt result = new Integer(result.intValue() + 1); // nowy obiekt result = new Integer(result.intValue()) + 1; // czary-mary, też nowy obiekt System.out.println(result.doubleValue()); // 3.0Nie istnieje słowo najważniejsze immutable. Obiekty niemodyfikowalne tworzy programista, korzystając z mechanizmu enkapsulacji oraz słowafinal.
Wszystko razem, czyli static final String
Rozważmy definicję poniższych pól:
public static final int ERROR_CODE = 404; public static final String RESPONSE = "Ouch"; public static final Client CLIENT = new Client("John Snow");Wszystkie trzy pola są opisane jako static final, czyli są to stałe współdzielone przez wszystkie obiekty danej klasy.
W pierwszym przypadku mamy stałą typu prostego. Sprawa jest oczywista – nie wolno nam zmienić jej wartości. Nie możemy przypisać pod ERROR_CODE żadnej innej wartości.
W drugim przypadku mamy obiekt. Jest to, dodatkowo, obiekt niemodyfikowalny (immutable). Uczący się Javy kolega zapytał ostatnio, po co dodawać final, skoro obiekt jest niemodyfikowalny. Po co? – żeby nie można było zmienić referencji. Gdyby nie słowo final, możliwa byłaby następująca operacja:
RESPONSE = "Ooops!"; // Możliwe bez użycia finalObiekt String z wartością “Ouch” jest co prawda niemodyfikowalny (nie można zmienić przechowywanej w nim wartości), ale nic nie stałoby na przeszkodzie, żeby podmienić obiekty przypisane do zmiennej RESPONSE. Użycie final nas przed tym chroni.
W ostatnim przypadku mamy zwykły (modyfikowalny) obiekt ze zmiennym stanem, przypisany do pola oznaczonego jako final. Co to oznacza? Że co prawda, dzięki final, nie możemy zmienić referencji (to będzie przez cały czas tem sam obiekt) ale ciągle możemy nieźle namieszać w jego wnętrzu przy użyciu API:
// Nie skompiluje się, // nie wolno nam przypisać tu nowego obiektu. CLIENT = new Client("Aegon Targaryen"); // Nic nie stoi na przeszkodzie. // Słowo final chroni referencję. // Obiekt musi chronić się sam. CLIENT.setName("Aegon");Jasne?
Odpowiedne korzystanie z javowych “stałych”, wartości statycznych i obiektów niemodyfikowalnych pozwala na oszczędzenie miejsca w pamięci i stworzenie czytelnego, łatwego w utrzymaniu kodu.