Dobre powody, by zakazać dziedziczenia w Javie?

178

Jakie są dobre powody, aby zabronić dziedziczenia w Javie, na przykład używając klas końcowych lub klas używających pojedynczego, prywatnego konstruktora bez parametrów? Jakie są dobre powody, dla których metoda jest ostateczna?

cretzel
źródło
1
Pedantry: domyślnym konstruktorem jest ten, który został wygenerowany przez kompilator języka Java, jeśli nie napiszesz go bezpośrednio w kodzie. Masz na myśli konstruktor bez argumentów.
John Topley
Czy nie jest to „niejawny domyślny konstruktor”?
cretzel
2
„Nie musisz dostarczać żadnych konstruktorów dla swojej klasy, ale musisz zachować ostrożność podczas wykonywania tego. Kompilator automatycznie udostępnia bezargumentowy, domyślny konstruktor dla każdej klasy bez konstruktorów”. java.sun.com/docs/books/tutorial/java/javaOO/constructors.html - John Topley
John Topley

Odpowiedzi:

184

Najlepszym odniesieniem jest punkt 19 znakomitej książki Joshuy Blocha „Efektywna Java”, zatytułowanej „Projektuj i dokumentuj w celu dziedziczenia albo zakazuj”. (To punkt 17 w drugim wydaniu i punkt 15 w pierwszym wydaniu). Naprawdę powinieneś go przeczytać, ale podsumuję.

Interakcja klas odziedziczonych z rodzicami może być zaskakująca i nieprzewidywalna, jeśli przodek nie został zaprojektowany do dziedziczenia.

Dlatego klasy powinny być dwojakiego rodzaju:

  1. Klasy zaprojektowane do rozszerzania i posiadające wystarczającą dokumentację, aby opisać, jak należy to zrobić

  2. Zajęcia oceniane jako końcowe

Jeśli piszesz czysto wewnętrzny kod, może to być trochę przesada. Jednak dodatkowy wysiłek związany z dodaniem pięciu znaków do pliku klasy jest bardzo mały. Jeśli piszesz tylko do użytku wewnętrznego, przyszły programista może zawsze usunąć „końcowy” - możesz o tym myśleć jako o ostrzeżeniu mówiącym, że „ta klasa nie została zaprojektowana z myślą o dziedziczeniu”.

DJClayworth
źródło
6
Z mojego doświadczenia wynika, że ​​nie jest to przesada, jeśli ktoś poza mną będzie używał kodu. Nigdy nie rozumiałem, dlaczego Java ma domyślnie nadpisywane metody.
Eric Weilnau
9
Aby zrozumieć część Efektywnej Javy, musisz zrozumieć podejście Josha do projektowania. Mówi [coś w stylu], że zawsze powinieneś projektować interfejsy klas tak, jakby były publicznym API. Myślę, że większość jest zdania, że ​​takie podejście jest zwykle zbyt ciężkie i nieelastyczne. (YAGNI itp.)
Tom Hawtin - tackline
9
Dobra odpowiedź. (Nie mogę się powstrzymać od wskazania, że ​​to sześć znaków, ponieważ jest też spacja ... ;-)
joel.neely
36
Pamiętaj, że
ukończenie
10
Jeśli ostatnia klasa pisze do interfejsu, to mockowanie nie powinno stanowić problemu.
Fred Haslam
26

Możesz chcieć, aby metoda była ostateczna, aby nadpisywanie klas nie zmieniało zachowania liczonego w innych metodach. Metody wywoływane w konstruktorach są często uznawane za końcowe, więc podczas tworzenia obiektów nie ma żadnych przykrych niespodzianek.

Bill the Lizard
źródło
Nie całkiem zrozumiałem tę odpowiedź. Jeśli nie masz nic przeciwko, czy mógłbyś wyjaśnić, co masz na myśli mówiąc, że „klasy nadpisujące nie mogą zmienić zachowania, na które liczą się inne metody”? Pytam, po pierwsze, ponieważ nie zrozumiałem zdania, a po drugie, ponieważ Netbeans zaleca, aby jedna z moich funkcji, którą wywołuję z konstruktora, była final.
Nav
1
@Nav Jeśli sprawisz, że metoda jest ostateczna, klasa nadpisująca nie będzie mogła mieć własnej wersji tej metody (lub będzie to błąd kompilacji). W ten sposób wiesz, że zachowanie, które zaimplementujesz w tej metodzie, nie zmieni się później, gdy ktoś inny rozszerzy klasę.
Bill the Lizard
19

Jednym z powodów, dla których klasa kończy się klasą, byłaby chęć narzucenia kompozycji zamiast dziedziczenia. Jest to ogólnie pożądane, aby uniknąć ścisłego powiązania między klasami.

John Topley
źródło
15

Istnieją 3 przypadki użycia, w których możesz przejść do ostatecznych metod.

  1. Aby uniknąć przesłaniania przez klasę pochodną określonej funkcji klasy bazowej.
  2. Ma to na celu zapewnienie bezpieczeństwa, gdzie klasa bazowa zapewnia ważną podstawową funkcjonalność frameworka, gdzie klasa pochodna nie powinna jej zmieniać.
  3. Metody końcowe są szybsze niż metody instancji, ponieważ nie ma koncepcji tabeli wirtualnej dla metod ostatecznych i prywatnych. Więc gdziekolwiek jest taka możliwość, spróbuj użyć ostatecznych metod.

Cel zaliczenia klasy:

Aby żadne ciało nie mogło rozszerzyć tych klas i zmienić ich zachowania.

Np .: Klasa opakowująca Integer jest klasą końcową. Jeśli ta klasa nie jest ostateczna, to każdy może rozszerzyć Integer na swoją własną klasę i zmienić podstawowe zachowanie klasy całkowitej. Aby tego uniknąć, java utworzyła wszystkie klasy opakowania jako klasy końcowe.

user1923551
źródło
Twój przykład nie jest zbyt przekonany. Jeśli tak jest, dlaczego nie uczynić ostatecznymi metod i zmiennych zamiast uczynić całą klasę ostateczną. Czy możesz edytować swoją odpowiedź ze szczegółami.
Rajkiran
4
Nawet jeśli zmienisz wszystkie zmienne i metody jako ostateczne, jeszcze inni mogą odziedziczyć twoją klasę i dodać dodatkowe funkcje oraz zmienić podstawowe zachowanie twojej klasy.
user1923551
3
> Jeśli ta klasa nie jest ostateczna, to każdy może rozszerzyć Integer do swojej własnej klasy i zmienić podstawowe zachowanie klasy całkowitej. Powiedzmy, że było to dozwolone i ta nowa klasa została wywołana DerivedInteger, nadal nie zmienia oryginalnej klasy Integer, a ktokolwiek używa, DerivedIntegerrobi to na własne ryzyko, więc nadal nie rozumiem, dlaczego jest to problem.
Siddhartha,
7

Dziedziczenie jest jak piła łańcuchowa - bardzo potężna, ale okropna w niepowołanych rękach. Albo zaprojektujesz klasę, z której będzie dziedziczona (co może ograniczyć elastyczność i zająć dużo więcej czasu), albo powinieneś tego zabronić.

Zobacz Effective Java 2. wydanie, pozycje 16 i 17 lub mój wpis na blogu „Podatek spadkowy” .

Jon Skeet
źródło
Link do wpisu na blogu jest uszkodzony. Czy myślisz też, że mógłbyś trochę więcej na ten temat rozwinąć?
@MichaelT: Naprawiono link, dzięki - podążaj za nim, aby uzyskać więcej informacji :) (niestety, nie mam teraz czasu na dodawanie do tego.)
Jon Skeet
@JonSkeet, Link do wpisu na blogu jest uszkodzony.
Lucyfer
@Kedarnath: To działa dla mnie ... przez jakiś czas było zepsute, ale teraz wskazuje na właściwą stronę, na codeblog.jonskeet.uk ...
Jon Skeet
4

Hmmm ... przychodzą mi do głowy dwie rzeczy:

Być może masz klasę zajmującą się pewnymi kwestiami bezpieczeństwa. Podklasyfikując go i dostarczając do systemu jego podklasę, atakujący może obejść ograniczenia bezpieczeństwa. Np. Twoja aplikacja może obsługiwać wtyczki i jeśli wtyczka może po prostu podklasować klasy związane z bezpieczeństwem, może użyć tej sztuczki, aby w jakiś sposób przemycić jej podklasę na miejsce. Jednak jest to raczej coś, z czym Sun musi sobie radzić w odniesieniu do apletów i tym podobnych, może nie jest to tak realistyczny przypadek.

O wiele bardziej realistyczne jest unikanie zmienności obiektu. Np. Ponieważ ciągi znaków są niezmienne, Twój kod może bezpiecznie przechowywać odniesienia do niego

 String blah = someOtherString;

zamiast najpierw kopiować ciąg. Jeśli jednak możesz podklasę String, możesz dodać do niego metody, które pozwalają na modyfikację wartości ciągu, teraz żaden kod nie może już polegać na tym, że ciąg pozostanie taki sam, jeśli po prostu skopiuje ciąg jak powyżej, zamiast tego musi powielić strunowy.

Mecki
źródło
2

Ponadto, jeśli piszesz komercyjną klasę o zamkniętym kodzie źródłowym, możesz nie chcieć, aby ludzie mogli zmieniać funkcjonalność w dół linii, zwłaszcza jeśli potrzebujesz jej wsparcia, a ludzie zastąpili twoją metodę i narzekają, że wywołanie jej daje nieoczekiwane wyniki.

DavidG
źródło
2

Jeśli oznaczysz klasy i metody jako ostateczne, możesz zauważyć niewielki wzrost wydajności, ponieważ środowisko wykonawcze nie musi wyszukiwać właściwej metody klasy do wywołania dla danego obiektu. Metody nieokończone są oznaczane jako wirtualne, aby można je było odpowiednio rozszerzyć w razie potrzeby, metody końcowe można bezpośrednio łączyć lub kompilować w klasie.

seanalltogether
źródło
1
Uważam, że to mit, ponieważ maszyny wirtualne obecnej generacji powinny być w stanie optymalizować i deoptymalizować w razie potrzeby. Pamiętam, że widziałem to jako punkt odniesienia i klasyfikowałem to jako mit.
Miguel Ping
1
Różnica w generowaniu kodu nie jest mitem, ale być może wzrost wydajności tak. Jak powiedziałem, jest to niewielki zysk, jedna witryna wspomniała o 3,5% wzroście wydajności, trochę wyższym, co w większości przypadków nie jest warte używania nazistowskiego kodu.
seanalltogether
1
To nie jest mit. W rzeczywistości kompilator może wbudować tylko końcowe metody V. Nie jestem pewien, czy jakiekolwiek elementy JIT są „wbudowane” w środowisku wykonawczym, ale wątpię, czy może to być możliwe, ponieważ później możesz ładować klasę pochodną.
Uri
1
Microbenchmark == ziarno soli. To powiedziawszy, po rozgrzaniu cyklu 10B, średnio ponad 10B połączeń nie pokazuje zasadniczo żadnej różnicy w Eclipse na moim laptopie. Dwie metody zwracające String, ostateczna to 6,9643us, niefinałowa to 6,9641us. Ta delta to szum tła, IMHO.
joel.neely
1

Aby powstrzymać ludzi przed robieniem rzeczy, które mogą zmylić siebie i innych. Wyobraź sobie bibliotekę fizyki, w której masz zdefiniowane stałe lub obliczenia. Bez użycia końcowego słowa kluczowego ktoś mógłby przyjść i przedefiniować podstawowe obliczenia lub stałe, które NIGDY nie powinny się zmieniać.

Anson Smith
źródło
2
Jest coś, czego nie rozumiem w tym argumencie - jeśli ktoś zmienił te obliczenia / stałe, które nie powinny się zmieniać - czy nie ma żadnych awarii z tego powodu w 100% z ich winy? Innymi słowy, jakie korzyści przyniesie ta zmiana?
mat b
Tak, z technicznego punktu widzenia to „ich” wina, ale wyobraź sobie, że ktoś inny korzystający z biblioteki, który nie został poinformowany o zmianach, może prowadzić do zamieszania. Zawsze myślałem, że to był powód, dla którego wiele klas Java Base zostało oznaczonych jako ostateczne, aby powstrzymać ludzi przed zmienianiem podstawowych funkcji.
Anson Smith
@matt b - Wydaje się, że zakładasz, że programista, który wprowadził zmianę, zrobił to świadomie lub że obchodzi go, że to ich wina. Jeśli ktoś oprócz mnie będzie używał mojego kodu, oznaczę go jako ostateczny, chyba że ma być zmieniony.
Eric Weilnau
W rzeczywistości jest to inne użycie słowa „końcowy” niż to, o które pytano.
DJClayworth
Ta odpowiedź wydaje się mylić dziedziczenie i zmienność poszczególnych pól z możliwością napisania podklasy. Możesz oznaczyć poszczególne pola i metody jako ostateczne (zapobiegając ich redefinicji) bez zakazu dziedziczenia.
joel.neely
0

Chcesz, aby metoda była ostateczna, aby zastępowanie klas nie zmieniało jej zachowania. Jeśli chcesz mieć możliwość zmiany zachowania, upublicznij metodę. Kiedy zastępujesz metodę publiczną, można ją zmienić.

Alexander Robinson
źródło