Mariusz Prowaźnik

o programowaniu w Javie, Scali i Clojure.


Przesłanianie w Javie i jego kruczki

Na początek zagadka. Są klasy:
public class Bazowa {
 public int i = 5;
 public int getI1() {
  return i;
 }
 public int getI2() {
  return i;
 }
}
public class Pochodna extends Bazowa {
 public int i = 0;
 public int getI1() {
  return i;
 }
        public void sth(){};
}
Jaki będzie wynik uruchomienia kodu:
public class Test {
 public static void main(String[] args) {
  Bazowa b = new Pochodna();
  System.out.print(b.i);
  System.out.print(b.getI1());
  System.out.print(b.getI2());

  System.out.println("");
  Pochodna p = new Pochodna();
  System.out.print(p.i);
  System.out.print(p.getI1());
  System.out.print(p.getI2());
 }
}
Jeśli bez wahania odpowiedziałeś:
505
005
to brawo. Ten post nie będzie dla Ciebie niczym odkrywczym.

W powyższym kodzie zadeklarowano dwie zmienne lokalne (b,p). Jedna z nich jest referencją typu Bazowa, druga typu Pochodna. Dzięki polimorfizmowi referencja typu Bazowa może odnosić do obiektu typu pochodnego Pochodna. Mimo, że referencje są różnego typu, obiekty na który wskazują mają taki sam typ.
Korzystając z referencji b, możemy wykonywać widoczne metody klasy Bazowa. Dlatego nie można zrobić takiego wywołania: b.sth(), mimo, że właściwy obiekt taką metodę zawiera.

W klasie Pochodna została przesłonięta metoda getI1(), która jest zadeklarowaną w klasie Bazowa i wykonując b.getI1() w 6. linii, JVM "wie" jakiego typu jest obiekt i wykona odpowiednią wersję metody, czyli tę z klasy Pochodna. Dlatego rezultatem jest wartość 0.

Nie dotyczy to jednak zmiennych. Dlatego b.i zawiera wartość 5, czyli wartość zmiennej i w klasie Bazowa, która jest zupełnie inną zmienną niż zmienna i w klasie Pochodna, mimo, że posiadają taką samą nazwę.

Wywołanie p.getI2() zwróci 5, ponieważ klasa Pochodna dziedziczy z Bazowa metodę getI2(), zwracająca wartość zmiennej i klasy Bazowa.

Na koniec dodam kilka innych zasad dotyczących przesłaniania:
  1. metody, które nie są dziedziczone, nie mogą zostać przesłonięte
  2. przesłaniające metody mogą mieć mniej restrykcyjny poziom dostępu
  3. przesłaniające metody mogą rzucać taki sam, lub mniejszy zakres wyjątków (nie licząc wyjątków typu runtime)

ad.1. Przykład:

package jeden;
public class Bazowa {
 public int i = 5;
 int getI1() {
  return i;
 }
}
package dwa;
import jeden.Bazowa;
public class Pochodna extends Bazowa {
 public int i = 0;
 public int getI1() {
  return i;
 }
}
public class Test {
 public static void main(String[] args) {
  Bazowa b = new Pochodna();
  System.out.print(b.getI1());
 }
}
Tutaj, w przeciwieństwie do wcześniejszego przykładu, rezultatem będzie "5", ponieważ Pochodna nie dziedziczy metody getI1() (klasy są w różnych paczkach, a metoda ma domyślny poziom dostępu), dlatego metoda ta nie może być przesłonięta.

ad.2. Gdyby w poprzednim przykładzie zmienić poziom dostępu metody getI1() w klasie Bazowa na protected, wtedy nie możliwe byłoby przesłonięcie metody i rezultat wyniósłby "0""