
Słowo kluczowe final
Na początku mojej drogi z programowaniem w Javie, wydawało mi się, że słowo final jest proste i oczywiste. Po jakimś czasie okazało się, że ma więcej zastosowań.
W tym artykule zabiorę Cię do swojego labolatorium i pokażę Ci co można zyskać korzystając ze słowa kluczowego final w Javie.
Pole
Pierwszym zagadnieniem jakim się porusza podczas przerabiania final jest możliwość oznaczenie pola jako stała.
Nie jest to nic trudnego, wystarczy przed typem zmiennej dodamy final.
final String name = "Pablo";
Często wykorzystuje się final podczas deklarowania pól statycznych np.:
static final String CONFIG_NAME = "config.ini";
Niestety słowo final nie zapewnia nam niezmienności obiektu, czyli możemy bez problemu zmieniać jego zawartość:
final User user = new User("Pablo", "pablo1", "pablo@example.com"); user.setLogin("NewPablo");
Ale za to pilnuje tego, aby nie została zmieniona referencja obiektu – krótko mówiąc, nie możemy zmienić “przypisania” do zmiennej, więc np. taka operacja nie jest możliwa do wykonania:
final User user = new User("Pablo", "pablo1", "pablo@example.com"); user.setLogin("NewPablo"); user = new User("newPablo", "pablo2", "pablo@example.com");
W przypadku, gdy tworzymy klasę, a pole oznaczymy jako final, musimy zapewnić wtedy inicjalizację pola od razu podczas deklaracji lub bezpośrednio w konstruktorze.
public final class User { private final String login; private final String password; private final String email; public User(String login, String password, String email) { this.login = login; this.password = password; this.email = email; } public String getLogin() { return login; } public String getPassword() { return password; } public String getEmail() { return email; } }
Ponownie otrzymujemy niezmienność pól – krótko mówiąc w takiej sytuacji settery nie zadziałają. Nawet generator setterów ALT + INSERT w IntelliJ podpowiada nam, że nie może znaleźć pól, dla których mógłby wygenerować settera. 😉
Argumenty final
Słowo kluczowe final możemy również wykorzystać podczas deklarowania argumentów metody.
public static void transformUser(final User user) { user = new User("Pablo", "pablo1", "pablo@example.com"); }
Ponownie taki kod nie zostanie skompilowany – zadeklarowanie argumentu jako final zapewnia, że referencja obiektu w ciele metody nie zostanie zmieniona.
Metoda jako finalna
Słówka final możemy również użyć podczas deklaracji metody. Użyjmy jej podczas deklaracji metody addUser.
import java.util.LinkedList; import java.util.List; import java.util.Optional; public class UserService { protected final List<User> users = new LinkedList<>(); public final void addUser(final User user) { users.add(user); } public Optional<User> getUser(User user) { return users.stream().filter(u -> u.getEmail().equals(user.getEmail())).findFirst(); } }
Stwórzmy klasę SuperUserService, która będzie rozszerać klasę UserService:
public class SuperUserService extends UserService { }
Przysłońmy obie metody w nowej klasie – używając adnotacji @Override
import org.springframework.util.Assert; import java.util.Optional; public class SuperUserService extends UserService { @Override public Optional<User> getUser(User user) { Assert.notNull(user, "User cannot be null!"); Assert.hasText(user.getEmail(), "Email cannot be empty!"); return getUser(user); } @Override public void addUser(User user) { Assert.notNull(user, "User cannot be null!"); super.addUser(user); } }
Obie metody zostały lekko rozszerzone poprzez sprawdzanie podanych argumentów – zostało do tego wykorzystane Assert ze Spring Core.
Niestety kod się nie kompiluje – dokładnie chodzi o tą linie:
public void addUser(User user)
Dlaczego akurat tutaj? Dzieje się tak, z powodu oznaczenia metody jako finalna w klasie rodzica:
public final void addUser(final User user)
Jak widać oznaczenie metody jako final zapewnia nam to, że nie może ona zostać przysłonięta w klasie podrzędnej. 😉
Klasa jako final
Ostatnią możliwością jest oznaczenie klasy jako final – sprawdźmy teraz, co dzięki temu zyskujemy.
public final class User { private final String login; private final String password; private final String email; public User(String login, String password, String email) { this.login = login; this.password = password; this.email = email; } public String getLogin() { return login; } public String getPassword() { return password; } public String getEmail() { return email; } }
Klasa User została oznaczona jako final, stwórzmy teraz klasę SuperUser, która będzie rozszerzać klasę User.
public class SuperUser extends User { private final String superPower; public SuperUser(String login, String password, String email, String superPower) { super(login, password, email); this.superPower = superPower; } public String getSuperPower() { return superPower; } }
Jak widać ponownie – kod się nie kompiluje.
Dzieje się tak, ponieważ klasa rodzica (User) została oznaczona jako final – jest finalna. Podsumowując oznaczenie klasy jako final uniemożliwia jej dziedziczenie.
Czy taki mechanizm jest przydatny?
W pewnych przypadkach na pewno tak, m.in korzysta z niego klasa String – używa tego, ponieważ jej twórcy z założenia nie chcą, aby klasa była rozszerzana przez innych programistów, aby nie narazać niezmienności obiektu. W końcu String jest używany np. do przechowywania haseł i przeróżnych kluczy. 😉
Podsumowanie
Powyżej przedstawiłem Ci cztery sytuacje, w których możesz użyć słowa kluczowego final. Co prawda prawie żaden kod się nie kompiluje, lecz mam nadzieje, że te czerwone podkreślenia w IDE zapadną Ci w pamięć i zapamiętasz co daje nam użycie słowa kluczowe final. 😉
Kod używany w artykule znajdziesz w tym repozytorium.
Bardzo dobry artykuł….
Błędy w języku polskim są na porządku dziennym. Szkoła była pod górę?
Hej Rafał
Dzięki za zwrócenie uwagi na błędy. Postarałem się poprawić czytelność artykułu. Faktycznie było sporo błędów, które od razu rzucały się w oczy.
Możliwe, że artykuł był napisany na szybko i bez sprawdzenia przez osobę trzecią. 😉
Jeszcze raz dzięki!