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ć niż ja wcześniej myślałem.
W tym artykule zabiorę Cię do swojego labolatorium i pokażę Ci co można zyskać korzystając ze słówka kluczowego final w Javie.
Pole
Pierwszym zagadnieniem jakim się porusza podczas przerabiania final jest możliwość oznaczenie pola jako stałą.
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 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 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łowa kluczowe final możemy również korzystać podczas podawania argumentów metody. Oczywiście nie bezpośrednio podczas wywołania funkcji, tylko podczas jej tworzenia.
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 pilnuje tego, aby referencja obiektu w środku nie została zmieniona.
Metoda jako finalna
Słówka final możemy również użyc 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 – podświetla nam dokładnie w tej linii:
public void addUser(User user)
Dlaczego 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 4 sytuacje kiedy możesz używać słowa kluczowe 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.