Kihagyás

Típus információk

Az előadás videója elérhető a itt.

Futás közbeni típusazonosítás

A futás közbeni típusazonosítás, vagy angolul Runtime type information (RTTI) teszi lehetővé, hogy a program futása közben kinyerjük egy objektum típusát akár úgy, hogy mást nem ismerünk, csak az objektumra mutató referenciát. Ez a mechanizmus teszi lehetővé például, hogyha egy ős osztály típusú referenciával rendelkező objektumunk van, akkor ott megállapítható, hogy az melyik konkrét altípusú objektumra hivatkozik.

Javaban ezen kívül a "reflection" mechanizmussal arra is lehetőség van, hogy akár egy teljesen ismeretlen osztály felépítése is lekérhető legyen futásidőben, esetleg azzal a többlettel is, hogy akár módosítani is tudunk egy-egy típus jellemzőt.

Példa

import java.util.*;

class Alakzat {
  public void rajzolj() {
    System.out.println(this + ".rajzolj");
  }
}

class Haromszog extends Alakzat {
  public String toString() {
    return "Haromszog";
  }
}

class Negyzet extends Alakzat {
  public String toString() {
    return "Negyzet";
  }
}

class Kor extends Alakzat {
  public String toString() {
    return "Kor";
  }
}

public class AlakzatPelda {
  public static void main(String[] args) {
    List s = new ArrayList();
    s.add(new Kor());
    s.add(new Haromszog());
    s.add(new Negyzet());
    Iterator i = s.iterator();
    while(i.hasNext())
      ((Alakzat)i.next()).rajzolj();  // RTTI használata impliciten
  }
}

Az AlakzatPelda osztály main metódusában adott egy listánk, amibe különböző alakzat típusokat pakolunk: négyzetet, kört, háromszöget. A lista, illetve bármelyik kollekció Java 5 előtti formája a konkrét típus helyett csak az objektumok referenciáját tárolja el Object típusú objektumként. Amikor bejárjuk a tárolót, és egyesével elérjük az eltárolt elemeket, akkor ahhoz, hogy az Alakzat osztályban definiált rajzolj metódust meg tudjuk hívni, egyszerűen downcastoljuk az Object típusként adott objektumainkat Alakzat típusra, amely az RTTI implicit használata hatására egyből a megfelelő gyerek osztályra kasztolódik, így mindig a soron következő objektum dinamikus típusának megfelelő rajzolj metódus fog meghívódni, amit igazol a kimenet is:

Kimenet

Kor . rajzolj

Haromszog . rajzolj

Negyzet . rajzolj

A Class objektum

Hogy pontosan megértsük, hogyan is működik az RTTI a Java nyelvben, megismerkedünk azzal, hogyan is tárolja el a Java a típus információkat. A Java egy speciális Class objektumot hoz létre minden osztályhoz. Ez a Class objektum lesz felelős azért, hogy létrehozza a "szabályos" objektumait az adott osztálynak. A Java az RTTI-t ennek a Class objektumnak a segítségével oldja meg még az olyan dolgokat is, mint a kasztolás, de a Class osztály számos egyéb módon használható az RTTI-hez.

Szóval minden osztályhoz lesz egy Class objektum, ami a program része. Ez a Class objektum nem más, mint az a "class", ami akkor keletkezik, amikor megírtunk, majd lefordítottunk egy új osztályt, és ami kiíródik nekünk egy class kiterjesztésű fájlként. Ahhoz, hogy ebből a binárisból létrejöjjön egy objektum, a JVM a program futása során a class loadert veszi segítségül (illetve class loaderek láncolatát), amiből nekünk a lényeg az, hogy valamikor, amikor először hivatkozunk az osztály egy statikus elemére, a használni kívánt osztályok betöltődnek dinamikusan. Igen-igen, a konstruktor hívás is statikus hívásnak minősül, akkor is, ha előtte nem szerepel a static módosító, így amikor létre akarunk hozni egy adott típusú objektumot, és még nem használtuk azt az osztályt, betöltődik az.

A Java program nem töltődik be teljesen a memóriába a program indulásakor. Ebből a szempontból eltér más, hagyományos programozási nyelvektől. Ehelyett darabokban töltődik be, igény szerint. A dinamikus betöltés olyan tulajdonságokat tesz lehetővé, amit nem lehetne megoldani, vagy csak nehezen azoknál a nyelveknél, amelyek statikusan betöltik a teljes programot a program végrehajtása előtt. A class loader először ellenőrzi, hogy az adott típushoz a Class objektum már létezik-e, ha nem, akkor a default class loader betölti (ha nem találja, akkor valamelyik más class loader fogja majd a sorban). A lényeg, hogy ezután lehet csak használni az adott osztály elemeit.

A Class objektum elérése

A Class objektumát egy osztálynak különböző módokon tudjuk elérni. Egyik lehetőség, hogy a reflection segítségével a Class osztály forName metódusával betöltjük: Class.forName("OsztalyNev")

Másik lehetőség, hogy az osztálynév után tesszük a class literált: OsztalyNev.class

Harmadik lehetőség, hogy az Object osztályban definiált getClass metódussal lekérjük az adott objektumtól: x.getClass(), ha az adott objektum az x.

Példa

class Alakzat {
  static {
    System.out.println("Alakzat betoltese");
  }
  public void rajzolj() {
    System.out.println(this + ".rajzolj");
  }
}
class Haromszog extends Alakzat {
  static {
    System.out.println("Haromszog betoltese");
  }
  public String toString() {
    return "Haromszog";
  }
}
public class AlakzatPelda2 {
  public static void main(String[] args) {
    new Alakzat();
    try {
      Class c = Class.forName("Haromszog");
    } catch (ClassNotFoundException e) {
      e.printStackTrace(System.err);
    }
  }
}

A példában adott egy ős osztály (Alakzat) és ennek egy gyerek osztálya (Háromszög). Mindkét osztályban van egy-egy static init blokk. Ezeknek a blokknak az utasításai akkor fognak lefutni, amikor az adott osztályokat a megfelelő class loader betölti.

Az AlakzatPelda2 main metódusában először létrehozunk egy Alakzat objektumot, ekkor töltődik be az Alakzat osztály. Betöltődéskor pedig végrehajtódnak a static init blokkjának utasításai. Ezután reflection segítségével betöltjük a Haromszog osztályt. Ekkor meghívódik a Haromszog static init blokkja is. Fontos, hogy ez utóbbi esetet a megfelelő kivételkezelőkkel el kell lássuk, hiszen a forName metódus kivételt dobhat, ha a paraméterben kapott sztringgel nem találja meg a kért osztályt.

A program futásának az eredménye:

Kimenet

Alakzat betoltese

Haromszog betoltese

Class literálok

A következő példában az Alakzat és Haromszog osztályok legyenek változatlanok:

class Alakzat {
  static {
    System.out.println("Alakzat betoltese");
  }
  public void rajzolj() {
    System.out.println(this + ".rajzolj");
  }
}

class Haromszog extends Alakzat {
  static {
    System.out.println("Haromszog betoltese");
  }
  public String toString() {
    return "Haromszog";
  }
}

public class AlakzatPelda3 {
  public static void main(String[] args) {
    new Alakzat();
    Class c = Haromszog.class;
  }
}

Az AlakzatPelda3 osztály main metódusában szintén hozzunk létre egy Alakzat objektumot. Ekkor betöltődik az Alakzat osztály. Utána viszont a Haromszog osztály class literáljára való hivatkozással betöltődik a Haromszog osztály is. Így a kimenete ennek a programnak megegyezik az előző program kimenetével.

Objektumtípus ellenőrzése

Ha futásidőben szeretnénk ellenőrizni, hogy egy adott objektumnak mi a típusa, akkor használhatjuk az instanceof statikus operátort:

import java.util.*;

class Alakzat {
  public void rajzolj() {
    System.out.println(this + ".rajzolj");
  }
}

class Haromszog extends Alakzat {
  public String toString() {
    return "Haromszog";
  }
}

class Negyzet extends Alakzat {
  public String toString() {
    return "Negyzet";
  }
}

class Kor extends Alakzat {
  public String toString() {
    return "Kor";
  }
}

public class AlakzatPelda4 {
  public static void main(String[] args) {
    List s = new ArrayList();
    s.add(new Kor());
    s.add(new Haromszog());
    s.add(new Negyzet());
    Iterator i = s.iterator();
    while(i.hasNext()) {
      Object o = i.next();
      if (o instanceof Haromszog)
        ((Haromszog)o).rajzolj();
    }
  }
}

A példában az Alakzat osztálynak több speciális változatát is megvalósítjuk, és az AlakzatPelda4 main metódusában minden speciális osztályból létrehozott objektumából teszünk egy példányt a main listájába. Ezután a listát bejárjuk, és minden objektumot ellenőrzünk, hogy az adott objektum Haromszog-e. Ha igen, akkor konvertáljuk az adott objektumot Haromszog típusúvá, és meghívjuk a rajzolj metódusát. Mivel egyetlen egy háromszög volt a listában, így a program kimenete:

Kimenet

Haromszog . rajzolj

A class objektum isInstance metódusa hasonló ellenőrzést végez, csak dinamikus módon:

import java.util.*;

class Alakzat {
  public void rajzolj() {
    System.out.println(this + ".rajzolj");
  }
}

class Haromszog extends Alakzat {
  public String toString() {
    return "Haromszog";
  }
}

class Negyzet extends Alakzat {
  public String toString() {
    return "Negyzet";
  }
}

class Kor extends Alakzat {
  public String toString() {
    return "Kor";
  }
}

public class AlakzatPelda5 {
  public static void main(String[] args) {
    List s = new ArrayList();
    s.add(new Kor());
    s.add(new Haromszog());
    s.add(new Negyzet());
    Iterator i = s.iterator();
    while(i.hasNext()) {
      Object o = i.next();
      if (Haromszog.class.isInstance(o))
        ((Haromszog)o).rajzolj();
      // vagy:
      try {
        if (Class.forName("Haromszog").isInstance(o))
          ((Haromszog)o).rajzolj();
      } catch (ClassNotFoundException e) {
        e.printStackTrace(System.err);
      }
    }
  }
}

Az AlakzatPelda5-ben a Haromszog Class objektumán keresztül ellenőrizzük, hogy az aktuális objektum a Haromszog osztály példánya-e.

Egyéb Class metódusok

A Class osztálynak az eddigieken kívül még van pár olyan metódusa, ami biztos kelleni fog, ha a Java osztályaink "mélyére" szeretnénk látni. Ezek a teljesség igénye nélkül:

Metódus Leírás
getName visszaadja a paraméterben kapott osztály Class objektumát
isInterface ellenőrzi, hogy az adott osztály interface-e
getMethods visszaadja az osztály metódusait
getConstructors visszaadja az osztály konstruktorait
getSuperclass visszaadja az osztály ősosztályát
getPackage visszaadja az oszály csomagját

Példa reflection-re: metóduslistázó

A következő példában egy kicsit lessünk be jobban a reflection lehetőségeibe.

import java.lang.reflect.*;

class Kutya {
  private int kutyaSz;
  Kutya(int i) {kutyaSz = i;}
  public String toString() {
    return "Kutya #" + kutyaSz;
  }
  public int sorszam() {
    return kutyaSz;
  }
}
public class KutyaPelda {
  public static void main(String[] args) {
    if (args.length != 1) System.exit(0);
    try {
      Class c = Class.forName(args[0]);
      System.out.println(c.getName());
      System.out.println(c.isInterface());
      Method[] m = c.getMethods();
      Constructor[] ctor = c.getConstructors();
      for (int i = 0; i < m.length; i++)
        System.out.println(m[i]);
      for (int i = 0; i < ctor.length; i++)
        System.out.println(ctor[i]);
    } catch(ClassNotFoundException e) {
      System.err.println(e);
    }
  }
}

A KutyaPelda osztályban dinamikusan töltsünk be egy osztályt. Hogy biztosak is legyünk abban, hogy nem tudhatunk semmit erről a betöltendő osztályról fordítási időben, így az osztály nevét a parancssori argumentumokban adjuk meg (azaz a már futó program tudja azt csak értelmezni).

A betöltött osztálynak írjuk ki a nevét, azt, hogy interface-e, soroljuk fel a metódusait, konstruktorait.

Próbáljuk is ki a programot! Ha argumentumként a Kutya osztályra hivatkozunk, akkor a kimenet:

Kimenet

Kutya
false
public java.lang.String Kutya.toString()
public int Kutya.sorszam()
public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
public final void java.lang.Object.wait() throws java.lang.InterruptedException
public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
public boolean java.lang.Object.equals(java.lang.Object)
public native int java.lang.Object.hashCode()
public final native java.lang.Class java.lang.Object.getClass()
public final native void java.lang.Object.notify()
public final native void java.lang.Object.notifyAll()

Ha a paraméter maga a KutyaPelda osztály, akkor a kimenet:

Kimenet

KutyaPelda
false
public static void KutyaPelda.main(java.lang.String[])
public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
public final void java.lang.Object.wait() throws java.lang.InterruptedException
public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
public boolean java.lang.Object.equals(java.lang.Object)
public java.lang.String java.lang.Object.toString()
public native int java.lang.Object.hashCode()
public final native java.lang.Class java.lang.Object.getClass()
public final native void java.lang.Object.notify()
public final native void java.lang.Object.notifyAll()
public KutyaPelda()

Ha egy kicsit alaposabban megvizsgáljuk ez utóbbi kimenetet és a forráskódot, akkor észrevehetjük, hogy a Class objektumban valóban minden információ megvan az adott osztályról. Az is, ami a forráskódban nincs jelen, mint amilyen a default konstruktor, ami fordításkor generálódik le.


Utolsó frissítés: 2024-04-11 07:54:27