Paradygmaty programowania
Programowanie strukturalne vs. programowanie obiektowe (OOP), hermetyzacja (enkapsulacja), dziedziczenie, polimorfizm, klasy abstrakcyjne i interfejsy.
Wstęp: ten dział rozstrzyga się na czterech pojęciach
Egzamin z INF.04 nie pyta „czym jest programowanie obiektowe". Pyta, co wypisze
program, w którym metoda klasy pochodnej ma tę samą sygnaturę co metoda klasy
bazowej. Pyta, który modyfikator dostępu udostępnia pole klasie pochodnej,
ale nie kodowi na zewnątrz. Pyta, ile interfejsów może zaimplementować jedna
klasa. Cały dział sprowadza się do czterech filarów — hermetyzacji,
dziedziczenia, polimorfizmu i abstrakcji — plus do jednej pary pojęć, którą
myli najwięcej osób: przeciążanie kontra przesłanianie. Przykłady w tym
artykule pisane są w Javie i krążą wokół jednej rodziny klas:
Pracownik,
Programista,
Menedzer.
📋 Spis treści
Podstawy i strukturalnie
1
Czym jest paradygmat: imperatywnie vs deklaratywnie
Paradygmat programowania to sposób myślenia o programie: co jest podstawową „cegiełką", z której go budujesz. W paradygmacie strukturalnym cegiełką jest instrukcja i funkcja, w obiektowym — obiekt, w funkcyjnym — funkcja matematyczna. To nie jest cecha języka, tylko stylu pisania: w tym samym języku (Java, C++, Python) można pisać strukturalnie i obiektowo, a większość współczesnych języków jest wieloparadygmatowa.
Opisujesz krok po kroku, jak komputer ma dojść do wyniku: przypisz, sprawdź warunek, powtórz, zmień zmienną. Kluczowe jest pojęcie stanu programu, który zmienia się z instrukcji na instrukcję. Odmiany: programowanie proceduralne, strukturalne i obiektowe. Języki: C, C++, Java, C#, Python, asembler.
Opisujesz, co ma być wynikiem, a sposób wyliczenia zostawiasz środowisku. Nie sterujesz kolejnością kroków ani zmiennymi pomocniczymi. Odmiany: programowanie funkcyjne, logiczne, języki zapytań i opisu. Przykłady: SQL, Prolog, Haskell, HTML i CSS (opis, nie algorytm).
Zadanie: policz sumę liczb parzystych z tablicy. Po lewej myślenie imperatywne (pętla, licznik, zmienna sumy), po prawej deklaratywne (opis warunku i operacji).
// IMPERATYWNIE - opisuję każdy krok
int[] liczby = {4, 7, 10, 3, 8};
int suma = 0;
for (int i = 0; i < liczby.length; i++) {
if (liczby[i] % 2 == 0) {
suma = suma + liczby[i];
}
}
System.out.println(suma); // 22
// DEKLARATYWNIE (styl funkcyjny) - opisuję warunek i wynik
int suma2 = Arrays.stream(liczby)
.filter(n -> n % 2 == 0)
.sum();
-- DEKLARATYWNIE w SQL - nie ma słowa o pętli
SELECT SUM(wartosc) FROM liczby WHERE wartosc % 2 = 0;Mapa paradygmatów do zapamiętania
Imperatywne: proceduralne, strukturalne (bez goto, trzy konstrukcje
sterujące), obiektowe. Deklaratywne: funkcyjne, logiczne (fakty i reguły,
Prolog), zapytań (SQL). Osobno wymienia się programowanie zdarzeniowe.
2
Programowanie strukturalne i trzy konstrukcje
Programowanie strukturalne to odmiana imperatywnego, w której rezygnujesz
ze skoków goto,
a program budujesz wyłącznie z trzech konstrukcji sterujących. Twierdzenie
Böhma i Jacopiniego mówi, że te trzy konstrukcje wystarczą do zapisania
dowolnego algorytmu — to teza, którą warto znać z nazwy.
Instrukcje wykonywane jedna po drugiej, w kolejności zapisu. Na schemacie blokowym: prostokąty połączone pionową strzałką, bez rozgałęzień.
Rozgałęzienie zależne od warunku:
if,
if-else,
switch.
Na schemacie: romb z dwoma wyjściami TAK/NIE.
Powtarzanie bloku instrukcji:
for,
while (warunek
sprawdzany przed), do-while (po,
więc wykona się co najmniej raz).
// SEKWENCJA
double brutto = 6000;
double skladki = brutto * 0.1371;
double podstawa = brutto - skladki;
// WYBOR
if (podstawa <= 10000) {
System.out.println("Pierwszy prog podatkowy");
} else {
System.out.println("Drugi prog podatkowy");
}
// ITERACJA - wyplaty za 12 miesiecy
double rocznie = 0;
for (int miesiac = 1; miesiac <= 12; miesiac++) {
rocznie += podstawa;
}
// ITERACJA z warunkiem sprawdzanym PO wykonaniu - zawsze min. 1 przebieg
int proba = 0;
do {
proba++;
} while (proba < 3);
Każdy blok strukturalny (funkcja, pętla, instrukcja warunkowa) ma dokładnie jeden punkt wejścia i jeden punkt wyjścia. Dzięki temu blok da się traktować jak pojedynczy prostokąt na schemacie blokowym i wstawić w dowolne miejsce innego bloku. Skok do środka pętli z zewnątrz łamie tę zasadę i tworzy tzw. kod spaghetti — nieczytelny i praktycznie niemożliwy do przetestowania.
- Konsekwencja praktyczna: instrukcja
gotojest w programowaniu strukturalnym zakazana. - Sporne, ale dopuszczalne:
break,continuei wiele instrukcjireturnto formalnie dodatkowe wyjścia. W praktyce się je stosuje, ale na egzaminie zasadę podajesz w wersji klasycznej. - Podział na podprogramy: duży algorytm dzielisz na funkcje (zwracają wartość) i procedury (nie zwracają, w Javie i C++ typ
void). To metoda „dziel i zwyciężaj" zastosowana do kodu.
// PRZED: jeden wielki blok, wszystko wymieszane
double brutto = 6000, godziny = 168, stawka = 40;
double wyplata = brutto + godziny * stawka * 0.5;
System.out.println("Do wyplaty: " + wyplata + " zl");
// PO: podzial na podprogramy - kazdy robi jedna rzecz
static double policzNadgodziny(double godziny, double stawka) {
return godziny * stawka * 0.5; // funkcja - zwraca wartosc
}
static void wypisz(String opis, double kwota) {
System.out.println(opis + ": " + kwota + " zl"); // procedura - void
}
public static void main(String[] args) {
double wyplata = 6000 + policzNadgodziny(168, 40);
wypisz("Do wyplaty", wyplata);
}
Po co dzielić na podprogramy
Trzy powody, które warto umieć wymienić: czytelność (nazwa
funkcji mówi, co się dzieje), wielokrotne użycie (poprawiasz
w jednym miejscu), łatwiejsze testowanie. Argumenty
przekazujesz przez wartość (kopia) albo przez referencję — w C++ referencję
zaznaczasz znakiem & przy parametrze.
3
Klasa i obiekt, konstruktor, destruktor, this
Programowanie obiektowe łączy dane i operujący na nich kod w jedną całość. Zamiast osobno trzymać tablicę pensji i osobno funkcję licząca podatek, tworzysz obiekt, który ma pensję w środku i sam potrafi policzyć swój podatek.
Wzorzec, szablon, przepis. Opisuje, jakie dane (pola) i jakie zachowania (metody) będą miały obiekty tego typu. Sama klasa nie zajmuje pamięci na dane — to tylko definicja. Analogia egzaminacyjna: klasa to formularz albo forma do odlewu.
Konkretny egzemplarz klasy utworzony w pamięci, z własnym kompletem wartości pól. Z jednej klasy tworzysz dowolnie wiele obiektów i każdy ma swoje dane. Słowo instancja to synonim obiektu — w poleceniach CKE pojawiają się wymiennie.
public class Pracownik {
// POLA (atrybuty, składowe) - opisują stan obiektu
private String imie;
private String nazwisko;
private double pensjaPodstawowa;
private static int licznikPracownikow = 0; // pole klasy, wspólne dla wszystkich
// KONSTRUKTOR - nazwa taka jak klasy, bez typu zwracanego
public Pracownik(String imie, String nazwisko, double pensjaPodstawowa) {
this.imie = imie; // this = "ten konkretny obiekt"
this.nazwisko = nazwisko;
this.pensjaPodstawowa = pensjaPodstawowa;
licznikPracownikow++;
}
// KONSTRUKTOR PRZECIĄŻONY - inna lista parametrów
public Pracownik(String imie, String nazwisko) {
this(imie, nazwisko, 4666.0); // wywołanie innego konstruktora
}
// METODY - opisują zachowanie obiektu
public double obliczWynagrodzenie() {
return pensjaPodstawowa;
}
public String opis() {
return imie + " " + nazwisko + ", wynagrodzenie: " + obliczWynagrodzenie();
}
}
// TWORZENIE OBIEKTÓW - operator new wywołuje konstruktor
Pracownik p1 = new Pracownik("Anna", "Kowalska", 7200);
Pracownik p2 = new Pracownik("Jan", "Nowak"); // pensja domyślna 4666
System.out.println(p1.opis()); // Anna Kowalska, wynagrodzenie: 7200.0
System.out.println(p2.opis()); // Jan Nowak, wynagrodzenie: 4666.0
// p1 i p2 to DWA obiekty jednej klasy - mają własne kopie pól,
// ale współdzielą pole statyczne licznikPracownikow (teraz = 2)
- Konstruktor — metoda specjalna wywoływana automatycznie przy tworzeniu obiektu. Ma nazwę identyczną z nazwą klasy i nie ma typu zwracanego (nawet void). Służy do nadania polom wartości początkowych. Jeśli nie napiszesz żadnego, kompilator dodaje konstruktor domyślny (bezparametrowy) — ale gdy napiszesz własny z parametrami, domyślny znika.
- Destruktor — metoda wywoływana przy niszczeniu obiektu, sprząta po nim (zwalnia pamięć, zamyka pliki i połączenia). W C++ nazywa się jak klasa z tyldą z przodu i jest kluczowy przy pamięci przydzielanej przez
new. W Javie i C# destruktora nie ma — pamięć zwalnia automatyczny odśmiecacz (garbage collector). - this — referencja do obiektu, na rzecz którego wywołano metodę. Używasz go, gdy parametr ma tę samą nazwę co pole (
this.imie = imie;) oraz do wywołania innego konstruktora tej samej klasy. W C++thisjest wskaźnikiem, więc pisze sięthis->imie. - Składowa statyczna (
static) — należy do klasy, nie do obiektu. Jedna kopia dla wszystkich instancji, wywołujesz przez nazwę klasy.
// TEN SAM MECHANIZM W C++ - z destruktorem i wskaźnikiem this
class Pracownik {
private:
std::string imie;
double pensjaPodstawowa;
public:
Pracownik(std::string imie, double pensja) { // KONSTRUKTOR
this->imie = imie;
this->pensjaPodstawowa = pensja;
std::cout << "Tworze obiekt: " << imie << std::endl;
}
~Pracownik() { // DESTRUKTOR (tylda + nazwa klasy)
std::cout << "Niszcze obiekt: " << imie << std::endl;
}
double obliczWynagrodzenie() { return pensjaPodstawowa; }
};
int main() {
Pracownik p("Anna", 7200); // obiekt na stosie - destruktor wywoła się sam
Pracownik* q = new Pracownik("Jan", 4666);
delete q; // obiekt na stercie - destruktor TYLKO po delete
return 0;
}
4
Hermetyzacja i modyfikatory dostępu
Hermetyzacja (enkapsulacja, ukrywanie implementacji) to ukrycie pól obiektu
przed światem zewnętrznym i udostępnienie ich wyłącznie przez metody. Cel jest
praktyczny: obiekt sam pilnuje, żeby jego dane były poprawne. Jeśli pole
pensjaPodstawowa jest
publiczne, ktokolwiek może wpisać tam liczbę ujemną. Jeśli jest prywatne
i zmieniane tylko setterem z walidacją — nie może.
| Modyfikator | Wewnątrz klasy | W klasie pochodnej | Na zewnątrz | Kiedy stosować |
|---|---|---|---|---|
| public | TAK | TAK | TAK | metody będące interfejsem klasy (gettery, settery, operacje) |
| protected | TAK | TAK | NIE | składowe, które ma widzieć klasa pochodna, ale nie użytkownik klasy |
| private | TAK | NIE | NIE | domyślny wybór dla wszystkich pól |
| (brak) package | TAK | tylko w tym samym pakiecie | tylko w tym samym pakiecie | specyfika Javy; w C++ brak modyfikatora oznacza private w klasie |
Uwaga na różnicę języków: w C++ składowe klasy bez modyfikatora są domyślnie private, a składowe struktury (struct) domyślnie public. W Javie brak modyfikatora to dostęp pakietowy.
public class Pracownik {
private String imie; // ukryte przed swiatem
protected double pensjaPodstawowa; // widoczne dla klas pochodnych
private static final double MINIMALNA = 4666.0;
public Pracownik(String imie, double pensja) {
this.imie = imie;
setPensjaPodstawowa(pensja); // walidacja juz w konstruktorze
}
// GETTER - tylko odczyt, brak parametrow, zwraca wartosc pola
public String getImie() {
return imie;
}
public double getPensjaPodstawowa() {
return pensjaPodstawowa;
}
// SETTER - zapis Z WALIDACJA, typ void, jeden parametr
public void setPensjaPodstawowa(double pensja) {
if (pensja < MINIMALNA) {
throw new IllegalArgumentException("Pensja ponizej minimalnej krajowej");
}
this.pensjaPodstawowa = pensja;
}
}
// Proba obejscia hermetyzacji:
Pracownik p = new Pracownik("Anna", 7200);
// p.pensjaPodstawowa = -500; // BLAD KOMPILACJI - pole nie jest public
p.setPensjaPodstawowa(-500); // wyjatek w czasie wykonania - obiekt sie broni
Treść: „Zadeklaruj pole klasy tak, aby było dostępne w klasie
oraz w klasach po niej dziedziczących, ale niedostępne dla pozostałego kodu
programu."
Odpowiedź: modyfikator protected. To jedyny
modyfikator, który daje dokładnie taką kombinację. Gdyby polecenie mówiło
„dostępne wyłącznie wewnątrz klasy" — private. Gdyby
„dostępne z każdego miejsca programu" — public.
Zasada projektowa: pola zawsze prywatne, dostęp przez
metody. Konwencji nazewniczej (getPole, setPole, dla typu
logicznego isPole) trzymaj się także w zadaniach praktycznych.
5
Dziedziczenie: klasa bazowa i pochodna
Dziedziczenie pozwala zbudować nową klasę na podstawie istniejącej: klasa pochodna przejmuje pola i metody klasy bazowej, a następnie dokłada własne albo zmienia odziedziczone. Testem, czy dziedziczenie jest tu na miejscu, jest zdanie „jest rodzajem": programista jest rodzajem pracownika, więc dziedziczenie ma sens. Silnik nie jest rodzajem samochodu — tam potrzebna jest kompozycja.
- Klasa bazowa = nadklasa = klasa rodzicielska = superklasa.
- Klasa pochodna = podklasa = klasa dziedzicząca.
- Java:
class Programista extends Pracownik - C++:
class Programista : public Pracownik - C#:
class Programista : Pracownik
Pojedyncze — jedna klasa bazowa. Tak działa Java, C#, Python jest tu wyjątkiem. Wielokrotne — kilka klas bazowych naraz, dopuszcza je C++ i Python. Grozi problemem diamentu: jeśli dwie klasy bazowe dziedziczą po tej samej klasie i obie mają metodę o tej samej nazwie, kompilator nie wie, którą wybrać. Java rozwiązuje to zakazem — jedna klasa bazowa, ale dowolnie wiele interfejsów.
public class Programista extends Pracownik {
private String jezykGlowny; // POLE WŁASNE klasy pochodnej
public Programista(String imie, double pensja, String jezykGlowny) {
super(imie, pensja); // WYWOŁANIE KONSTRUKTORA KLASY BAZOWEJ
this.jezykGlowny = jezykGlowny; // musi być PIERWSZĄ instrukcją
}
public String getJezykGlowny() {
return jezykGlowny;
}
public void przeglądKodu() { // METODA WŁASNA - Pracownik jej nie ma
System.out.println("Recenzuję kod w " + jezykGlowny);
}
}
public class Menedzer extends Pracownik {
private int liczbaPodwladnych;
public Menedzer(String imie, double pensja, int liczbaPodwladnych) {
super(imie, pensja);
this.liczbaPodwladnych = liczbaPodwladnych;
}
public int getLiczbaPodwladnych() { return liczbaPodwladnych; }
}
- Konstruktory nie są dziedziczone. Klasa pochodna musi mieć własny konstruktor, choćby tylko po to, by wywołać konstruktor bazowy.
- Kolejność jest odwrotna do intuicji: najpierw wykonuje się konstruktor klasy bazowej, potem pochodnej. Destruktory (w C++) wywoływane są w kolejności odwrotnej — najpierw pochodnej, potem bazowej.
- Java:
super(argumenty)musi być pierwszą instrukcją konstruktora. Jeśli go pominiesz, kompilator wstawia bezparametrowesuper()— i jeśli klasa bazowa nie ma konstruktora bezparametrowego, dostajesz błąd kompilacji. - C++: konstruktor bazowy wywołujesz na liście inicjalizacyjnej, po dwukropku:
Programista(...) : Pracownik(imie, pensja) { ... } - Co się dziedziczy: pola i metody public oraz protected. Składowe private istnieją w obiekcie klasy pochodnej, ale są dla niej niedostępne wprost — sięgasz po nie odziedziczonym getterem. Dziedziczenie blokujesz słowem final (Java, C++) albo sealed (C#).
6
Polimorfizm: przesłanianie kontra przeciążanie
Polimorfizm (wielopostaciowość) to zdolność jednego wywołania do wykonania
różnego kodu w zależności od tego, jakiego typu obiekt faktycznie kryje się
pod referencją. Piszesz
p.obliczWynagrodzenie(),
a wykona się wersja z klasy Programista albo Menedzer — mimo
że zmienna jest typu Pracownik.
// KLASA BAZOWA - wersja domyślna metody
public class Pracownik {
protected String imie;
protected double pensjaPodstawowa;
public double obliczWynagrodzenie() {
return pensjaPodstawowa;
}
}
// PRZESŁONIĘCIE (override): ta sama nazwa, te same parametry, inna treść
public class Programista extends Pracownik {
private int liczbaNadgodzin;
@Override
public double obliczWynagrodzenie() {
return super.obliczWynagrodzenie() + liczbaNadgodzin * 80.0;
}
}
public class Menedzer extends Pracownik {
private int liczbaPodwladnych;
@Override
public double obliczWynagrodzenie() {
return pensjaPodstawowa * 1.2 + liczbaPodwladnych * 150.0;
}
}
// POLIMORFIZM W AKCJI - jedna tablica, jedna pętla, trzy różne wyniki
Pracownik[] zespol = {
new Pracownik("Anna", 5000),
new Programista("Jan", 6000, 10),
new Menedzer("Ewa", 8000, 4)
};
double kosztFirmy = 0;
for (Pracownik p : zespol) {
System.out.println(p.imie + ": " + p.obliczWynagrodzenie());
kosztFirmy += p.obliczWynagrodzenie();
}
// Anna: 5000.0 -> wersja z klasy Pracownik
// Jan: 6800.0 -> 6000 + 10 * 80, wersja z klasy Programista
// Ewa: 10200.0 -> 8000 * 1.2 + 4 * 150, wersja z klasy Menedzer
// kosztFirmy = 22000.0
// Kluczowe: typ ZMIENNEJ to Pracownik, ale wykonuje się metoda
// z klasy OBIEKTU. To jest późne wiązanie (dynamiczne, w czasie wykonania).
| Cecha | Przeciążanie (overloading) | Przesłanianie (overriding) |
|---|---|---|
| Gdzie występuje | w tej samej klasie (może też w pochodnej) | wyłącznie w klasie pochodnej |
| Nazwa metody | taka sama | taka sama |
| Lista parametrów | musi się różnić (liczba lub typy) | musi być identyczna |
| Typ zwracany | dowolny, sam nie wystarczy do rozróżnienia | taki sam (lub typ pochodny) |
| Kiedy rozstrzygane | na etapie kompilacji (wiązanie statyczne, wczesne) | w czasie wykonania (wiązanie dynamiczne, późne) |
| Nazwa zjawiska | polimorfizm statyczny | polimorfizm dynamiczny (właściwy) |
| Wymóg w C++ | brak | metoda bazowa musi być virtual |
// PRZECIĄŻANIE - trzy metody o tej samej nazwie, różne parametry.
// Kompilator wybiera wersję po TYPACH ARGUMENTÓW, już podczas kompilacji.
public class Kalkulator {
public int dodaj(int a, int b) { return a + b; }
public double dodaj(double a, double b) { return a + b; }
public int dodaj(int a, int b, int c) { return a + b + c; }
// BŁĄD KOMPILACJI - sam typ zwracany nie odróżnia przeciążeń:
// public double dodaj(int a, int b) { return a + b; }
}
Kalkulator k = new Kalkulator();
k.dodaj(2, 3); // 5 -> wersja (int, int)
k.dodaj(2.5, 3.5); // 6.0 -> wersja (double, double)
k.dodaj(1, 2, 3); // 6 -> wersja (int, int, int)
// C++ - BEZ virtual NIE MA POLIMORFIZMU. Klasyczna pułapka.
class Pracownik {
protected:
double pensja;
public:
Pracownik(double p) : pensja(p) {}
virtual double obliczWynagrodzenie() { return pensja; } // METODA WIRTUALNA
virtual ~Pracownik() {} // destruktor bazowy TEŻ wirtualny
};
class Menedzer : public Pracownik {
public:
Menedzer(double p) : Pracownik(p) {}
double obliczWynagrodzenie() override { return pensja * 1.2; }
};
Pracownik* p = new Menedzer(8000);
std::cout << p->obliczWynagrodzenie(); // 9600 - bo metoda jest virtual
// bez virtual byłoby 8000 (wiązanie statyczne)
delete p; // wirtualny destruktor -> sprząta CAŁY obiekt
Java kontra C++ w jednym zdaniu
W Javie każda metoda niestatyczna jest domyślnie wirtualna,
więc przesłanianie działa samo z siebie (adnotacja
@Override to
tylko zabezpieczenie dla kompilatora, nie warunek działania). W C++ metoda
jest wirtualna wyłącznie wtedy, gdy oznaczysz ją słowem
virtual —
inaczej dostajesz przesłonięcie statyczne i wywoła się wersja z klasy bazowej.
W C# potrzebna jest para virtual w bazie i override
w pochodnej.
7
Abstrakcja: klasa abstrakcyjna vs interfejs
Abstrakcja to pokazywanie tylko tego, co istotne, i ukrywanie szczegółów realizacji. W praktyce sprowadza się do dwóch narzędzi: klasy abstrakcyjnej i interfejsu. Oba mówią „każdy, kto to rozszerza, musi mieć taką metodę", ale robią to inaczej i egzamin lubi pytać właśnie o różnicę.
// KLASA ABSTRAKCYJNA - nie da się z niej utworzyć obiektu
public abstract class Pracownik {
protected String imie;
protected double pensjaPodstawowa; // POLA - klasa abstrakcyjna je ma
public Pracownik(String imie, double pensja) { // KONSTRUKTOR - też ma
this.imie = imie;
this.pensjaPodstawowa = pensja;
}
// METODA ABSTRAKCYJNA - sama deklaracja, bez ciała, zakończona średnikiem.
// Każda klasa pochodna MUSI ją zaimplementować.
public abstract double obliczWynagrodzenie();
// METODA ZWYKŁA - z gotową implementacją, dziedziczona bez zmian
public String opis() {
return imie + ": " + obliczWynagrodzenie() + " zl";
}
}
// Pracownik p = new Pracownik("Anna", 5000); // BŁĄD KOMPILACJI!
Pracownik p = new Programista("Jan", 6000, 10); // OK - obiekt klasy konkretnej
// INTERFEJS - kontrakt, czysty zbiór metod bez stanu
public interface Premiowalny {
double STAWKA_PREMII = 0.10; // pola tylko public static final (stałe)
double obliczPremie(); // metody domyślnie public abstract
}
public interface Szkolacy {
void przeprowadzSzkolenie(String temat);
}
// JEDNA klasa bazowa + DOWOLNIE WIELE interfejsów
public class Menedzer extends Pracownik implements Premiowalny, Szkolacy {
private int liczbaPodwladnych;
public Menedzer(String imie, double pensja, int podwladni) {
super(imie, pensja);
this.liczbaPodwladnych = podwladni;
}
@Override
public double obliczWynagrodzenie() { // z klasy abstrakcyjnej
return pensjaPodstawowa * 1.2 + obliczPremie();
}
@Override
public double obliczPremie() { // z interfejsu Premiowalny
return pensjaPodstawowa * STAWKA_PREMII * liczbaPodwladnych;
}
@Override
public void przeprowadzSzkolenie(String temat) { // z interfejsu Szkolacy
System.out.println(imie + " prowadzi szkolenie: " + temat);
}
}
| Cecha | Klasa abstrakcyjna | Interfejs |
|---|---|---|
| Tworzenie obiektu | niemożliwe | niemożliwe |
| Pola z danymi (stan obiektu) | TAK, dowolne, także prywatne | NIE — tylko stałe public static final |
| Metody z implementacją | TAK, dowolnie wiele | tylko domyślne i statyczne (Java 8+); klasycznie nie |
| Konstruktor | TAK — wywoływany przez klasę pochodną | NIE |
| Modyfikatory dostępu składowych | dowolne | metody zawsze publiczne |
| Ile można mieć w Javie | jedną (extends) | dowolnie wiele (implements) |
| Sens użycia | wspólna baza dla spokrewnionych klas — relacja „jest rodzajem" | wspólna umiejętność klas niespokrewnionych — relacja „potrafi" |
W C++ nie ma słowa kluczowego interface. Rolę interfejsu pełni klasa abstrakcyjna z samymi metodami czysto wirtualnymi, zapisywanymi jako virtual void metoda() = 0; — taki zapis nazywa się metodą czysto wirtualną, a klasa, która ją zawiera, jest abstrakcyjna.
8
Relacje między klasami, paradygmat funkcyjny i zdarzeniowy
Dziedziczenie to nie jedyny sposób, w jaki klasy mogą być ze sobą powiązane. Na diagramach klas UML pojawiają się trzy relacje „ma": asocjacja, agregacja i kompozycja. Różnią się siłą powiązania i tym, czy części przeżyją całość.
Najluźniejsze powiązanie: obiekty o sobie wiedzą i się komunikują, ale żaden nie jest częścią drugiego. Pracownik korzysta z Drukarki. Na diagramie UML: zwykła linia.
Całość zawiera części, ale części istnieją niezależnie. Zespół ma Pracowników — rozwiązanie zespołu nie zwalnia pracowników. W kodzie: obiekt przekazany z zewnątrz do konstruktora. UML: pusty romb.
Silne powiązanie: część nie istnieje bez całości i ginie razem z nią. Pracownik ma UmoweOPrace. W kodzie: obiekt tworzony wewnątrz konstruktora całości. UML: wypełniony romb.
// KOMPOZYCJA - Umowa powstaje WEWNĄTRZ Pracownika i ginie razem z nim
public class Pracownik {
private Umowa umowa;
public Pracownik(String rodzajUmowy, double stawka) {
this.umowa = new Umowa(rodzajUmowy, stawka); // tworzone w środku
}
}
// AGREGACJA - Zespół dostaje gotowych pracowników z zewnątrz;
// po usunięciu zespołu obiekty Pracownik nadal istnieją
public class Zespol {
private List<Pracownik> czlonkowie;
public Zespol(List<Pracownik> czlonkowie) {
this.czlonkowie = czlonkowie; // przekazane z zewnątrz
}
}
// ASOCJACJA - obiekt tylko chwilowo korzysta z innego obiektu
public class Programista extends Pracownik {
public void wypisz(Drukarka d, String dokument) {
d.drukuj(dokument); // zna Drukarkę, ale jej nie posiada
}
}
Dziedziczenie wiąże klasy na sztywno: zmiana w klasie bazowej rozlewa się na
wszystkie pochodne. Jeśli zaczynasz mnożyć klasy typu
ProgramistaZdalnyNaPolEtatu, to znak, że próbujesz dziedziczeniem
opisać kombinację cech — a to zadanie dla kompozycji.
Reguła decyzyjna: jeśli zdanie „X jest rodzajem Y"
jest prawdziwe i pozostanie prawdziwe zawsze — dziedziczenie. Jeśli
naturalniej brzmi „X ma Y" albo „X używa Y" — kompozycja.
Zaletą kompozycji jest to, że zachowanie można podmienić w czasie działania
programu: wystarczy podstawić inny obiekt składowy, zamiast tworzyć nową
klasę.
- Program to złożenie funkcji, a nie ciąg zmian stanu.
- Brak efektów ubocznych — funkcja czysta nie zmienia niczego poza swoim wynikiem i dla tych samych argumentów zawsze zwraca to samo.
- Niezmienność danych — zamiast modyfikować listę, tworzysz nową.
- Funkcje wyższego rzędu — funkcja przyjmuje inną funkcję jako argument lub ją zwraca. Stąd wyrażenia lambda.
- Zamiast pętli: map (przekształć), filter (odsiej), reduce (zwiń do jednej wartości).
- Języki: Haskell, Lisp, Erlang; elementy w Javie, C#, JavaScript i Pythonie.
- O przebiegu programu decyduje zdarzenie, a nie kolejność instrukcji w kodzie.
- Program czeka w pętli zdarzeń na kliknięcie, wpisanie tekstu, upływ czasu, nadejście danych z sieci.
- Reakcję opisujesz w procedurze obsługi zdarzenia (handler, słuchacz, listener), którą rejestrujesz przy komponencie.
- Podstawa każdego interfejsu graficznego (okna, przyciski) i aplikacji webowych po stronie przeglądarki.
- Nie wyklucza obiektowości — w praktyce zdarzeniowe GUI pisze się obiektowo.
// STYL FUNKCYJNY w Javie - lambda + strumień, zero pętli i zmiennych pomocniczych
List<Pracownik> zespol = List.of(
new Programista("Jan", 6000, 10),
new Menedzer("Ewa", 8000, 4));
double kosztFirmy = zespol.stream()
.mapToDouble(Pracownik::obliczWynagrodzenie) // map
.sum(); // reduce
List<String> dobrzeOplacani = zespol.stream()
.filter(p -> p.obliczWynagrodzenie() > 7000) // filter
.map(Pracownik::getImie)
.toList();
// STYL ZDARZENIOWY - rejestruję procedurę obsługi, nie wywołuję jej sam
przyciskZapisz.addActionListener(e -> {
kadry.zapisz(zespol);
System.out.println("Zapisano " + zespol.size() + " pracownikow");
});
// Kod w nawiasach wykona się dopiero wtedy, gdy użytkownik kliknie przycisk.
- Mylenie przeciążania z przesłanianiem. Przeciążanie: ta sama nazwa, inne parametry, rozstrzygane przy kompilacji. Przesłanianie: ta sama nazwa i identyczne parametry, w klasie pochodnej, rozstrzygane w czasie wykonania. Sam inny typ zwracany nigdy nie tworzy przeciążenia.
- Twierdzenie, że private jest widoczne w klasie pochodnej. Nie jest. Kombinację „widoczne w pochodnej, niewidoczne na zewnątrz" daje wyłącznie protected.
- Zapominanie o virtual w C++. Bez virtual wywołanie przez wskaźnik do klasy bazowej uruchomi metodę bazową — polimorfizmu nie będzie. W Javie problem nie istnieje, bo metody są wirtualne domyślnie.
- Wielokrotne dziedziczenie klas w Javie oraz „interfejs z konstruktorem". Java dopuszcza jedną klasę bazową (wielokrotne dziedziczenie klas mają C++ i Python), za to dowolnie wiele interfejsów. Interfejs nie ma konstruktora ani pól z danymi — tylko stałe; konstruktor i pola ma klasa abstrakcyjna.
- Pomijanie super() w konstruktorze klasy pochodnej. Jeśli klasa bazowa nie ma konstruktora bezparametrowego, brak jawnego super(...) to błąd kompilacji. Kolejność wykonania: najpierw konstruktor bazowy, potem pochodnej.
- Nazywanie kompozycji dziedziczeniem. „Samochód ma silnik" to kompozycja. Dziedziczenie stosujesz wyłącznie przy relacji „jest rodzajem".
Paradygmaty wchodzą do głowy dopiero wtedy, gdy sam odróżnisz przeciążenie od przesłonięcia w cudzym kodzie i bez wahania wskażesz właściwy modyfikator dostępu. Rozwiąż test z tego działu i zobacz, gdzie jeszcze masz luki.