– Opowiem Ci o „modyfikatorach dostępu”. Już raz o nich mówiłem, ale powtórki to podstawa procesu uczenia się.

Możesz kontrolować dostęp (widoczność) innych klas do metod i zmiennych Twojej klasy. Modyfikator dostępu odpowiada na pytanie „kto ma dostęp do tej metody/zmiennej?”. Dla każdej metody lub zmiennej możesz określić tylko jeden modyfikator.

1) modyfikator «public».

Do zmiennej, metody lub klasy oznaczonej modyfikatorem public można uzyskać dostęp z dowolnego miejsca w programie. To najwyższy poziom dostępu – nie ma tutaj żadnych ograniczeń.

2) modyfikator «private».

Do zmiennej, metody lub klasy oznaczonej modyfikatorem private można uzyskać dostęp tylko w klasie, w której zostało to zadeklarowane. Zaznaczona metoda lub zmienna jest ukryta przed wszystkimi innymi klasami. Jest to najwyższy stopień prywatności: dostępny tylko dla Twojej klasy. Takie metody nie są dziedziczone i nie mogą być nadpisywane. Dodatkowo, nie ma dostępu do nich w klasie podrzędnej.

3) «modyfikator domyślny».

Jeżeli zmienna lub metoda nie jest oznaczona żadnym modyfikatorem, uznaje się ją za oznaczoną „domyślnym” modyfikatorem dostępu. Zmienne i metody z tym modyfikatorem są widoczne dla wszystkich klas w paczce, w której są one zadeklarowane i tylko dla tych klas. Ten modyfikator nazywany jest również dostępem „package” lub „package private”, co wskazuje na fakt, że dostęp do zmiennych i metod jest otwarty dla całego pakietu, który zawiera daną klasę.

4) modyfikator «protected».

Ten poziom dostępu jest nieco szerszy niż package. Dostęp do zmiennej, metody lub klasy oznaczonej modyfikatorem protected można uzyskać z poziomu pakietu oraz wszystkich dziedziczonych klas.

Poniższa tabela wszystko to wyjaśnia:

Typ widoczności Słowo kluczowe Dostęp
Twoja klasa Twój pakiet Obiekt podrzędny Wszystkie klasy
Prywatny private Tak Nie Nie Nie
Pakiet (brak modyfikatora) Tak Tak Nie Nie
Chroniony protected Tak Tak Tak Nie
Publiczny public Tak Tak Tak Tak

Istnieje sposób na łatwe zapamiętanie tej tabeli. Wyobraź sobie, że piszesz testament. Dzielisz cały swój dobytek na cztery kategorie. Kto będzie używał Twoich rzeczy?

Kto ma dostęp Modyfikator Przykład
Tylko ja private Osobisty dziennik
Rodzina (brak modyfikatora) Zdjęcia rodzinne
Rodzina i spadkobiercy protected Rodzinny majątek
Każdy public Pamiętniki

– Można to sobie wyobrazić w taki sposób, że klasy w tej samej paczce są częścią jednej rodziny.

– Chcę Ci również powiedzieć o kilku interesujących niuansach dotyczących nadpisywania metod.

1) Domyślna implementacja metody abstrakcyjnej.

Powiedzmy, że masz następujący kod:

Kod
class Cat
{
 public String getName()
 {
  return "Oskar";
 }
}

I postanowiłeś utworzyć klasę Tiger, która dziedziczy tę klasę i do nowej klasy dodać interfejs

Kod
class Cat
{
 public String getName()
 {
   return "Oskar";
 }
}
interface HasName
{
 String getName();
 int getWeight();
}
class Tiger extends Cat implements HasName
{
 public int getWeight()
 {
  return 115;
 }

}

Jeśli po prostu zaimplementujesz wszystkie brakujące metody, które IntelliJ IDEA każe Ci zaimplementować, może się okazać, że później spędzisz dużo czasu szukając błędu.

Okazuje się, że klasa Tiger posiada metodę getName odziedziczoną po Cat, która zostanie przyjęta jako implementacja metody getName dla interfejsu HasName.

– Nie widzę w tym nic strasznego.

– To zwyczajnie miejsce, w którym łatwo jest popełnić błąd.

Ale może być jeszcze gorzej:

Kod
interface HasWeight
{
 int getValue();
}
interface HasSize
{
 int getValue();
}
class Tiger extends Cat implements HasWeight, HasSize
{
 public int getValue()
 {
  return 115;
 }
}

Okazuje się, że nie zawsze można dziedziczyć po kilku interfejsach. Mówiąc ściślej, można je dziedziczyć, ale nie można ich prawidłowo implementować. Spójrz na ten przykład. Oba interfejsy wymagają zaimplementowania metody getValue(), ale nie jest jasne, co powinna ona zwrócić: wagę czy rozmiar? To dość nieprzyjemne, że trzeba sobie z tym jakoś poradzić.

– To dziwne, wiem. Chcesz implementować metodę, ale nie możesz tego zrobić. Odziedziczyłeś już z klasy bazowej metodę o tej samej nazwie. Ale nie działa.

Mam jednak dobrą wiadomość.

2) Rozszerzenie widoczności. Kiedy dziedziczysz typ, możesz rozszerzyć widoczność metody. Wygląda to tak:

Kod Java Opis
class Cat
{
 protected String getName()
 {
  return "Oskar";
 }
}
class Tiger extends Cat
{
 public String getName()
 {
  return "Oskar Tiggerman";
 }
}
Rozszerzyliśmy widoczność metody z protected na public.
Kod Dlaczego jest to „dozwolone”
public static void main(String[] args)
{
 Cat cat = new Cat();
 cat.getName();
}
Wszystko się zgadza. Tutaj nawet nie wiemy, że w klasie podrzędnej została rozszerzona widoczność.
public static void main(String[] args)
{
 Tiger tiger = new Tiger();
 tiger.getName();
}
W tym miejscu wywołujemy metodę, której widoczność została rozszerzona.

Gdyby to nie było możliwe, moglibyśmy zawsze zadeklarować metodę w Tiger:
public String getPublicName()
{
super.getName(); //wywołaj metodę oznaczoną modyfikatorem protected
}

Innymi słowy, nie ma mowy o żadnym naruszeniu bezpieczeństwa.

public static void main(String[] args)
{
 Cat catTiger = new Tiger();
 catTiger.getName();
}
Jeżeli zostały spełnione wszystkie warunki niezbędne do wywołania metody w klasie bazowej (Cat), to z pewnością zostały też spełnione warunki umożliwiające wywołanie metody na typie podrzędnym (Tiger). Dzieje się tak, dlatego że ograniczenia dotyczące wywoływania metody były słabe, a nie silne.

– Nie jestem pewien, czy do końca zrozumiałem, ale będę pamiętał, że to możliwe.

3) Zawężanie zwracanego typu.

W nadpisanej metodzie możemy zmienić zwracany typ na zawężony typ referencyjny.

Kod Java Opis
class Cat
{
 public Cat parent;
 public Cat getMyParent()
 {
  return this.parent;
 }
 public void setMyParent(Cat cat)
 {
  this.parent = cat;
 }
}
class Tiger extends Cat
{
 public Tiger getMyParent()
 {
  return (Tiger) this.parent;
 }
}
Nadpisaliśmy metodę getMyParent i teraz zwraca ona obiekt Tiger.
Kod Dlaczego jest to „dozwolone”
public static void main(String[] args)
{
 Cat parent = new Cat();

 Cat me = new Cat();
 me.setMyParent(parent);
 Cat myParent = me.getMyParent();
}
Wszystko się zgadza. Tutaj nawet nie wiemy, że zwracany typ metody getMyParent został rozszerzony w klasie podrzędnej.

Jak działał i działa „stary kod”.

public static void main(String[] args)
{
 Tiger parent = new Tiger();

 Tiger me = new Tiger();
 me.setMyParent(parent);
 Tiger myParent = me.getMyParent();
}
W tym miejscu wywołujemy metodę, której zwracany typ został rozszerzony.

Gdyby to nie było możliwe, moglibyśmy zawsze zadeklarować metodę w Tiger:
public Tiger getMyTigerParent()
{
return (Tiger) this.parent;
}

Innymi słowy, nie ma żadnych naruszeń bezpieczeństwa i/lub naruszeń rzutowania typów.

public static void main(String[] args)
{
 Tiger parent = new Tiger();

 Cat me = new Tiger();
 me.setMyParent(parent);
 Cat myParent = me.getMyParent();
}
I wszystko działa prawidłowo, choć zawęziliśmy typ zmiennych do klasy bazowej (Cat).

Ze względu na nadpisanie wywoływana jest właściwa metoda setMyParent.

Nie musimy się niczym martwić przy wywoływaniu metody getMyParent, ponieważ zwracana wartość, choć z klasy Tiger, może nadal być bez problemu przypisana do zmiennej myParent z klasy bazowej (Cat).

Obiekty Tiger mogą być bezpiecznie przechowywane zarówno w zmiennych Tiger, jak i w zmiennych Cat.

– Jasne. Kumam. Przy stosowaniu nadpisywania metod, musisz być świadomy, jak to wszystko działa, gdy przekazujemy nasze obiekty do kodu, który może obsługiwać tylko klasę bazową i nie wie nic o naszej klasie.

– Dokładnie! A zatem nasuwa się pytanie, dlaczego nie możemy zawęzić typu zwracanej wartości podczas nadpisywania metody?

– To oczywiste, że w tym przypadku kod w klasie bazowej przestałby działać:

Kod Java Wyjaśnienie problemu
class Cat
{
 public Cat parent;
 public Cat getMyParent()
 {
  return this.parent;
 }
 public void setMyParent(Cat cat)
 {
  this.parent = cat;
 }
}
class Tiger extends Cat
{
 public Object getMyParent()
 {
  if (this.parent != null)
   return this.parent;
  else
   return "Jestem sierotą";
 }
}
Przeciążyliśmy metodę getMyParent i zawęziliśmy typ zwracanej przez nią wartości.

Wszystko jest tu w porządku.

public static void main(String[] args)
{
 Tiger parent = new Tiger();

 Cat me = new Tiger();
 Cat myParent = me.getMyParent();
}
Następnie ten kod przestanie działać.

Metoda getMyParent może zwrócić każdą instancję Object, ponieważ tak naprawdę jest ona wywoływana na obiekcie Tiger.

I nie mamy możliwości przeprowadzenia sprawdzenia przed przypisaniem. Jest więc całkowicie możliwe, że zmienna myParent typu Cat będzie przechowywać referencję typu String.

– Wspaniały przykład, Amigo!

W Javie, zanim dana metoda zostanie wywołana, nie ma możliwości sprawdzenia, czy obiekt posiada taką metodę. Kontrola następuje dopiero w momencie wykonywania programu. A (czysto hipotetyczne) wywołanie brakującej metody najprawdopodobniej spowodowałoby, że program próbowałby wykonać nieistniejący kod bajtowy. Doprowadziłoby to ostatecznie do błędu krytycznego, a system operacyjny wymusiłby zamknięcie programu.

– Wow! Teraz już to będę wiedział.