| Java SE 8 DocumentationFeladatok
 Osztályok származtatása és kiterjesztéseAmikor osztályokat definiálunk, könnyen kerülhetünk olyan helyzetbe, amikor tulajdonképpen már sokadjára ugyanazokat a metódusokat vagy adattagokat írjuk le, csak éppen egy újabb osztályban. Tekintsük például azt, amikor készítünk egy köröket ábrázoló objektumtípust: // Ez a korábbi implementációnak egy rövidített változata.
class Circle {
    private Point2D center;
    private double radius;
    private double area;
    private double circumference;
    public static Circle make(double x, double y, double radius) {
        return ((radius > 0) ? new Circle(x, cy, radius) : null);
    }
    private Circle(double x, double y, double radius) {
        center = new Point2D();
        center.x = x;
        center.y = y;
        this.radius = radius;
        derive();
    }
    public double getRadius() {
        return radius;
    }
    public void setRadius(double radius) {
        if (radius > 0) {
            this.radius = radius;
            derive();
        }
    }
    private void derive() {
        area          = radius * radius * Math.PI;
        circumference = 2 * radius * Math.PI;
    }
    public double getArea() {
        return area;
    }
    public double getCircumference() {
        return circumference;
    }
    public String toString() {
        return String.format("Circle { center = %s, radius = %.3f }",
          center.show(), radius);
    }
}
Később aztán szeretnénk készíteni egy négyzeteket ábrázoló objektumtípust: class Square {
    private Point2D center;
    private double side;
    private double area;
    private double circumference;
    public static Square make(double x, double y, double side) {
        return ((side > 0) ? new Square(x, y, side) : null);
    }
    private Square(double x, double y, double side) {
        center = new Point2D();
        center.x = x;
        center.y = y;
        this.side = side;
        derive();
    }
    public double getSide() {
        return side;
    }
    public void setSide(double radius) {
        if (side > 0) {
            this.side = side;
            derive();
        }
    }
    private void derive() {
        area          = side * side;
        circumference = 4 * side;
    }
    public double getArea() {
        return area;
    }
    public double getCircumference() {
        return circumference;
    }
    public String toString() {
        return String.format("Square { center = %s, side = %.3f }",
          center.show(), side);
    }
}
Észrevehetünk némi hasonlóságot az osztályok felépítésében és a metódusok viselkedésében, ezért célszerű lenne ezt kihasználnunk! Ezzel egy könnyebben kezelhető, karbantartható programot kapunk, valamint jobban tudjuk modularizálni a szerkezetét. Ezért emeljük ki a közös részeket egy közös ősbe, egy ún. ősosztályba. Ebből aztán a másik kettőt származtathatjuk, vagyis az őst megfelelő módon kiterjesztve visszakapjuk a korábbi implementációkat, csak éppen egy rövidebb programban. Ebben az esetben a közös őst a Shapeosztály lesz, amely általánosságban ír le alakzatokat. Ennek megfelelően csak az azokra jellemző általános metódusokat és adattagokat tartalmazza: class Shape {
    Point2D center;
    protected double area;
    protected double circumference;
    public Shape(double x, double y) {
        center = new Point2D();
        center.x = x;
        center.y = y;
    }
    public double getArea() {
        return area;
    }
    public double getCircumference() {
        return circumference;
    }
    public String toString() {
        return String.format("center = %s", center.show());
    }
}
Utána ennek specializációjaként hozhatjuk létre a korábbi osztályainkat. Ilyenkor az ősosztály metódusait és adattagjait is látjuk, ezért azokat már nem kell újra megadnunk. class Circle extends /* "is a" */ Shape {
    private double radius;
    public static Circle make(double x, double y, double radius) {
        return ((radius > 0) ? new Circle(x, y, radius) : null);
    }
    private Circle(double x, double y, double radius) {
        super(x, y);
        this.radius = radius;
        derive();
    }
    public double getRadius() {
        return radius;
    }
    public void setRadius(double radius) {
        if (radius > 0) {
            this.radius = radius;
            derive();
        }
    }
    private void derive() {
        super.area          = radius * radius * Math.PI;
        super.circumference = 2 * radius * Math.PI;
    }
    public String toString() {
        return String.format("Circle { %s, radius = %.3f }", super.toString(),
          radius);
    }
}
class Square extends /* "is a" */ Shape {
    private double side;
    public static Square make(double x, double y, double side) {
        return ((side > 0) ? new Square(x, y, side) : null);
    }
    private Square(double x, double y, double side) {
        super(x, y);
        this.side = side;
        derive();
    }
    public double getSide() {
        return side;
    }
    public void setSide(double side) {
        if (side > 0) {
            this.side = side;
            derive();
        }
    }
    private void derive() {
        super.area          = side * side;
        super.circumference = 4 * side;
    }
    public String toString() {
        return String.format("Square { %s, side = %.3f }", super.toString(),
          side);
    }
}
Az eredményül keletkező kódban azonban megfigyelhetjük, hogy a "tömörítés" során előkerült két új szimbólum is: a superésprotected. A supertulajdonképpen athistestvére, csak ezúttal nem az aktuális objektumra, vagy annak konstruktorára fog hivatkozni, hanem az ősre. A származtatás miatt ugyanis az objektumokhoz mindig létrejön a neki megfelelő ősobjektum, amelyet szükség esetén asuperreferencián keresztül tudunk elérni. Itt most arra használtuk, hogy meg tudjuk hívni az őshöz tartozó konstruktort, valamint annak atoString()metódusát. ACircleés aSquarekonstruktoraiban asuper()hívás lényegében kötelező is, mivel másképpen a rendszer nem fogja tudja inicializálni az őshöz tartozó adattagokat (leginkább acenterértéket). Asuper()hívással kapcsolatban még arra kell figyelnünk, hogy mindig a konstruktor első utasítása legyen. Ha az ős konstruktorának nincsenek paraméterei, akkor el is hagyhatjuk, mert ezt a fordító képes magától is beilleszteni. Ha azonban nem ez a helyzet, akkor mindenképpen ki kell írnunk. A másik eset, amikor a supermég előkerült, az a referencia. A fenti kódrészleteben asuper.toString()hívást használtuk, ami arra utal, hogy nem az aktuális típushoz tartozótoString()példányt akarjuk meghívni, hanem az ős eredeti metódusát, amelyet felüldefiniáltunk. Ennek segítségével tudjuk elérni, hogyShapea ponthoz tartozó részszöveg előállítsa, és azt mi csak felhasználjuk a leszármazattokban (tehát nem kell már újra megírni). Amit még megfigyelhetünk, hogy a korábbi privatemódosító helyett az osztott adattagok esetén aprotectedmódosítót használtuk. Ennek az a célja, hogy elrejtse kívülről az adattagokat, viszont lehetővé tegye ezek elérhetőségét a leszármazatottakban. Ha ugyanisprivatemaradna, akkor már a származtatott osztályokból sem tudnánk elérni ezeket. Erre most azért volt szükség, mert aCircleésSquareesetén közvetlenül módosítjuk azareaéscircumferenceadattagok értékeit. De ha például külön írtunk volna erre az ősben módosítófüggvényeket, akkor maradhattak volnaprivateértékek. A Java nyelvben megtalálható objektumoknak van egy közös őse, ez pedig a java.lang.Object. Mivel bármelyik objektumtípus implicit módon ebből az osztályból származik (tehát ha nem írjuk ki azextendsután ősét, akkor az automatikusan ez az osztály lesz), vagyis örökli annak a műveleteit. Ilyen műveletek például azequals()és atoString(). Viszont ezek, tekintettel arra, hogy ajava.lang.ObjectaShapeosztályunkhoz hasonlóan egy általánosítás, egy elég általános viselkedést társítanak az objektumhoz. Azequals()lényegében a referenciákat hasonlítja össze (mivel más információ nem áll rendelkezésre), atoString()pedig az objektum tárbeli címe és a konkrét típusa alapján állít elő egy szöveges alakot. class Dummy /* extends Object */ {}
A Dummyosztályunk tehát mivel ajava.lang.Objectosztályt terjeszti ki, eleve rendelkezni fog annak metódusaival. Így leszequals()éstoString()is hozzá. Dummy d1 = new Dummy();
Dummy d2 = new Dummy();
System.out.println(String.format("d1 = %s, d2 = %s", d1.toString(), d2.toString()));
System.out.println(d1.equals(d2));
Láthattuk viszont az eddigiekben, hogy a toString()metódust szükség szerint felül tudtuk definiálni a leszármazattokban. (A felüldefiniálásról még később részletesebb szó lesz.) Öröklődés helyett kompozícióA származtatásnak lesz egy azonban mellékhatása is, ugyanis a származtatás során az ősként funkcionáló osztály a leszármazottjainak általánosítása lesz, vagyis (az ún. Liskov-féle helyettesítési elv mentén) bárhova be tudjuk írni a leszármazottat, ahova az őst be tudtuk korábban írni (hiszen ismeri ugyanazokat a műveleteket). Ezért lehet akár ilyet is írni: Shape[] shapes = new Shape[] { Circle.make(0,0,1), Square.make(0,0,1) };
for (Shape s : shapes) {
    System.out.println(s.toString());
}
Ezért amikor különböző objektumtípusokat akarunk összekapcsolni a programjainkban, akkor nem biztos, hogy valójában az öröklődést kell használunk. Sőt, legtöbb esetben az öröklődést egyáltalán nem is kell használnunk. A kompozíció az objektumtípusok közti kapcsolat (asszociáció) egyik fajtája. Ebben az esetben arról van szó, hogy a két osztály között nem öröklődési kapcsolat van, hanem csupán az egyik osztály felhasználja a másik osztály a definíciójához, annak szerves része lesz, anélkül nem létezhet. Példaként tekintsük a Persontípus definícióját: class Person {
    private String  name;  /* String  "is part of" Person */
    private Integer age;   /* Integer "is part of" Person */
    public static Person make(String name, Integer age) {
        return ((age > 0 && !name.isEmpty()) ? new Person(name, age) : null);
    }
    private Person(String name, Integer age) {
        this.name = new String(name);
        this.age  = new Integer(age);
    }
    public String getName() {
        return new String(name);
    }
    public Integer getAge() {
        return new Integer(age);
    }
    public String toString() {
        return String.format("Person { name = %s, age = %d }", name, age);
    }
}
Itt a Personobjektum felhasználta a saját definíciójához aStringésIntegerobjektumokat. Ezek a komponensek mindig egyPersonértékhez tartoznak, önmagukban használjuk ezeket. Person jesus = Person.make("Jesus Christ", 32);
Tulajdonképpen az öröklődés kompozícióval ki is váltható. A Circleosztályunkat például meg tudjuk úgy is írni, hogy aShape, mint alkatrész szerepel benne. class Circle {
    private Shape shape;
    private double radius;
    public static Circle make(double x, double y, double radius) {
        return ((radius > 0) ? new Circle(x, y, radius) : null);
    }
    private Circle(double x, double y, double radius) {
        shape = new Shape(x, y);
        this.radius = radius;
        derive();
    }
    public double getRadius() {
        return radius;
    }
    public void setRadius(double radius) {
        if (radius > 0) {
            this.radius = radius;
            derive();
        }
    }
    private void derive() {
        shape.area          = radius * radius * Math.PI;
        shape.circumference = 2 * radius * Math.PI;
    }
    public String toString() {
        return String.format("Circle { %s, radius = %.3f }",
          shape.toString(), radius);
    }
}
Ekkor viszont a Circlenem használhatóShapeértékként. Öröklődés helyett agregációTermészetesen nem kötelező, hogy a kompozícióban felhasznált objektum létezése mindig függjön a befoglaló objektum létezésétől. Ezért alkalmazható az agregáció is, mint az objektumtípusok közti másik lehetséges kapcsolat. Ennek szemléltetéséhez maradjunk a Persondefiníciójánál, és egészítsük ki egy újabb adattaggal, amely egy személy barátait tárolja el! class Person {
    private String            name;    /* String  "is part of" Person */
    private Integer           age;     /* Integer "is part of" Person */
    private ArrayList<Person> friends; /* Person  "has" Persons */
    public static Person make(String name, Integer age) {
        return ((age > 0 && !name.isEmpty()) ? new Person(name, age) : null);
    }
    private Person(String name, Integer age) {
        this.name    = new String(name);
        this.age     = new Integer(age);
        this.friends = new ArrayList<Person>();
    }
    public String getName() {
        return new String(name);
    }
    public Integer getAge() {
        return new Integer(age);
    }
    public void addFriend(Person friend) {
        friends.add(friend);
    }
    public String toString() {
        return String.format("Person { name = %s, age = %d, friends = %s }",
          name, age, friends.toString());
    }
}
Látható, hogy a barátok a konkrét Persontípusú érték nélkül is léteznek. De ha szeretnénk, akkor kétPersonértéket így össze tudunk kapcsolni. Person stan = Person.make("Stanley Marsh", 11);
jesus.addFriend(stan);
System.out.println(stan.toString());
System.out.println(jesus.toString());
Feladatok
Valósítsunk meg egy utils.Listtípust, amely láncolt lista adatszerkezetben képes dinamikus növekvő méretű,java.lang.Objecttípusú értékeket tároló (agregáció) tömböket ábrázolni! A láncolás azt jelenti, hogy az új elemeket nem konkrétan egy tömb típusú értékben tároljuk el, hanem egymásra hivatkozó (rekurzív) listák sorozataként, amely a rákövetkező lista mellett mindig tárolja az aktuális elején található elemet is. Ha egy listának már rákövetkező listája, akkor ott anullreferencia szerepeljen! Ennek az ábrázolási módnak a felhasználásával implementáljuk erre a típusra az alábbi műveleteket: 
egy konstruktor, amely egy elemet kapva paraméterül, létrehozza az azt az elemet tartalmazó, egyelemű listát,add(): egy elem beillesztése a láncolt lista elejére,
concat(): a paraméterül kapott másik láncolt lista az aktuális lista után fűzése,
getFirst(): a listában található első elem lekérdezése,
getRest(): a lista fennmaradó részének (mintList) visszaadása, vagyis a lista az első elem nélkül,
length(): a lista hosszának (rekurzív) lekérdezése,
remove(): az első elem törlése,
toArray(): a listában tárolt értékek egyetlen tömbként történő visszaadása (rekurzívan),
toString(): (rekurzívan) szöveggé alakítás.
Készítsünk egy olyan programot, amely a szabványos bemenetről be tudja olvasni különböző fajta alakzatok adatait, majd azokat eltárolni! Mivel nem tudjuk előre pontosan, hogy mennyi alakzattal kapcsolatban akarunk majd adatokat rögzíteni, ezért célszerű azokat egy java.util.ArrayListtípusú objektumba tenni. A beolvasást addig folytatjuk, amíg a bemenet tart ("EOF, az "End of File" jelzést nem kapunk), vagy amíg nem kapunk egy "quit"üzenetet. A másik ilyen parancsunk legyen az "add", amely után zárójelek között megadjuk, hogy milyen alakzatot, milyen paraméterekkel akarunk felvenni. Az alakzataink rendre: 
"rectangle":Rectangletípusú objektum, vagyis egy téglalap létrehozása a bal felső pont x és y koordinátájával, valamint az x és y irányú oldalhosszok megadásával,
"square":Squaretípusú objektum, vagyis négyzet lébrehozása a bal felső pont x és y koordinátáival, valamint az oldalhossz megadásával,
"ellipse":Ellipsetípusú objektum, vagyis ellipszis létrehozása a középpont x és y koordinátáival, valamint a nagytengely és a kistengely megadásával,
"circle":Circletípusú objektum, vagyis kör létrehozása a középpont x és y koordinátánaik, valamint a sugár megadásával.
 Miután befejeztük a beolvasást, a szabványos kimeneten jelenítsük meg az összes beolvasott értéket (a saját toString()metódusán keresztül)!Készítsünk egy arrays.generic.MArraynevű osztály, amely tetszőleges dimenziójú tömböt képes ábrázolni egyetlen egydimenziós tömb segítségével! A tömbbenjava.lang.Objectértékeket tároljunk el (agregáció), így ennek köszönhetően lényegében bármilyen típusúak lehetnek. A műveletei legyenek a következőek: 
konstruktor, amellyel meg lehet adni, hogy mennyi dimenziónban és azokban a tömbünk milyen kiterjedésű legyen,set(), amellyel be tudjuk állítani egy elem értéket az adott dimenziók szerinti pozícióban, nyilván csak akkor, ha a dimenziók száma, és azon belül a indexek megfelelőek,
get(), amellyel le tudjuk kérdezni egy elem értékét, amennyiben a megadott pozíció dimenziója és azon belül az indexek megfelelőek, ellenkező esetbennullreferenciát adjunk vissza,
getDimensions(), amely visszaadja, hogy mekkorák a tömb egyes dimenziói.
Az arrays.generic.MArrayfelhasználásával (kompozíció) valósítsunk meg egyarrays.DoubleMatrixosztályt, amelyDoubletípussal valós számokat tárol egy mátrixban! Az osztálynak legyenek a következő műveletei: 
konstruktor, amely megadja, hogy az egyes dimenziókban mekkora a mátrix kiterjedése,set(), amellyel be tudunk állítani értéket az adott pozícióban,
get(), amellyel le tudjuk kérdezni az adott pozíción található értéket,
 Érvénytelen indexek (vagyis amikor olyan értéket adunk meg, amelyek már nem a mátrix egy elemét címzik) esetén ne változtassuk meg a mátrixot! A get()esetén pedigDouble(csomagoló) objektumokat adjunk vissza, így anullreferenciával tudjuk jelezni, ha érvénytelen pozícióról nem kérdezhető le elem. Fontos, hogy a felhasználó számára ne legyen látható, hogy az arrays.generic.MArraysegítségével valósítottuk meg ezt a típust! LinkekKapcsolódó forráskódokOktatói honlap
 Vissza
 |