tifyty

pure Java, what else ?

Havi archívumok: február 2014

Auto boxing

Az autoboxing-gal minden Java fejlesztő tisztában van, amióta van 1.5 Java. (Vagy én vagyok nagyon naiv. Ha a matchbox jutott eszedbe, vagy hirtelen box kesztyűket húzó, és boxoló autókra asszociáltál, akkor vagy nem Java fejlesztő vagy, vagy ideje átismételni.) Van is róla jó kis tutorial az ORACLE oldalain. Az autoboxing az, amikor a Java fordító olyan kódot generál, amelyik valamelyik primitív típusból objektumot állít elő automatikusan, amikor arra van szükség. Tehát írhatjuk, hogy

Integer a = 42;

és ebből automatikusan generálódik egy olyan értékadás, ami az int 42 értéket Integer objektummá teszi. És ez annyira szép, és egyszerű, és annyira belefeledkezünk az évek során, hogy egyszer csak senior fejjel is rácsodálkozunk egyes részletekre, és egy ideig nem értjük.

Például azt, hogy van double.class és Double.class. Ugye mind a kettő egy objektum (mint minden osztály) és mind a kettő típusa Class. Sőt! Java 1.5 óta mind a kettő típusa Class<Double>.

No de, ha ugyanaz a típusuk, akkor kompatibiliseknek kellene lenniük egymással. Márpedig

public class TypeFun {
    public static void main(String[] args) {
        // public static final Class<Double>   TYPE = (Class<Double>)Class.getPrimitiveClass("double");
        System.out.println("Double.TYPE == double.class: " + (Double.TYPE == double.class));
        System.out.println("Double.TYPE == Double.class: " + (Double.TYPE == Double.class));
        System.out.println("double.class.isAssignableFrom(Double.class): " + (double.class.isAssignableFrom(Double.class)));
        System.out.println("Double.class.isAssignableFrom(double.class): " + (Double.class.isAssignableFrom(double.class)));
    }
}

eredménye

Double.TYPE == double.class: true
Double.TYPE == Double.class: false
double.class.isAssignableFrom(Double.class): false
Double.class.isAssignableFrom(double.class): false

vagyis a Double primitív párja a double.class (nem meglepő), mégis az egyik a másikból nem assign-olható. (na ezt fordítsa le valaki úgy, mint az aggódások szétválasztását)

Persze utána lehet nézni a dolgoknak, hiszen a Double osztály benne van a Java RT-ben, és open source. Ott viszont azt látjuk, hogy a

public static final Class<Double>	TYPE = (Class<Double>) Class.getPrimitiveClass("double");

Miért nem double.class? Hiszen az a Double típus primitív megfelelője, és minden olyan osztályhoz, amelyiknek van primitív párja a TYPE mező a primitív párját adja vissza.

A válasz nem triviális és elég mélyen bele kell vájni a Java és a JVM lelki világába, hogy megértsük. Mivel a double nem osztály, ezért nincs is olyan igazán, hogy double.class. A Java fordító, és a run time library itt is összefonódik egy kicsit, és a fordító tudja, hogy a Double osztály definiál egy TYPE típusú konstans mezőt, amelyik a Double primitív megfelelőjének az osztálya, és valahányszor a kódban double.class-t lát valójában Double.TYPE-ot fordít. (Ezt érdemes kipróbálni és megnézni javap-pal.) Emiatt nem írhattak a RT fejlesztői sem

public static final Class<Double>	TYPE = double.class;

kódot a Double forrásába, hiszen abból

public static final Class<Double>	TYPE = TYPE;

lett volna, aminek nem lenne sok értelme. És hogyan megy az autobox-ing pontosan?

Double b = (double)1.0;

forrásból

         0: dconst_1      
         1: invokestatic  #2                  // Method java/lang/Double.valueOf:(D)Ljava/lang/Double;
         4: astore_1 

lesz, ha viszont megfordítjuk, és kicseréljük a két d betűt

double b = (Double)1.0;

akkor

         0: dconst_1      
         1: invokestatic  #2                  // Method java/lang/Double.valueOf:(D)Ljava/lang/Double;
         4: invokevirtual #3                  // Method java/lang/Double.doubleValue:()D
         7: dstore_1    

ami elég sok mindent megmagyaráz. A double.class és a Double.class nem assignolható egymásból, de az autoboxing ezt megoldja. Rég volt már a Java 1.4 szerencsére el is felejtettük.

Házi feladat: ismételjük át önállóan, hogy mi a helyzet az autoboxing-gal túltöltött metódus argumentumoknál!

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.