Przejdź do treści
INF.04 · Dział II

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.

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.

Imperatywny — JAK

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.

Deklaratywny — CO

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).

To samo zadanie w dwóch stylach

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.

1. Sekwencja

Instrukcje wykonywane jedna po drugiej, w kolejności zapisu. Na schemacie blokowym: prostokąty połączone pionową strzałką, bez rozgałęzień.

2. Wybór (selekcja)

Rozgałęzienie zależne od warunku: if, if-else, switch. Na schemacie: romb z dwoma wyjściami TAK/NIE.

3. Iteracja

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);
Zasada jednego wejścia i jednego wyjścia

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 goto jest w programowaniu strukturalnym zakazana.
  • Sporne, ale dopuszczalne: break, continue i wiele instrukcji return to 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.

Klasa

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.

Obiekt (instancja)

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, destruktor i słowo this
  • 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++ this jest 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.

Modyfikatory dostępu — tabela do zapamiętania
ModyfikatorWewnątrz klasyW klasie pochodnejNa zewnątrzKiedy stosować
publicTAKTAKTAKmetody będące interfejsem klasy (gettery, settery, operacje)
protectedTAKTAKNIEskładowe, które ma widzieć klasa pochodna, ale nie użytkownik klasy
privateTAKNIENIEdomyślny wybór dla wszystkich pól
(brak) packageTAKtylko w tym samym pakiecietylko w tym samym pakieciespecyfika 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
Typowe polecenie egzaminacyjne

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.

Nazewnictwo
  • 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 vs wielokrotne

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; }
}
Konstruktor klasy bazowej — reguły, o które pyta egzamin
  • 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 bezparametrowe super() — 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).
Przeciążanie vs przesłanianie — najczęściej mylona para w dziale
CechaPrzeciążanie (overloading)Przesłanianie (overriding)
Gdzie występujew tej samej klasie (może też w pochodnej)wyłącznie w klasie pochodnej
Nazwa metodytaka samataka sama
Lista parametrówmusi się różnić (liczba lub typy)musi być identyczna
Typ zwracanydowolny, sam nie wystarczy do rozróżnieniataki sam (lub typ pochodny)
Kiedy rozstrzyganena etapie kompilacji (wiązanie statyczne, wczesne)w czasie wykonania (wiązanie dynamiczne, późne)
Nazwa zjawiskapolimorfizm statycznypolimorfizm dynamiczny (właściwy)
Wymóg w C++brakmetoda 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);
  }
}
Klasa abstrakcyjna kontra interfejs — tabela egzaminacyjna
CechaKlasa abstrakcyjnaInterfejs
Tworzenie obiektuniemożliweniemożliwe
Pola z danymi (stan obiektu)TAK, dowolne, także prywatneNIE — tylko stałe public static final
Metody z implementacjąTAK, dowolnie wieletylko domyślne i statyczne (Java 8+); klasycznie nie
KonstruktorTAK — wywoływany przez klasę pochodnąNIE
Modyfikatory dostępu składowychdowolnemetody zawsze publiczne
Ile można mieć w Javiejedną (extends)dowolnie wiele (implements)
Sens użyciawspó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ść.

Asocjacja — „zna"

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.

Agregacja — „ma luźno"

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.

Kompozycja — „ma na własność"

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
  }
}
Zasada „kompozycja ponad dziedziczenie"

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ę.

Programowanie funkcyjne
  • 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.
Programowanie zdarzeniowe
  • 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.
Najczęstsze pułapki egzaminacyjne
  • 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".
Sprawdź, ile z tego zostało w głowie

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.