– 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:
class Cat
{
public String getName()
{
return "Oskar";
}
}
I postanowiłeś utworzyć klasę Tiger, która dziedziczy tę klasę i do nowej klasy dodać interfejs
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:
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 |
---|---|
|
|
|
Rozszerzyliśmy widoczność metody z protected na public . |
Kod | Dlaczego jest to „dozwolone” |
---|---|
|
Wszystko się zgadza. Tutaj nawet nie wiemy, że w klasie podrzędnej została rozszerzona widoczność. |
|
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: Innymi słowy, nie ma mowy o żadnym naruszeniu bezpieczeństwa. |
|
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 |
---|---|
|
|
|
Nadpisaliśmy metodę getMyParent i teraz zwraca ona obiekt Tiger . |
Kod | Dlaczego jest to „dozwolone” |
---|---|
|
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”. |
|
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: Innymi słowy, nie ma żadnych naruszeń bezpieczeństwa i/lub naruszeń rzutowania typów. |
|
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 |
---|---|
|
|
|
Przeciążyliśmy metodę getMyParent i zawęziliśmy typ zwracanej przez nią wartości.
Wszystko jest tu w porządku. |
|
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ł.