Treść
- Wstęp
- Interfejsy
- Znaczniki interfejsu
- Interfejsy funkcjonalne, metody statyczne i metody domyślne
- Zajęcia abstrakcyjne
- Klasy niezmienne (trwałe).
- Zajęcia anonimowe
- Widoczność
- Dziedzictwo
- Dziedziczenie wielokrotne
- Dziedziczenie i kompozycja
- Kapsułkowanie
- Zajęcia końcowe i metody
- Co dalej
- Pobierz kod źródłowy
1. WSTĘP
Bez względu na to, jakiego języka programowania używasz (a Java nie jest wyjątkiem), przestrzeganie zasad dobrego projektowania jest kluczem do pisania czystego, zrozumiałego i weryfikowalnego kodu; a także stworzyć go tak, aby był trwały i łatwo wspierał rozwiązywanie problemów. W tej części samouczka omówimy podstawowe elementy składowe języka Java i przedstawimy kilka zasad projektowania, które pomogą Ci podejmować lepsze decyzje projektowe. Mówiąc dokładniej, omówimy interfejsy i interfejsy wykorzystujące metody domyślne (nowa funkcja w Javie 8), klasy abstrakcyjne i końcowe, klasy niezmienne, dziedziczenie, kompozycję i ponownie omówimy zasady widoczności (lub dostępności), o których pokrótce wspomnieliśmy w Część 1 lekcja
„Jak tworzyć i niszczyć obiekty” .
2. INTERFEJSY
W programowaniu obiektowym koncepcja interfejsów stanowi podstawę tworzenia kontraktów . Krótko mówiąc, interfejsy definiują zestaw metod (kontraktów), a każda klasa wymagająca obsługi tego konkretnego interfejsu musi zapewniać implementację tych metod: jest to dość prosty, ale potężny pomysł. Wiele języków programowania ma interfejsy w takiej czy innej formie, ale w szczególności Java zapewnia obsługę tego języka. Przyjrzyjmy się prostej definicji interfejsu w Javie.
package com.javacodegeeks.advanced.design;
public interface SimpleInterface {
void performAction();
}
W powyższym fragmencie interfejs, który wywołaliśmy
SimpleInterface
, deklaruje tylko jedną metodę o nazwie
performAction
. Główna różnica między interfejsami a klasami polega na tym, że interfejsy określają, jaki powinien być kontakt (deklarują metodę), ale nie zapewniają ich implementacji. Jednak interfejsy w Javie mogą być bardziej złożone: mogą zawierać zagnieżdżone interfejsy, klasy, liczniki, adnotacje i stałe. Na przykład:
package com.javacodegeeks.advanced.design;
public interface InterfaceWithDefinitions {
String CONSTANT = "CONSTANT";
enum InnerEnum {
E1, E2;
}
class InnerClass {
}
interface InnerInterface {
void performInnerAction();
}
void performAction();
}
W tym bardziej złożonym przykładzie istnieje kilka ograniczeń, które interfejsy bezwarunkowo nakładają na zagnieżdżone konstrukcje i deklaracje metod wymuszane przez kompilator Java. Po pierwsze, nawet jeśli nie jest to jawnie zadeklarowane, każda deklaracja metody w interfejsie jest
publiczna (i może być tylko publiczna). Zatem następujące deklaracje metod są równoważne:
public void performAction();
void performAction();
Warto wspomnieć, że każda pojedyncza metoda w interfejsie jest domyślnie zadeklarowana jako
abstrakcyjna i nawet te deklaracje metod są równoważne:
public abstract void performAction();
public void performAction();
void performAction();
Jeśli chodzi o zadeklarowane pola stałe, oprócz tego, że są
publiczne , są one również domyślnie
statyczne i oznaczone jako
final . Dlatego następujące deklaracje są również równoważne:
String CONSTANT = "CONSTANT";
public static final String CONSTANT = "CONSTANT";
Wreszcie zagnieżdżone klasy, interfejsy lub liczniki, oprócz tego, że są
publiczne , są również domyślnie deklarowane
jako statyczne . Na przykład te deklaracje są również równoważne:
class InnerClass {
}
static class InnerClass {
}
Wybrany styl jest kwestią osobistych preferencji, ale znajomość tych prostych właściwości interfejsów może uchronić Cię przed niepotrzebnym pisaniem.
3. Znacznik interfejsu
Interfejs znacznika to specjalny rodzaj interfejsu, który nie zawiera metod ani innych zagnieżdżonych konstrukcji. Jak definiuje to biblioteka Java:
public interface Cloneable {
}
Znaczniki interfejsu nie są kontraktami per se, ale są dość przydatną techniką „dołączania” lub „kojarzenia” określonej cechy z klasą. Na przykład w odniesieniu do
Cloneable klasa jest oznaczona jako możliwa do sklonowania, ale sposób, w jaki można lub należy to zaimplementować, nie jest częścią interfejsu. Innym bardzo znanym i szeroko stosowanym przykładem znacznika interfejsu jest
Serializable
:
public interface Serializable {
}
Interfejs ten oznacza klasę jako odpowiednią do serializacji i deserializacji i ponownie nie określa, w jaki sposób można lub należy to zaimplementować. Znaczniki interfejsu mają swoje miejsce w programowaniu obiektowym, chociaż nie spełniają głównego celu interfejsu, jakim jest kontrakt.
4. INTERFEJSY FUNKCJONALNE, METODY DOMYŚLNE I METODY STATYCZNE
Od czasu wydania Java 8 interfejsy zyskały kilka bardzo interesujących nowych funkcji: metody statyczne, metody domyślne i automatyczną konwersję z lambd (interfejsy funkcjonalne). W sekcji interfejsy podkreśliliśmy fakt, że interfejsy w Javie mogą jedynie deklarować metody, ale nie zapewniają ich implementacji. W przypadku metody domyślnej sytuacja wygląda inaczej: interfejs może oznaczyć metodę słowem kluczowym
default i zapewnić jej implementację. Na przykład:
package com.javacodegeeks.advanced.design;
public interface InterfaceWithDefaultMethods {
void performAction();
default void performDefaulAction() {
}
}
Będąc na poziomie instancji, metody domyślne można zastąpić każdą implementacją interfejsu, ale interfejsy mogą teraz zawierać także metody
statyczne , na przykład: pakiet com.javacodegeeks.advanced.design;
public interface InterfaceWithDefaultMethods {
static void createAction() {
}
}
Można powiedzieć, że zapewnienie implementacji w interfejsie przeczy całemu celowi programowania kontraktowego. Istnieje jednak wiele powodów, dla których te funkcje zostały wprowadzone do języka Java i niezależnie od tego, jak bardzo są przydatne lub zagmatwane, są one dostępne dla Ciebie i Twojego użytku. Interfejsy funkcjonalne to zupełnie inna historia i okazały się bardzo przydatnym dodatkiem do języka. Zasadniczo interfejs funkcjonalny to interfejs z zadeklarowaną tylko jedną abstrakcyjną metodą.
Runnable
Standardowy interfejs biblioteki jest bardzo dobrym przykładem tej koncepcji.
@FunctionalInterface
public interface Runnable {
void run();
}
Kompilator Java traktuje interfejsy funkcjonalne w różny sposób i może zamienić funkcję lambda w implementację interfejsu funkcjonalnego, jeśli ma to sens. Rozważmy następujący opis funkcji:
public void runMe( final Runnable r ) {
r.run();
}
Aby wywołać tę funkcję w Javie 7 i niższych należy zapewnić implementację interfejsu
Runnable
(np. przy użyciu klas anonimowych), natomiast w Javie 8 wystarczy zapewnić implementację metody run() przy użyciu składni lambda:
runMe( () -> System.out.println( "Run!" ) );
Dodatkowo adnotacja
@FunctionalInterface (adnotacje zostaną szczegółowo omówione w części 5 tutoriala) podpowiada, że kompilator może sprawdzić, czy interfejs zawiera tylko jedną metodę abstrakcyjną, więc jakiekolwiek zmiany wprowadzone w interfejsie w przyszłości nie naruszą tego założenia .
5. ZAJĘCIA STRESZCZONE
Kolejną ciekawą koncepcją wspieraną przez język Java jest koncepcja klas abstrakcyjnych. Klasy abstrakcyjne są nieco podobne do interfejsów w Javie 7 i są bardzo zbliżone do domyślnych metod interfejsu w Javie 8. W przeciwieństwie do zwykłych klas, nie można utworzyć instancji klasy abstrakcyjnej, ale można ją podklasować (więcej szczegółów można znaleźć w sekcji Dziedziczenie). Co ważniejsze, klasy abstrakcyjne mogą zawierać metody abstrakcyjne: specjalny rodzaj metody bez implementacji, podobnie jak interfejs. Na przykład:
package com.javacodegeeks.advanced.design;
public abstract class SimpleAbstractClass {
public void performAction() {
}
public abstract void performAnotherAction();
}
W tym przykładzie klasa
SimpleAbstractClass
jest zadeklarowana jako
abstrakcyjna i zawiera jedną zadeklarowaną metodę abstrakcyjną. Klasy abstrakcyjne są bardzo przydatne; większość lub nawet niektóre części szczegółów implementacji mogą być współużytkowane przez wiele podklas. Tak czy inaczej, nadal pozostawiają uchylone drzwi i pozwalają dostosować zachowanie właściwe każdej z podklas za pomocą metod abstrakcyjnych. Warto wspomnieć, że w przeciwieństwie do interfejsów, które mogą zawierać jedynie
publiczne deklaracje, klasy abstrakcyjne mogą wykorzystywać pełną moc reguł dostępności do kontrolowania widoczności metody abstrakcyjnej.
6. ZAJĘCIA BEZPOŚREDNIE
Niezmienność staje się obecnie coraz ważniejsza w tworzeniu oprogramowania. Pojawienie się systemów wielordzeniowych spowodowało wiele problemów związanych z udostępnianiem danych i równoległością. Ale na pewno pojawił się jeden problem: posiadanie niewielkiego (lub nawet żadnego) stanu zmiennego prowadzi do lepszej rozszerzalności (skalowalności) i łatwiejszego wnioskowania o systemie. Niestety, język Java nie zapewnia przyzwoitej obsługi niezmienności klas. Jednak stosując kombinację technik, możliwe staje się projektowanie klas, które są niezmienne. Przede wszystkim wszystkie pola klasy muszą być ostateczne (oznaczone jako
final ). To dobry początek, ale nie gwarantuje.
package com.javacodegeeks.advanced.design;
import java.util.Collection;
public class ImmutableClass {
private final long id;
private final String[] arrayOfStrings;
private final Collection<String> collectionOfString;
}
Po drugie, zadbaj o odpowiednią inicjalizację: jeśli pole jest referencją do kolekcji lub tablicy, nie przypisuj tych pól bezpośrednio z argumentów konstruktora, tylko wykonaj kopie. Dzięki temu stan kolekcji lub tablicy nie zostanie zmodyfikowany poza nią.
public ImmutableClass( final long id, final String[] arrayOfStrings,
final Collection<String> collectionOfString) {
this.id = id;
this.arrayOfStrings = Arrays.copyOf( arrayOfStrings, arrayOfStrings.length );
this.collectionOfString = new ArrayList<>( collectionOfString );
}
I wreszcie zapewnienie odpowiedniego dostępu (gettery). W przypadku kolekcji niezmienność musi być zapewniona jako opakowanie
Collections.unmodifiableXxx
: w przypadku tablic jedynym sposobem zapewnienia prawdziwej niezmienności jest dostarczenie kopii zamiast zwracania odniesienia do tablicy. Może to być nie do przyjęcia z praktycznego punktu widzenia, ponieważ jest bardzo zależne od rozmiaru tablicy i może wywierać ogromny nacisk na moduł zbierający elementy bezużyteczne.
public String[] getArrayOfStrings() {
return Arrays.copyOf( arrayOfStrings, arrayOfStrings.length );
}
Nawet ten mały przykład dobrze pokazuje, że niezmienność nie jest jeszcze w Javie obywatelem pierwszej klasy. Sprawy mogą się skomplikować, jeśli niezmienna klasa ma pole odwołujące się do obiektu innej klasy. Klasy te również powinny być niezmienne, ale nie ma sposobu, aby to zapewnić. Istnieje kilka przyzwoitych analizatorów kodu źródłowego Java, takich jak FindBugs i PMD, które mogą bardzo pomóc, sprawdzając kod i wskazując typowe wady programowania Java. Narzędzia te są świetnymi przyjaciółmi każdego programisty Java.
7. ZAJĘCIA ANONIMOWE
W epoce poprzedzającej Java 8 klasy anonimowe były jedynym sposobem na zapewnienie, że klasy będą definiowane na bieżąco i natychmiastowo tworzone. Celem zajęć anonimowych było ograniczenie schematów i zapewnienie krótkiego i łatwego sposobu reprezentowania klas jako rekordu. Przyjrzyjmy się typowemu, staromodnemu sposobowi tworzenia nowego wątku w Javie:
package com.javacodegeeks.advanced.design;
public class AnonymousClass {
public static void main( String[] args ) {
new Thread(
new Runnable() {
@Override
public void run() {
}
}
).start();
}
}
W tym przykładzie implementacja
Runnable
interfejsu jest dostarczana natychmiastowo jako klasa anonimowa. Chociaż istnieją pewne ograniczenia związane z klasami anonimowymi, główną wadą ich używania jest bardzo rozwlekła składnia konstrukcji, do której zobowiązuje się Java jako język. Nawet anonimowa klasa, która nic nie robi, wymaga co najmniej 5 linii kodu za każdym razem, gdy jest zapisywana.
new Runnable() {
@Override
public void run() {
}
}
Na szczęście dzięki Javie 8, lambdzie i funkcjonalnym interfejsom wszystkie te stereotypy wkrótce znikną, a pisanie kodu w Javie będzie wyglądać naprawdę zwięźle.
package com.javacodegeeks.advanced.design;
public class AnonymousClass {
public static void main( String[] args ) {
new Thread( () -> { } ).start();
}
}
8. WIDOCZNOŚĆ
Mówiliśmy już trochę o regułach widoczności i dostępności w Javie w części 1 samouczka. W tej części ponownie powrócimy do tego tematu, ale w kontekście podklas.
Widoczność na różnych poziomach umożliwia lub uniemożliwia klasom oglądanie innych klas lub interfejsów (na przykład, jeśli znajdują się w różnych pakietach lub są zagnieżdżone w sobie) lub podklasom wyświetlanie metod, konstruktorów i pól swoich rodziców oraz uzyskiwanie do nich dostępu. W następnej sekcji, dziedziczenie, zobaczymy to w akcji.
9. DZIEDZICZENIE
Dziedziczenie jest jedną z kluczowych koncepcji programowania obiektowego, służącą jako podstawa do konstruowania klasy relacji. W połączeniu z regułami widoczności i dostępności, dziedziczenie umożliwia projektowanie klas w hierarchię, którą można rozszerzać i utrzymywać. Na poziomie koncepcyjnym dziedziczenie w Javie jest implementowane przy użyciu podklas i słowa kluczowego
Extends wraz z klasą nadrzędną. Podklasa dziedziczy wszystkie publiczne i chronione elementy klasy nadrzędnej. Dodatkowo podklasa dziedziczy elementy pakietu-prywatnego swojej klasy nadrzędnej, jeśli oba (podklasa i klasa) znajdują się w tym samym pakiecie. Biorąc to pod uwagę, bardzo ważne jest, niezależnie od tego, co próbujesz zaprojektować, trzymanie się minimalnego zestawu metod, które klasa udostępnia publicznie lub swoim podklasom. Przyjrzyjmy się na przykład klasie
Parent
i jej podklasie,
Child
aby zademonstrować różnicę w poziomach widoczności i ich efektach.
package com.javacodegeeks.advanced.design;
public class Parent {
public static final String CONSTANT = "Constant";
private String privateField;
protected String protectedField;
private class PrivateClass {
}
protected interface ProtectedInterface {
}
public void publicAction() {
}
protected void protectedAction() {
}
private void privateAction() {
}
void packageAction() {
}
}
package com.javacodegeeks.advanced.design;
public class Child extends Parent implements Parent.ProtectedInterface {
@Override
protected void protectedAction() {
super.protectedAction();
}
@Override
void packageAction() {
}
public void childAction() {
this.protectedField = "value";
}
}
Dziedziczenie to bardzo obszerny temat sam w sobie, zawierający wiele drobnych szczegółów specyficznych dla języka Java. Istnieje jednak kilka zasad, których można łatwo przestrzegać i które mogą znacznie pomóc w utrzymaniu zwięzłości hierarchii klas. W Javie każda podklasa może zastąpić wszelkie odziedziczone metody swojego rodzica, chyba że zostanie uznana za ostateczną. Jednakże nie ma specjalnej składni ani słowa kluczowego oznaczającego metodę jako przesłoniętą, co może prowadzić do zamieszania. Właśnie dlatego wprowadzono adnotację
@Override : ilekroć Twoim celem jest zastąpienie odziedziczonej metody, użyj adnotacji
@Override , aby to zwięźle wskazać. Kolejnym dylematem, przed którym stale stają programiści Java podczas projektowania, jest konstrukcja hierarchii klas (z klasami konkretnymi lub abstrakcyjnymi) w porównaniu z implementacją interfejsów. Zdecydowanie zalecamy, jeśli to możliwe, faworyzowanie interfejsów zamiast klas lub klas abstrakcyjnych. Interfejsy są lżejsze, łatwiejsze w testowaniu i utrzymaniu, a także minimalizują skutki uboczne zmian wdrożeniowych. Wiele zaawansowanych technik programowania, takich jak tworzenie klas proxy w standardowej bibliotece Java, w dużym stopniu opiera się na interfejsach.
10. DZIEDZICZENIE WIELOKROTNE
W przeciwieństwie do C++ i niektórych innych języków, Java nie obsługuje dziedziczenia wielokrotnego: w Javie każda klasa może mieć tylko jednego bezpośredniego rodzica (z klasą
Object
na górze hierarchii). Jednakże klasa może implementować wiele interfejsów, dlatego też układanie interfejsów to jedyny sposób na osiągnięcie (lub symulację) wielokrotnego dziedziczenia w Javie.
package com.javacodegeeks.advanced.design;
public class MultipleInterfaces implements Runnable, AutoCloseable {
@Override
public void run() {
}
@Override
public void close() throws Exception {
}
}
Implementacja wielu interfejsów ma naprawdę duże możliwości, ale często potrzeba wielokrotnego korzystania z implementacji prowadzi do głębokiej hierarchii klas, co pozwala przezwyciężyć brak obsługi wielokrotnego dziedziczenia w Javie.
public class A implements Runnable {
@Override
public void run() {
}
}
public class B extends A implements AutoCloseable {
@Override
public void close() throws Exception {
}
}
public class C extends B implements Readable {
@Override
public int read(java.nio.CharBuffer cb) throws IOException {
}
}
I tak dalej... Najnowsza wersja Java 8 w pewnym stopniu rozwiązuje problem wstrzykiwania metody domyślnej. Ze względu na metody domyślne interfejsy w rzeczywistości zapewniają nie tylko kontrakt, ale także implementację. Dlatego klasy implementujące te interfejsy również automatycznie odziedziczą te zaimplementowane metody. Na przykład:
package com.javacodegeeks.advanced.design;
public interface DefaultMethods extends Runnable, AutoCloseable {
@Override
default void run() {
}
@Override
default void close() throws Exception {
}
}
public class C implements DefaultMethods, Readable {
@Override
public int read(java.nio.CharBuffer cb) throws IOException {
}
}
Należy pamiętać, że dziedziczenie wielokrotne jest bardzo potężnym, ale jednocześnie niebezpiecznym narzędziem. Dobrze znany problem Diamentu Śmierci jest często wymieniany jako główna wada implementacji dziedziczenia wielokrotnego, zmuszająca programistów do bardzo ostrożnego projektowania hierarchii klas. Niestety interfejsy Java 8 z metodami domyślnymi również padają ofiarą tych defektów.
interface A {
default void performAction() {
}
}
interface B extends A {
@Override
default void performAction() {
}
}
interface C extends A {
@Override
default void performAction() {
}
}
Na przykład następujący fragment kodu nie zostanie skompilowany:
interface E extends B, C {
}
W tym miejscu można śmiało powiedzieć, że Java jako język zawsze starała się unikać skrajnych przypadków programowania obiektowego, ale w miarę ewolucji języka niektóre z tych przypadków nagle zaczęły się pojawiać.
11. DZIEDZICZENIE I SKŁAD
Na szczęście dziedziczenie nie jest jedynym sposobem projektowania klasy. Inną alternatywą, zdaniem wielu programistów, znacznie lepszą niż dziedziczenie, jest kompozycja. Pomysł jest bardzo prosty: zamiast tworzyć hierarchię klas, należy je składać z innych klas. Spójrzmy na ten przykład:
interface E extends B, C {
}
Klasa
Vehicle
składa się z silnika i kół (oraz wielu innych części, które dla uproszczenia pominięto). Można jednak powiedzieć, że klasa
Vehicle
jest także silnikiem, więc można ją projektować za pomocą dziedziczenia.
public class Vehicle extends Engine {
private Wheels[] wheels;
}
Które rozwiązanie projektowe będzie właściwe? Ogólne podstawowe wytyczne są znane jako zasady IS-A (jest) i HAS-A (zawiera). IS-A to relacja dziedziczenia: podklasa spełnia również specyfikację klasy klasy nadrzędnej i jest odmianą klasy nadrzędnej. podklasa) rozszerza swojego rodzica. Jeśli chcesz wiedzieć, czy jedna jednostka rozszerza drugą, wykonaj test dopasowania - IS -A (jest).") Dlatego HAS-A jest relacją kompozycji: klasa posiada (lub zawiera) obiekt, który W większości przypadków zasada HAS-A działa lepiej niż IS-A z wielu powodów:
- Projekt jest bardziej elastyczny;
- Model jest bardziej stabilny, ponieważ zmiana nie rozprzestrzenia się w hierarchii klas;
- Klasa i jej skład są luźno powiązane w porównaniu z kompozycją, która ściśle łączy klasę nadrzędną i jej podklasę.
- Logiczny tok myślenia w klasie jest prostszy, ponieważ wszystkie jej zależności są w niej zawarte, w jednym miejscu.
Niezależnie od tego, dziedziczenie ma swoje miejsce i rozwiązuje wiele istniejących problemów projektowych na różne sposoby, więc nie należy go zaniedbywać. Projektując model obiektowy, należy pamiętać o tych dwóch alternatywach.
12. ENKAPSULACJA.
Koncepcja enkapsulacji w programowaniu obiektowym polega na ukryciu wszystkich szczegółów implementacji (takich jak tryb działania, metody wewnętrzne itp.) przed światem zewnętrznym. Zaletami enkapsulacji są łatwość konserwacji i łatwość wprowadzania zmian. Wewnętrzna implementacja klasy jest ukryta, praca z danymi klasy odbywa się wyłącznie za pomocą publicznych metod klasy (prawdziwy problem, jeśli tworzysz bibliotekę lub framework używany przez wiele osób). Hermetyzację w Javie osiąga się poprzez reguły widoczności i dostępności. W Javie za najlepszą praktykę uważa się, aby nigdy nie ujawniać pól bezpośrednio, tylko za pomocą metod pobierających i ustawiających (chyba że pola są oznaczone jako ostateczne). Na przykład:
package com.javacodegeeks.advanced.design;
public class Encapsulation {
private final String email;
private String address;
public Encapsulation( final String email ) {
this.email = email;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getEmail() {
return email;
}
}
Ten przykład przypomina tak zwane komponenty
JavaBeans w języku Java: standardowe klasy Java są pisane zgodnie z zestawem konwencji, z których jedna umożliwia dostęp do pól wyłącznie za pomocą metod pobierających i ustawiających. Jak już podkreślaliśmy w części dotyczącej dziedziczenia, prosimy zawsze przestrzegać minimalnego kontraktu reklamowego w klasie, stosując zasady enkapsulacji. Wszystko, co nie powinno być publiczne, powinno stać się prywatne (lub chronione/pakietowe, w zależności od problemu, który rozwiązujesz). Opłaci się to na dłuższą metę, zapewniając swobodę projektowania bez (lub przynajmniej minimalizując) istotnych zmian.
13. ZAJĘCIA I METODY KOŃCOWE
W Javie istnieje sposób, aby zapobiec temu, aby klasa stała się podklasą innej klasy: druga klasa musi zostać uznana za ostateczną.
package com.javacodegeeks.advanced.design;
public final class FinalClass {
}
To samo słowo kluczowe
final w deklaracji metody zapobiega przesłanianiu metody przez podklasy.
package com.javacodegeeks.advanced.design;
public class FinalMethod {
public final void performAction() {
}
}
Nie ma ogólnych zasad decydujących o tym, czy klasa lub metody powinny być ostateczne, czy nie. Klasy i metody końcowe ograniczają rozszerzalność i bardzo trudno jest przewidzieć, czy klasa powinna być dziedziczona, czy też nie, lub czy metoda powinna być w przyszłości zastępowana. Jest to szczególnie ważne dla twórców bibliotek, ponieważ tego typu decyzje projektowe mogą znacznie ograniczyć możliwości zastosowania biblioteki. Biblioteka standardowa Java zawiera kilka przykładów klas końcowych, z których najsłynniejszą jest klasa String. Decyzja ta została podjęta na wczesnym etapie, aby zapobiec wszelkim próbom deweloperów wymyślenia własnego, „lepszego” rozwiązania w zakresie implementacji string.
14. CO DALEJ
W tej części lekcji omówiliśmy koncepcje programowania obiektowego w Javie. Przyjrzeliśmy się także szybko programowaniu kontraktowemu, poruszyliśmy niektóre koncepcje funkcjonalne i sprawdziliśmy, jak język ewoluował na przestrzeni czasu. W następnej części lekcji poznamy typy generyczne i to, jak zmieniają one sposób, w jaki podchodzimy do bezpieczeństwa typów w programowaniu.
15. POBIERZ KOD ŹRÓDŁOWY
Źródło możesz pobrać tutaj -
Advanced-Java-part-3 Źródło:
Jak projektować klasy an
GO TO FULL VERSION