Programowanie średniozaawansowane #5: słowo final

Wiesz już czym jest dziedziczenie oraz polimorfizm. Znasz także klasę abstrakcyjną, która musi rozszerzać inną, aby zostać użyta. Czasami jednak będziesz chcieć uniknąć rozszerzania jednej klasy przez drugą. Powodów może być wiele, a jednym z nich jest uproszczenie architektury kodu poprzez wykorzystanie słowa final. Słowo final może być użyte przed nazwą klasy, metody, pola, a także przed zmienną lokalną lub parametrem metody.

W przypadku użycia go w jakiejkolwiek zmiennej (klasowej, lokalnej, czy też jako argument metody) słowo final ma za zadanie od razu wymuszenie przypisania wartości do niej. Tutaj musisz mieć na uwadze, że trochę inaczej to działa w przypadku prymitywów (gdzie przypisujesz faktyczną wartość), jak w przypadku typów referencyjnych (gdzie stała będzie referencja do obiektu). Jeśli póki co nie rozumiesz, o co mi chodzi, to nie martw się, w kolejnych lekcjach wrócę do tego tematu i pokażę Ci jaka jest różnica. Na razie po prostu zapamiętaj, że final przed zmienną oznacza, że musisz coś do niej przypisać.

Teraz skupię się na dwóch pozostałych użyciach: w metodach i klasach. Jeśli użyjesz final przed nazwą metody, to zabraniasz nadpisania jej w obiekcie potomnych (czyli nie będzie możliwe przesłonienie metody). Czasami jednak masz mnóstwo metod w klasie i przypisywanie każdej z nich słowa final jest czasochłonne oraz mało elastyczne (gdy np. zmienisz zdanie). Lepiej wtedy po prostu użyć słowa final przed nazwą klasy. W efekcie nie możliwe będzie użycie tej klasy jako rodzica innej. Finalna klasa to klasa skończona i nie będzie można po niej dziedziczyć.

Spójrz na poniższy przykład.

public class Ball {
  private String name;
  
  public Ball(String name) {
    this.name = name;
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }
}

Nie ma tu nic zagadkowego. Zwykła klasa POJO z jednym polem oraz konstruktorem parametrowym.

public class Football extends Ball{
  private final String brand;

  public Football(String name) {
    super(name);
    this.brand = "unknown";
  }
  
  public Football(String name, String brand) {
    super(name);
    this.brand = brand;
  }
  
  public String getBrand() {
    return brand;
  }
  
  public final void showBrand() {
    System.out.println(brand);
  }
  
  public void showName() {
    System.out.println(super.getName());
  }
}

Klasa Football rozszerzona została o Ball. Oznacza to, że w jej konstruktorze obowiązkowo należy wywołać konstruktor klasy bazowej (dzieje się tak ponieważ dziedziczenie tak naprawdę polega na ‚składaniu’ obiektów, więc wpierw musisz stworzyć obiekt rodzica, aby później wykorzystać go w klasie dziecka). Jak widzisz, w nowej klasie napisałem dwa konstruktory (możesz ich tworzyć, w zależności od potrzeby, ile zechcesz, ważne żeby sygnatura się nie powtarzała). Pamiętaj, że ponieważ pole brand jest finalne, to musi zostać mu przypisana wartość (albo od razu przy jego deklaracji, albo w każdym konstruktorze). Ponad to klasa posiada jeden getter, jedną metodę finalną oraz jedną zwykłą.

public final class MyFootball extends Football {

  public MyFootball(String name, String brand) {
    super(name, brand);
  }
  
  @Override
  public void showName() {
    System.out.println(super.getName());
  }
  
  public void play(final String action) {
    final String ballString = "ball";
    System.out.println(action + " " + ballString);
  }

}

W powyższej klasie musiałem stworzyć konstruktor obowiązkowo wywołując konstruktor bazowy. Ponieważ chciałem też nadpisać metodę showName użyłem znanej Ci adnotacji override. Natomiast showBrand nie da się przesłonić, ze względu na to, że była ona oznaczona jako finalna w klasie rodzica. Klasa MyFootball dodatkowo jest zabezpieczona słowem final co oznacza, że choć możesz ją rozszerzyć (spójrz na sygnaturę klasy i fragment: extends Football) to jednak nie będzie ona mogła rozszerzyć żadnej kolejnej klasy. Dla treningu stwórz jakąkolwiek nową klasę i spróbuj ją rozszerzyć o MyFootball (np. public class Foo extends MyFootball). Nie będzie to możliwe ze względu na użycie w niej słowa final. Ostatnim ciekawym fragmentem w klasie jest metoda play, która zawiera finalny parametr o lokalną zmienną. Zapytasz pewnie, po co to komu? Powiem Ci szczerze, że na tym etapie nie ma to dla Ciebie wielkiego znaczenia. Czasami warto zmienną lokalną oznaczyć jako finalną, jeśli chcesz zaznaczyć dla kolejnego programisty, że nie powinna być ruszana. W praktyce nie ma to żadnego znaczenia, osobiście nigdy nie używam finalnych zmiennych lokalnych, chyba że zastany przeze mnie kod jest w taki sposób napisany, a ja nie chcę wyłamywać się z wcześniej przyjętej konwencji. Podobnie sprawa ma się z parametrami oznaczonymi jako final. Dla ćwiczenia spróbuj w tej metodzie nadpisać parametr i zmienną lokalną w jej kolejnych linijkach.

public class BallMain {

  public static void main(String[] args) {
    Ball ball = new Ball("Some ball");
    System.out.println(ball.getName());
    Football football = new Football("Anna's ball", "Adidas");
    football.showBrand();
    football.showName();
    MyFootball olaBall = new MyFootball("Ola's ball", "Nike");
    olaBall.showBrand();
    olaBall.showName();
    olaBall.play("pass");
    olaBall.play("shoot");
  }
}

Teraz poodpalaj sobie wszystkie klasy i metody za pomocą znanej już metody main. Postaraj się potestować co możesz wywołać, a co jest ‚zablokowane’ przez użycie słowa final.

Dodaj komentarz