Java SE 8 Documentation
Feladatok

``Stratégiák''

Visszatérve még egy kicsit az előző gyakorlaton megismert interfészekhez, érdemes megemlíteni azoknak az interfészeknek a csoportját, amelyek azt teszik lehetővé, hogy egy algoritmus viselkedését futási időben választhassuk meg. Ezt stratégiának (strategy) vagy házirendnek (policy) nevezik. Ekkor leírjuk algoritmusok egy családját egy interfésszel, majd az ezt megvalósító osztályok lesznek a konkrét algoritmusok. Ekkor a felhasználás helyén elegendő csak a családra (interfészre) hivatkozni, és később a konkrét osztályok példányainak segítségével megadhatjuk, melyik konkrét implementációt választjuk az adott helyen.

Például:

interface BinaryIntFunction {
    int apply(int x, int y);
}

class Add implements BinaryIntFunction {
    @Override
    public int apply(int x, int y) {
        return (x + y);
    }
}

class Subtract implements BinaryIntFunction {
    @Override
    public int apply(int x, int y) {
        return (x - y);
    }
}

class Multiply implements BinaryIntFunction {
    @Override
    public int apply(int x, int y) {
        return (x * y);
    }
}

class Context {
    private final BinaryIntFunction bif;

    public Context(BinaryIntFunction bif) {
        this.bif = bif;
    }

    public int compute(int x, int y) {
        return this.bif.apply(x, y);
    }
}

class Strategies {
    public static void main(String[] args) {
        Context context;
        int x, y;

        x = Integer.parseInt(args[0]);
        y = Integer.parseInt(args[1]);
        context = new Context(new Add());
        System.out.println("Add: " + context.compute(x, y));

        context = new Context(new Subtract());
        System.out.println("Subtract: " + context.compute(x, y));

        context = new Context(new Multiply());
        System.out.println("Multiply: " + context.compute(x, y));
    }
}

Egy másik ilyen hasznos interfész még a java.util.Comparator:

interface Comparator<T> {
    int compare(T o1, T o2);
    boolean equals(Object obj);
}

Egy ilyen interfészt implementáló osztály megadásával "ad hoc" módon tudunk rendezést definiálni. Vannak ugyanis bizonyos metódusok, amelyek egy ilyen interfésznek megfelelő objektumot várnak paraméterül és ezzel lényegében lehetővé teszik azt, hogy abban a hívásban felüldefiniálhassuk a típushoz tartozó alapértelmezett rendezést. Illetve ezzel olyan típusok esetén is használni tudjuk esetleg a metódust, amelyekhez nincs alapértelmezett semmilyen rendezést társítva.

Erre példa lehet a java.util.Collections osztály sort() metódusa, amellyel listákat tudunk rendezni egy Comparator felhasználásával:

static <T> void sort(List<T> list, Comparator<? super T> c);

Ebben a szignatúrában a super megszorítás azt jelenti a T típusváltozóra, hogy a T típusnak, vagy annak valamelyik ősosztályának meg kell tudnia valósítania a java.util.Comparator interfészt.

A használatára egy példa:

import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

class NumericStringComparator implements Comparator<String> {
    @Override
    public int compare(String s1, String s2) {
        int x, y;
        try {
            x = Integer.parseInt(s1);
            y = Integer.parseInt(s2);
            return (x - y);
        }
        catch (NumberFormatException e) {
            return s1.compareTo(s2);
        }
    }

    @Override
    public boolean equals(Object obj) {
        return (obj instanceof NumericStringComparator);
    }
}

public class Comp {
    public static void main(String args[]) {
        List<String> values = Arrays.asList(args);
        Collections.sort(values);
        System.out.println("values = " + values);
        Collections.sort(values, new NumericStringComparator());
        System.out.println("values = " + values);
    }
}

Absztrakt osztályok

Amikor osztályokat származtatunk, az ősosztályok lényegében mindig egy közös felületet alakítanak ki leszármazottaik között: definiálnak egy interfészt, vagyis egy adat-, és metóduskészletet, amelyen keresztül a leszármazottak egységesen kezelhetőek (hiszen minden leszármazott példánya egyben az ős típusához is tartozik, ezt írja elő az altípusosság).

Azonban érdemes hozzátenni, hogy ezekben a közös ősökben nem kötelező mindig minden metódust megadni, elengedő lehet csupán azok szignatúráját leírni. Az ilyen, megvalósítás nélküli metódusokat tartalmazó osztályokat, absztrakt osztályoknak nevezzük, és ezt az osztály és metódus definíciójában az abstract módosítószó hozzákapcsolásával jelezhetjük. A fordító nem engedi meg, hogy abstract módosítószó szerepeljen private, final, static mellett, mivel így akkor nem lehet felüldefiniálni a metódust, enélkül pedig értelmetlen az alkalmazása.

Mivel egy absztrakt osztálynak nem definiáltuk minden metódusát, az absztrakt osztályok nem példányosíthatóak!

Absztrakt osztály kiterjesztésekor nem kötelező minden metódust megvalósítani, viszont ekkor a leszármazottnak is absztraktnak kell lennie.

Példa:

abstract class Shape {
    protected boolean symmetric;

    public Shape(boolean symmetric) {
        this.symmetric = symmetric;
    }

    public abstract double circumference();
    public abstract double area();

    public boolean isSymmetric() {
        return symmetric;
    }

    public void print() {
        System.out.println("A: " + area());
        System.out.println("C: " + circumference());
    }
}

class Circle extends Shape {
    private static final double PI = 3.1415;
    private double r = 1.0;

    public Circle() {
        super(true);
    }

    @Override
    public double circumference() {
        return (2 * r * PI);
    }

    @Override
    public double area() {
        return (r * r) * PI;
    }
}

class Rectangle extends Shape {
    private double a = 1.0, b = 1.0;

    public Rectangle() {
        super(true);
    }

    @Override
    public double circumference() {
        return 2 * (a + b);
    }

    @Override
    public double area() {
        return a * b;
    }
}

Alkalmazásra példa:

public class Main {
    public static void printArea(Shape shape) {
        System.out.println(shape.area());
    }

    public static void main(String[] args) {
        Shape s = new Circle(); // vö.: statikus-dinamikus típus
        printArea(s);
    }
}

Az absztrakt osztályok és interfészek összehasonlítása

Felhasználásuk jellege miatt az absztrakt osztályok és interfészek közt tudunk húzni egyfajta párhuzamot: egyiket nem lehet példányosítani, lehetnek bennük törzsnélküli metódusok. Azonban az absztrakt osztályok esetében érdemes tudni, hogy fel lehet venni olyan adattagokat is, amelyek nem osztályszintűek és nincs final módosítószavuk, valamint lehetnek törzssel rendelkező publikus, védett és privát metódusaik is. Az interfészek esetében minden adattagnak kötelezően publikusnak, osztályszintűnek és final fajtájúnak kell lennie, valamint a metódusok csak publikusak lehetnek. Illetve mivel az absztrakt osztály, továbbra is osztály, ezért csak ilyenből lehet örökölni, miközben interfészt akár többet is megvalósíthat az osztály.

Felmerülhet ennek mentén a kérdés, hogy mégis mikor melyiket érdemes választani.

  • Absztrakt osztály akkor érdemes alkalmazni, ha:

    • kódot akarunk megosztani több, egymáshoz szorosan kapcsolódó osztály közt,

    • a leszármazottak példányai közt adattagokat és metódusokat akarunk újrahasznosítani és a public láthatóságnál szigorúbbra van szükségünk,

    • nem csak osztályszintű és nem csak final adattagokat szeretnénk használni. Itt olyan metódusok is készíthetőek, amelyek az osztály példányaihoz állapotot el tudjuk érni és módosítani tudják.

  • Interfészt akkor érdemes alkalmazni, ha:

    • egymáshoz nem kapcsolódó osztályok esetén szeretnénk megkövetelni ugyanazt a funkcionalitást, például legyenek összehasonlíthatóak (java.lang.Comparable) az elemei,

    • egy adott adattípus viselkedését kívánjuk leírni, viszont nem számít, hogy ki és miként fogja majd megvalósítani,

    • többszörös öröklődésre van szükségünk.

A Java saját osztályai közül az absztrakt osztályokra egy példa lehet a java.util.AbstractMap, amely a Java Collections eleme. A leszármazottai (java.util.HashMap, java.util.TreeMap stb.) több, általa definiált közös metódust is tartalmaznak, mint például a get(), put(), isEmpty(), containsKey(), containsValue().

A java.util.HashMap osztállyal kapcsolatban pedig érdemes megjegyezni, hogy ez pedig egy olyan osztályra példa, amely több interfészt is megvalósít: java.io.Serializable, java.lang.Cloneable és java.util.Map. Csupán a megvalósított interfészek listáját végignézve láthatjuk, hogy a java.util.HashMap osztály egy példánya klónozható (lemásolható), szerializálható (byte-sorozattá alakítható) és tud véges leképezésként viselkedni. Azt is észrevehetjük, hogy ez az osztályban egyben a java.util.AbstractMap származottja. Vagyis lehetnek osztályok (és elég gyakran akadnak is), amelyek egy absztrakt osztályből öröklődnek, miközben több interfészt is megvalósítanak.

Felsorolási típusok

A Java nyelvben a felsorolási típusok, enum értékek, egy olyan speciális adattípust jelentenek, ahol lehetővé válik, hogy korlátozzuk annak értékkészletét előre definiált konstansok egy halmazára. Ennek megfelelően tehát az ilyen típusú változók ebből a halmazból vehetnek fel értéket. Erre jellemző példa a hét napjainak, vagy akár égtájaknak az ábrázolása. Ezeket a konstansokat -- hasonlóan a korábban említett final módosítóval megadott osztályszintű adattagokhoz -- konvenció szerint nagybetűkkel írjuk. Magát a felsorolási típus definícióját az enum kulcsszó vezeti be:

public enum Day {
    SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY
}

enum értékeket tehát akkor érdemes használni, ha konstansok egy előre megadott halmazára van szükségünk.

public class EnumDemo {
    Day day;

    public EnumDemo(Day day) {
        this.day = day;
    }

    public void tellItLikeItIs() {
        switch (day) {
            case MONDAY:
                System.out.println("Mondays are bad.");
                break;

            case FRIDAY:
                System.out.println("Fridays are better.");
                break;

            case SATURDAY: case SUNDAY:
                System.out.println("Weekends are best.");
                break;

            default:
                System.out.println("Midweek days are so-so.");
                break;
        }
    }

    public static void main(String[] args) {
        EnumDemo firstDay = new EnumDemo(Day.MONDAY);
        firstDay.tellItLikeItIs();
        EnumDemo thirdDay = new EnumDemo(Day.WEDNESDAY);
        thirdDay.tellItLikeItIs();
        EnumDemo fifthDay = new EnumDemo(Day.FRIDAY);
        fifthDay.tellItLikeItIs();
        EnumDemo sixthDay = new EnumDemo(Day.SATURDAY);
        sixthDay.tellItLikeItIs();
        EnumDemo seventhDay = new EnumDemo(Day.SUNDAY);
        seventhDay.tellItLikeItIs();
    }
}

Érdemes megjegyezni, hogy az enum deklarációval tulajdonképpen egy osztályt hozunk létre. Ez azt jelenti, hogy egy ilyen definíció törzsébe a többi osztályhoz hasonlóan írhatunk metódusokat és adattagokat. Illetve az enum osztállyal együtt a fordítóprogram is automatikusan létrehoz bizonyos metódusokat. Például, minden enum rendelkezik egy osztályszintű, values() nevű metódussal, amely egy tömbben visszaadja a típusban definiált konstansokat, abban a sorrendben, ahogy azokat definiáltuk. Ezt a metódust remekül fel lehet használni egy "for each" szerkezetben, ahol így az enum elemein tudunk végigmenni.

for (Planet p : Planet.values()) {
    System.out.printf("Your weight on %s is %f.\n", p, p.surfaceWeight(mass));
}

Minden felsorolási típus automatikusan a java.lang.Enum osztály leszármazottja. Mivel a nyelvben az osztályok esetén egyszeres öröklődés van, az enum definíciók nem rendelkezhetnek másik ősosztállyal. Valamint a nyelv megköveteli, hogy a definícióban a konstansokat adjuk meg először, és csak ezt követően az adattagokat és metódusokat. Illetve, ha adunk meg ilyeneket, akkor a konstansok felsorolását pontosveszővel kell zárni. Az enum típusok konstruktora nem lehet public vagy protected, és ezt mi magunk nem tudjuk meghívni.

public enum Planet {
    MERCURY (3.303e+23, 2.4397e6),
    VENUS   (4.869e+24, 6.0518e6),
    EARTH   (5.976e+24, 6.37814e6),
    MARS    (6.421e+23, 3.3972e6),
    JUPITER (1.9e+27,   7.1492e7),
    SATURN  (5.688e+26, 6.0268e7),
    URANUS  (8.686e+25, 2.5559e7),
    NEPTUNE (1.024e+26, 2.4746e7);

    private final double mass;   // kilogramm
    private final double radius; // méter

    Planet(double mass, double radius) {
        this.mass = mass;
        this.radius = radius;
    }

    private double mass() { return mass; }
    private double radius() { return radius; }

    // gravitációs állandó  (m^3 kg^{-1} s^{-2})
    public static final double G = 6.67300E-11;

    double surfaceGravity() {
        return G * mass / (radius * radius);
    }

    double surfaceWeight(double otherMass) {
        return otherMass * surfaceGravity();
    }

    public static void main(String[] args) {
        if (args.length != 1) {
            System.err.println("Usage: java Planet <earth_weight>");
            System.exit(-1);
        }

        double earthWeight = Double.parseDouble(args[0]);
        double mass = earthWeight / EARTH.surfaceGravity();

        for (Planet p : Planet.values()) {
           System.out.printf("Your weight on %s is %f.\n", p,
             p.surfaceWeight(mass));
        }
    }
}

Feladatok

  • Az előző gyakorlaton elkészített veremszámológépünket módosítsuk úgy, hogy egy Map<String,BinaryIntFunction> adatszerkezetben tároljuk el a felhasználható (bináris) műveleteket, és a kifejezés feldolgozása során a neki megfelelő művelettel számítsuk ki az eredményt! Ha nem találjuk az adott operátort ebben az adatszerkezetben, akkor adjunk hibaüzenetet: "Unknown operator"!

  • Hozzunk létre egy absztrakt Prism osztályt, amely segítségével hasábokat tudunk ábrázolni! Tároljuk el benne a hasáb magasságát (height), valamint legyen egy olyan absztrakt (vagyis a leszármazottakban megvalósítandó) metódus, amely az alapterületét számolja ki (baseArea()). Ennek felhasználásával aztán készítsünk egy másik metódust (volume()), amely a hasáb térfogatát számítja ki a magasság és az alapterület segítségével. Tegyük a polyhedra csomagba!

  • A hasábokból származtassuk (és ezzel együtt konkretizáljuk) a hengereket ábrázoló Cylinder osztályt, illetve a kockákat ábrázoló Cube osztályt!

    Az absztrakt metódus implementációja mellett definiáljuk felül azok toString() metódusait, hogy a típusnak megfelelő szöveges reprezentációval térjenek vissza.

    Cylinder esetén:

    Cylinder: (h=10,r=5)

    Cube esetén:

    Cube: (h=4)

    Ezek az osztályok is kerüljenek a polyhedra csomagba.

  • Definiáljunk egy Scalable interfészt, amely tartalmaz egy scale() metódust és egy double típusú defaultScale adattagot! A metódussal paraméter nélkül a defaultScale szerint nagyíthatjuk az objektumot, illetve a nagyítás mértékét paraméterként megkapva aszerint.

  • Implementáljuk a Scalable interfészt a Prism osztály leszármazottai esetén!

  • Implementáljuk a szabványos Comparable interfészt — rendezést — a Prism osztály leszármazottai esetén! A rendezés alapja legyen a objektumok térfogata szerint csökkenő. A műkődése a Collections.sort() metódussal próbálható ki.

  • A Comparator interfész segítségével készítsünk egy olyan speciális rendezést a Prism értékek számára, amellyel hasábokat az alapterületük szerint tudunk rendezni! A műkődése a Collections.sort() metódussal próbálható ki.

  • Módosítsuk úgy a korábbi Date osztályunkat, hogy benne a hónapok helyett egy felsorolási típust használjunk, ezzel kizárjuk az érvénytelen indexű hónapok megadását!

Linkek

Kapcsolódó forráskódok
Oktatói honlap
Vissza