Kihagyás

11 - Objektumelvű programozási nyelvek

Osztály és objektum

Objektum: Objektumnak egy feladat megoldásának azon önálló egyedként azonosított részét nevezzük, amely magába foglalja az adott részért felelős adatokat, és az ezekkel kapcsolatos műveleteket.

Miután létrehoztunk egy objektumot (tehát példányosítottuk) - csak közvetett módon, egy változó segítségével tudunk majd hivatkozni, ez az objektum változó, amely annak a memória-területnek a címét tartalmazza, amely a program futása során (az objektum példányosításakor) jön létre, abból a célból, hogy ott az objektum adattagjai eltárolhassuk.

Osztály: Az osztály egy objektum szerkezetének és viselkedésének a mintáját adja meg.

Tehát, felsorolja:

  • Objektum adattagjait, az alábbiakkal kiegészítve:
    • név
    • típus
    • láthatóság
    • esetlegesen típusinvariáns
  • Objektumra meghívvató metódusokat:
    • Név
    • Paraméterlista
    • Visszatérési érték típusa
    • Törzs
    • Láthatóság

Az osztály, lényegében az objektum típusa, ez alapján hozzunk létre az objektumokat. Egy osztályho több objektum is példányosítható.

Egységbe zárás, tagok, konstruktorok

Konstruktorok: Az objektum példányosítását (létrehozását) speciális metódus, a konstruktor végzi: memóriát foglal az objektum adattagjai számára (ha egy adattag maga is objektum-hivatkozás, akkor azt is példányosítja), és kezdeti értéket ad az adattagoknak.

Ha mást konstruktort nem definiálunk, akkor is rendelkezünk egy (paraméter nélküli és üres törzsű) ún. üres konstruktorral.

Egységbezárás: egy adott feladatkör megvalósításához szükséges adatokat és az azokat manipuláló programrészeket a program többi részétől elkülönítve adhatjuk meg. Ez az egység az objektum.

Tagok: Egy osztályt felépítő változók és metódusok (függvények) összessége. Ezek határozzák meg, hogy az adott osztályból létrehozott objektumok milyen tulajdonságokkal rendelkeznek és milyen műveleteket képesek végrehajtani.

Információ elrejtése

Az egységbe zárt elemek láthatóságának korlátozása. Egy objektum adattagjainak közvetlen elérését általában nem támogatjuk, azok értékéhez csak közvetve, publikus metódusokkal férünk hozzá.

Túlterhelés

Ugyan azzal a névvel, ugyan azzal az osztályban több függvényt is definiálunk. A függvényeket az úgynevezett szignatúra különbözteti meg egymástól. Egy függvény szignatúrája: A név *és a paraméterei.

Például:

public class Rational {
    ...
    public void multiplyWith( Rational that) {
        this.numerator *= that.numerator;
        this.denominator *= that.denominator;
    }
    public void multiplyWith(int that) {
        this.numerator *= that;
    }
}

Itt, a Rational osztály multiplyWith függvényét túlterheltük (overload-oltuk).

"A túlterhelés úgy működik, hogy a túlterhelt metódusok valamelyikének meghívásánál használt aktuális paraméterek fordító által ismert típusából dönti el, a túlterhelt metódusok változatából melyiket kell meghívni." (Kozsik Tamás, 2023)

Vannak, edge-case-ek. pl.

static void m(long n) { ... }
static void m (float n) { ... }
public static void main(String[] args) {
    m(3);
}

Itt 3 egy int. Akkor lenne long ha 3l lenne vagy float ha 3f.

Memóriakezelés, szemétgyűjtés

Memóriakezelés: Referencia:

  • Osztály típusú változó
  • Objektumra hivatkozik
  • Heapen tárolódik
  • Létrehozás (Java): new kulcsszó
  • Dereferálás (Java): .

Referencia típusok pl.: Osztályok, Tömb típusok

Ábrázolás a Memóriában, Stack: Fontos, hogy hogyan deklaráltuk az adott adatot. Ha az adatot deklarációval hoztam létre egy metódusban (pl. formális param, vagy lokális változó) akkor az az adat Stack-en fog tárolódni a metódus aktivációs rekordja részeként. Ez lehet, primitív érték, vagy lehet egy referencia, amire hivatkozik az a Heap-en van, de maga a referencia a Stack-en van.

Ábrázolás a Memóriában, Heap: Amit pl. Java-ban a new-al hoztunk létre (dinamikus élettartamú) objektumok. Az objektumokban lehetnek adattagok, amik persze benne vannak az objektumban, ezek lehetnek primitívek és referenciák, ezek nyílvánvalóan a Heap-en lesznek. Tehát ami az objektumnak a része, az a Heap-en van.

Szemétgyűtjés:

Helyes: A feleslegessé vált objektumok felszabadítása. Csak olyat szabadít fel, amit már nem lehet elérni a programból.

Teljes: Mindent felszabadít, amit nem lehet már elérni.

Nem felszabadítható állapot


Felszabadítható állapot

Mark-and-Sweep szemétgyűjtés:

  • Mark fázis:
    • Kiindulunk a vermen lévő referenciákból
    • Megjelöljük a belőlük elérhető objektumokat
      • Megjelöljük a megjelöltekből elérhető objektumokat
      • ... amíg tudunk újabbat megjelölni (tranzitív lezárt)
  • Sweep Fázis:
    • A jelötletlen objektumok felszabadíthatók

Öröklődés, többszörös öröklődés

Öröklődés:

class A extends B { ... }
  • Egy típust egy másik típusból származtatunk

    • Csak a különbséget kell megadni: A Δ B
    • Újrafelhasználás
  • Itt: az A a gyermekosztálya a B szülőosztálynak

    • child class
    • parent class
  • Tranzitvitiás: leszármazott osztály - ősosztály
    • alosztály: subclass, derived class
    • bázisosztály: super class, base class
  • Körkörösség kizárva!

Töbszörös öröklődés: A többszörös öröklődés során egy osztály közvetlenül több szülőosztályból is származhat, így azoknak a funkcionalitását egyesíti.

A Java nyelv nem támogatja a többszörös öröklődést. Ennek oka az úgynevezett Gyémánt probléma.

     A
    / \
   B   C
    \ /
     D

Ha az A osztálynak van egy metodus() nevű függvénye, és a B, valamint a C osztály is felülírja azt, akkor a D osztálynak melyik metodus()-t kellene örökölnie? A B-ét vagy a C-ét?

Megoldás: Interfészek.

Vagy Python esetén: MRO (Method Resolution Order), C3 linearizációs algoritmus. A gyakorlatban ez azt jelenti, hogy a Python először a saját osztályban keresi a metódust. Ha nem találja, akkor a szülőosztályokban folytatja a keresést, az öröklődésnél megadott sorrendben.

Altípusosság

Az öröklődés egyfajta 'következménye'.

A Δ B => A <: B "Ha az A osztály örököl a B osztályból akkor az A osztály altípusa lesz a B osztálynak" - Legtöbb nyelven így van.

pl.:

public class ExactTime extends Time { ... }
  • Az ExactTime mindent tud, amit a Time
  • Amit lehet Time-mal, lehet ExactTime-mal is
  • ExactTime <: Time

Azaz az ExactTime altípusa a Time-nak.

Itt említsük meg a Lisfok-féle helyettesítési elv-et. (LSP: Liskov's Substitution Principle) Egy A típus altípusa a B (bázis-)típusna, ha az A egyedeit használhatjuk a B egyedei helyett, anélkül, hogy ebből baj lenne.

(ha pl. egy ExactTime objektumot Time-ként kezelünk akkor azt felfelé konvertáljuk, vagy "upcast"-oljuk.)

Statikus és dínamikus típus, típusellenőrzés

Statikus típus: Változó vagy paraméter deklarált típusa.

  • A programszövegből következik
  • Állandó
  • A fordítóprogram ez alapján ellenőríz. pl.:
Time time

Dinamikus típus: Változó vagy paraméter tényleges típusa

  • Futási időben derül ki
  • Változékony
  • A statikus típus altípusa pl.:
time = ... ? new ExactTime(): new Time()

Dinamikus típusellenőrzés:

  • Automatikus (upcast) - altípusosság
  • Explicit (downcast) - type-cast operátor

Típusellenőrzés

Típuskényszerítés (downcast)

  • A "(Time)o" kifejezés statikus típusa Time.
  • Ha o dinamikus típusa Time:
Object o = new Time(3,20);
o.aMinutePassed(); // fordítási hiba
((Time)o).aMinutePassed(); // lefordul, működik

Tehát:

  • Futás közbem, dínamikus típus alapján
  • Pontosabb, mint a statikus típus
    • Altípus lehet
  • Rugalmasság
  • Biztonság: csak ha explicit kérjük (type cast)

Felüldefiniálás, dinamikus kötés

Felüldefiniálás:

  • Bázisosztályban adott műveletre
  • Ugyanazzal a névvel és paraméterezéssel
    • Ugyanaz a metódus
    • Egy példánymetódusnak lehet több implementációja
  • Futás közben választódik ki a "legspeciálisabb" implementáció

Dinamikus kötés Példánymetódus hívásánál a használt kitüntetett paraméter dinamikus típusához legjobban illeszkedő implementáció hajtódik végre.

pl.:

ExactTime e = new ExactTIme();
Time t = e;
Object o = t;

System.out.println( e.toString() ); // 0:00:00
System.out.println( t.toString() ); // 0:00:00
System.out.println( o.toString() ); // 0:00:00

Generikusok

A generikusok (Generics) lehetővé teszik típus-paraméterezett osztályok, interfészek és metódusok létrehozását. A fő céljuk a kód típusbiztonságának növelése és az újrafelhasználhatóság javítása.

Legfontosabb előnyök

  1. Fordítási idejű típus-ellenőrzés: A hibák (pl. rossz típus hozzáadása egy listához) már a fordításkor kiderülnek, nem futás közben. Ezzel elkerülhető a ClassCastException.
  2. Nincs szükség explicit kasztolásra: A fordító ismeri a gyűjtemény típusát, így a kiolvasott elemeket nem kell manuálisan konvertálni.

    // Generikusokkal a kasztolás (String) felesleges
    List<String> nevek = new ArrayList<>();
    nevek.add("Péter");
    String nev = nevek.get(0); // Nincs szükség (String) cast-ra
    

Alapvető szintaxis

A típusparamétert (gyakran T, mint "Type") a kisebb-nagyobb jelek (<>) között adjuk meg.

Generikus osztály példa:

public class Doboz<T> {
    private T tartalom;

    public void set(T tartalom) { this.tartalom = tartalom; }
    public T get() { return tartalom; }
}

// Használata:
Doboz<Integer> szamDoboz = new Doboz<>();
szamDoboz.set(123);

Altípusos és parametrikus polimorfizmus

Parametrikus polimorfizmus

  • Több típusra is működik ugyanaz a kód
    • Haskell: függvény
    • Java. típus (osztály), metódus
  • Típussal paraméterezhető kód
    • Haskell: Bármilyen típussal
    • Java: referenciatípusokkal

Altípusos polimorfzmus

Ha egy kódbázist megírtunk, újrahasznosíthatjuk speciális típusokra.

  • Általánosabb típusok helyett használhatunk altípusokat
  • Több típusra is működik a kódbázis: polimorfizmus. Újrafelhasználhatóság!

  • Az altípus "mindent tud", amit a bázistípus

  • Az altípus speciálisabb lehet
  • Ez az is-egy reláció
    • Car is-a Vehicle
    • Boat is-a Vehicle
  • Emberi gondolkodás, OO modellezés

Példa (Thanks to Gemini 2.5 Pro PREVIEW):

// Bázistípus (szupertípus)
class Vehicle {
    public void move() {
        System.out.println("A jármű halad.");
    }
}

// Altípus 1: Car. A Car "is-a" (egyfajta) Vehicle.
class Car extends Vehicle {
    @Override
    public void move() {
        System.out.println("Az autó gurul az úton.");
    }
}

// Altípus 2: Boat. A Boat "is-a" (egyfajta) Vehicle.
class Boat extends Vehicle {
    @Override
    public void move() {
        System.out.println("A hajó úszik a vízen.");
    }
}

public class Main {
    // Ez egy általános metódus, ami a BÁZISTÍPUSRA (Vehicle) van megírva.
    // Ez a "kódbázis", amit újrahasznosítunk.
    public static void startMoving(Vehicle v) {
        System.out.print("Indulás... ");
        // Nem tudjuk, milyen jármű jön, de tudjuk, hogy van neki move() metódusa.
        v.move();
    }

    public static void main(String[] args) {
        // Létrehozunk konkrét ALTÍPUSOKAT.
        Car myCar = new Car();
        Boat myBoat = new Boat();

        System.out.println("--- Az altípusos polimorfizmus működés közben ---");

        // Meghívjuk ugyanazt a startMoving metódust,
        // de egyszer egy Car, másszor egy Boat ALTÍPUSSAL.
        // A kód mégis működik, mert mindkettő "is-a" Vehicle.

        startMoving(myCar);  // Itt a 'v' paraméter egy Car objektum lesz.
        startMoving(myBoat); // Itt a 'v' paraméter egy Boat objektum lesz.
    }
}

Objektumok Összehasonlítása és Másolása

Összehasonlítás

  • == operátor: Referenciákat (memóriacímeket) hasonlít össze. Azt vizsgálja, hogy a két változó ugyanarra az objektumra mutat-e.
  • .equals() metódus: Tartalmi egyenlőséget vizsgál. Saját osztályok esetén ezt a metódust felül kell írni, hogy az objektumok mezői alapján hasonlítson össze.

(Ez Java-ra vonatkozik, specifikusan.)

Másolás

  • Sekély másolás (Shallow Copy): Létrehoz egy új objektumot, de a benne lévő objektumokra mutató referenciákat egyszerűen átmásolja. Így az eredeti és a másolt objektum ugyanazokon a belső objektumokon osztozik.
  • Mély másolás (Deep Copy): Teljesen független másolatot készít. Nemcsak a külső objektumot, hanem az összes általa hivatkozott objektumot is rekurzívan lemásolja.