tifyty

pure Java, what else ?

Hídeljárások és volatile metódusok

Ha valaki valamikor játszott már a reflection-nel, és esetleg elengedte a getDeclaredMethods() eljárást bizonyos osztályokra, érhették meglepetések. Nevezetesen az, hogy nem csak azok a metódusok vannak az osztályban, amelyek a Java forráskódban szerepelnek. Vagy esetleg megnézte az eljárások módosítóit, és kiderült, hogy van közöttük törékeny (volatile). Ez egyébként egy jó interjú kérdés: mit jelent az, amikor egy metódus volatile? A helyes válasz: metódus nem lehet volatile. És mégis: a reflection getDeclaredMethods() esetleg getMethods() által visszaadott metódusok között lehet olyan, amelyikre a Modifier.isVolatile(method.getModifiers()) igaz értéket ad vissza.

Ilyen történt például az immutátor project egyik felhasználójával is. Azt találta, hogy a generált immutátor, amelyik egyébként elég mélyen nyúlkál a Java lelki világába, olyan kódot generált, amit aztán maga sem tudott lefordítani, működésről , meg mint a boszorkányokról ezek után szó sem eshetett.

Nah, de mi is történik itt? Mik azok a szintetikus metódusok, és mik a híd (bridge, most) metódusok?

Láthatóság

Nem tudom feltűnt-e fészkelt osztály (nested class) készítése során (ugye nested class, meg inner class nem ugyan az) a belső private változók kívülről is elérhetőek. Ezt ki is használja például az immutable embedded builder pattern. Ez egyébként le van írva a Java nyelvi speckóban is, hogy így kell lennie: ami a legkülső osztályon belül van, az elérhető.
(Jut eszembe: ebből a szempontból a belső és a beágyazott osztályok között nincs is különbség.)

JLS7, 6.6.1 Determining Accessibility

… if the member or constructor is declared private, then access is
permitted if and only if it occurs within the body of the top level class (§7.6)
that encloses the declaration of the member or constructor…

package synthetic;

public class SyntheticMethodTest1 {
    private A aObj = new A();

    public class A {
        private int i;
    }

    private class B {
        private int i = aObj.i;
    }

    public static void main(String[] args) {
        SyntheticMethodTest1 me = new SyntheticMethodTest1();
        me.aObj.i = 1;
        B bObj = me.new B();
        System.out.println(bObj.i);
    }
}

A gond akkor jön, amikor lefordítjuk az osztályt. A JVM-nek ugyanis hiába mondaná bárki, hogy belső osztály, meg beágyazott, mert fogalma nem lesz, hogy azok mik. A JVM nem ismeri ezeket a fogalmakat, a Java fordító minden osztályból “külső” osztályt csinál. Így készülnek a mindenféle ...$..class fájlok:

 $ ls -Fart
../                         SyntheticMethodTest2$A.class  MyClass.java  SyntheticMethodTest4.java  SyntheticMethodTest2.java
SyntheticMethodTest2.class  SyntheticMethodTest3.java     ./            MyClassSon.java            SyntheticMethodTest1.java

Ha készítesz egy belső osztályt (static class), akkor abból lesz egy teljes értékű külső osztály. Ha készítesz egy beágyazott osztályt (nem static csak sima class), akkor annyival megspékeli, hogy az összes konstruktornak átadja a referenciát ami a körülfogó osztályra mutat, és amin keresztül eléri a külső osztály mezőit. A beágyazott osztály beágyazott osztályáról most ne essen szó.

Igen, csak mi lesz a privát metódusokkal és mezőkkel? Ha azok kikerülnek egy külső osztályba, akkor hogyan lesznek elérhetőek?

Úgy, hogy minden ilyen metódushoz, konstruktorhoz, vagy mezőhöz legenerál egy/két szintetikus metódust, vagy konstruktort amelyiken keresztül az eredeti elérhető. Konstruktornál plusz egy szintetikus konstruktort hoz létre, metódusnál egy szintetikus metódust, mezőnél pedig egy szintetikus setter-t és egy szintetikus getter-t. A szép benne az, hogy ezt ráadásul okosan csinálja, és ha például egy mezőhöz csak az olvasáshoz kell a szintetikus metódus, mert írni csak a belső osztályon belülről írjuk, akkor setter-t nem fog generálni, csak getter-t, és persze fordított esetben is. Csak azokat a metódusokat fogja legenerálni, amelyek tényleg kellenek a láthatóság kiterjesztéséhez.

A metódusok neve pedig… nos az lesz, amit a fordító akar.

package synthetic;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

public class SyntheticMethodTest2 {

    public static class A {
        private A(){}
        private int x;
        private void x(){};
    }

    public static void main(String[] args) {
        A a = new A();
        a.x = 2;
        a.x();
        System.out.println(a.x);
        for (Method m : A.class.getDeclaredMethods()) {
            System.out.println(String.format("%08X", m.getModifiers()) + " " + m.getName());
        }
        System.out.println("--------------------------");
        for (Method m : A.class.getMethods()) {
            System.out.println(String.format("%08X", m.getModifiers()) + " " + m.getReturnType().getSimpleName() + " " + m.getName());
        }
        System.out.println("--------------------------");
        for( Constructor<?> c : A.class.getDeclaredConstructors() ){
            System.out.println(String.format("%08X", c.getModifiers()) + " " + c.getName());
        }
    }
}

Erre a kódra az a Java fordító és JVM amit éppen használok a következőt adja ki:

2
00001008 access$1
00001008 access$2
00001008 access$3
00000002 x
--------------------------
00000111 void wait
00000011 void wait
00000011 void wait
00000001 boolean equals
00000001 String toString
00000101 int hashCode
00000111 Class getClass
00000111 void notify
00000111 void notifyAll
--------------------------
00000002 synthetic.SyntheticMethodTest2$A
00001000 synthetic.SyntheticMethodTest2$A

A programban értéket adunk az x mezőnek, és meghívjuk az ugyanilyen nevű metódust is. Ezek ahhoz kellenek, hogy a fordító legenerálja a szintetikus metódusokat. Látszik, hogy három metódus keletkezett, sejthetően az x mező getter-e és setter-e, valamint egy szintetikus metódus az x() metódushoz. Ezek azonban nem látszanak a következő listában, hiszen ezek csak szintetikus metódusok, kívülről nem hívhatók meg, olyanok, mint a privát metódusok.

A hexa számok a java.lang.reflect.Modifier osztályban definiált konstansok segítségével fejthetők meg:

00001008 SYNTHETIC|STATIC
00000002 PRIVATE
00000111 NATIVE|FINAL|PUBLIC
00000011 FINAL|PUBLIC
00000001 PUBLIC
00001000 SYNTHETIC

Konstruktorból is kettő van: van egy privát és van egy szintetikus. Privát azért van, mert definiáltunk egyet, tehát annak lennie kell. Viszont van egy szintetikus is, mert meghívjuk ezt a privát konstruktort a külső osztályból. Hidunk eddig még nincs.

Genrikusok, öröklődés

Ez eddig egyszerű, és nagyszerű volt, de nem jutottunk el a “volatile” metódusokig.

A java.lang.reflec.Modifier forráskódjába belenézve láthatjuk, hogy a 0x00000040 konstans kétféleképpen is definiálva van, egyszer VOLATILE egyszer pedig BRIDGE néven (ez utóbbi nem publikus, csak package private).

Ahhoz, hogy ilyen metódust generáljon nekünk a java fordító egy nagyon egyszerű program is elég:

package synthetic;

import java.lang.reflect.Method;
import java.util.LinkedList;

public class SyntheticMethodTest3 {

    public static class MyLink extends LinkedList<String> {
        @Override
        public String get(int i) {
            return "";
        }
    }

    public static void main(String[] args) {

        for (Method m : MyLink.class.getDeclaredMethods()) {
            System.out.println(String.format("%08X", m.getModifiers()) + " " + m.getReturnType().getSimpleName() + " " + m.getName());
        }
    }
}

Mit látunk? Azt, hogy van egy láncolt listánk, aminek a get(int) metódusa String-eket ad vissza. Hogy ez mennyire szép és tiszta kód, most ne menjünk bele. A példa erőltetett, azért, hogy egyszerű legyen. A való életben, szép kódban is előjön, csak sokkal bonyolultabban. A kimenet szerint

00000001 String get
00001041 Object get

két get() metódus is lett. Az egyik, amelyik szerepel a forráskódban, a másik pedig szintetikus és híd metódus. (És mind a kettő publikus.) A javap azt mondja, hogy a kód

public java.lang.String get(int);
  Code:
   Stack=1, Locals=2, Args_size=2
   0:   ldc     #2; //String
   2:   areturn
  LineNumberTable:
   line 12: 0


public java.lang.Object get(int);
  Code:
   Stack=2, Locals=2, Args_size=2
   0:   aload_0
   1:   iload_1
   2:   invokevirtual   #3; //Method get:(I)Ljava/lang/String;
   5:   areturn

A két metódus szignatúrája megegyezik, csak a visszatérési érték más, és a híd metódus nem tesz mást, mint meghívja a deklaráltat.

De kinek kell ez a szintetikus metódus? Például annak a kódnak, amelyik meg akarja hívni a get metódust, de a változó, amin keresztül meg akarja hívni nem MyLink, csak…

        List<?> a = new MyLink();
        Object z = a.get(0);

Ez nem tudná meghívni a String-et visszaadó metódust, hiszen olyan a List-ben nincs. Talán még tisztább lesz, ha nem a get() hanem az add() metódust írjuk felül:

package synthetic;

import java.util.LinkedList;
import java.util.List;

public class SyntheticMethodTest4 {

    public static class MyLink extends LinkedList<String> {
        @Override
        public boolean add(String s) {
            return true;
        }
    }

    public static void main(String[] args) {
        List a = new MyLink();
        a.add("");
        a.add(13);
    }
}

Ha megnézzük, hogy milyen bridge metódus keletkezik, akkor láthatjuk:

public boolean add(java.lang.Object);
  Code:
   Stack=2, Locals=2, Args_size=2
   0:   aload_0
   1:   aload_1
   2:   checkcast       #2; //class java/lang/String
   5:   invokevirtual   #3; //Method add:(Ljava/lang/String;)Z
   8:   ireturn

hogy nem csak meghívja az eredeti metódust, hanem futási időben el is végzi azt a típus ellenőrzést, amit a generikusok kiirtása miatt a run-time magától nem tud megtenni. Amint az várható a 18. sorban el is dobja kivételt.

Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
	at synthetic.SyntheticMethodTest4$MyLink.add(SyntheticMethodTest4.java:1)
	at synthetic.SyntheticMethodTest4.main(SyntheticMethodTest4.java:18)

Ezek után senki sem fog meglepődni, ha egy interjún azt a kérdést kapja, hogy mit jelent, ha egy metódus volatile.

6 responses to “Hídeljárások és volatile metódusok

  1. Hemo február 20, 2014 4:48 du.

    Szia! Nagyon jó a cikk! Kár, hogy erről nem 2 hónappal korábban írtál erről!

  2. Kofa február 21, 2014 9:32 du.

    A bridge-nek pl. a visszafelé kompatibilitás miatt van nagy jelentősége.
    Egy pre-5-ös JDK-val lefordított kódban pl. a Comparable-t megvalósító osztályokban
    public int compareTo(Object o);
    metódus van, így két Integer összehasonlításakor az Integer példányon a compareTo(Object o) hívódik meg (normál esetben) Integer paraméterrel.
    Ha a generic-es
    public class Integer implements Comparable
    csak a
    public int compareTo(Integer other)
    metódust tartalmazná, nem futnának a generics bevezetése előtt lefordított kódok. A bridge viszont legacy-kompatibilis.

    • Peter Verhas február 21, 2014 9:38 du.

      Ezt tudhatná maga a JVM is. Az egész szintetikus metódus játék, és a bridge metódusok működhetnének JVM szinten is, csak jelezve a JVM számára, hogy privát, vagy nem olyan a típusa ámde mégis meg lehet hívni, csak speciálisan.

      Ehelyett kódot generál a javac.

  3. tamasrev február 22, 2014 11:53 du.

    Fhu, van itt bőven emésztenivaló. Elolvasom holnap is, hátha akkor már többet értek belőle.
    Addig is: s/reflec.Modifier/reflecT.Modifier/g (egy helyen el van írva)

Vélemény, hozzászólás?

Adatok megadása vagy bejelentkezés valamelyik ikonnal:

WordPress.com Logo

Hozzászólhat a WordPress.com felhasználói fiók használatával. Kilépés / Módosítás )

Twitter kép

Hozzászólhat a Twitter felhasználói fiók használatával. Kilépés / Módosítás )

Facebook kép

Hozzászólhat a Facebook felhasználói fiók használatával. Kilépés / Módosítás )

Google+ kép

Hozzászólhat a Google+ felhasználói fiók használatával. Kilépés / Módosítás )

Kapcsolódás: %s

%d blogger ezt kedveli: