A miért billeg cikkben körbejártuk egy kicsit, hogy mi a helyzet az Integer cache-sel. A cikk végén, pedig a szavazásba beleírtam, hogy
A szavazásba bele sem mertem írni, hogy reflection-nel megoldható, hogy az alsó határ se -128 legyen. Még valaki képes lett volna arra szavazni.
Ebben a cikkben azért mégiscsak megnézzük, hogy ha valaki ezt megpróbálná, akkor mire kell felkészülnie, és milyen trükköket kell alkalmaznia. A cikk végén pedig lesz csattanó is.
Mit is szeretnénk?
Azt szeretnénk elérni, hogy az Integer autoboxing ne csak -128 és 127 közötti értékekre adjon konstand Integer objektumokat, hanem egy ennél nagyobb intervallumra. Ebben az intervallumban a Java runtime két azonos értékű int értéket ugyanazon Integer objektummá konvertál autoboxing esetén. Azaz
Integer a = 13;
Integer b = 13;
if( a == b ){
System.out.println("kakukk");
}
az bizony kakukk. 13 helyett állhat itt bármilyen szám, -128 és 127 között (beleértve a határokat is), de ha -128-nál kisebb, vagy 127-nél nagyobb számot írunk, akkor bizony a != b.
A fentieket egyébiránt egészen pontosan úgy teszi a JVM, hogy amikor autoboxingot akar csinálni, akkor az Integer osztály statikus Integer.valueOf(int) metódusát hívja meg, ami kimásolva az rt.jar-ból (persze a forrás, de az is benne lehet a jar-ban):
public static Integer valueOf(int i) {
assert IntegerCache.high >= 127;
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
Az IntegerCache az Integer osztály egy belső privát statikus osztálya, mi osztálybetöltési időben beállítja a low és highint értékeket, valamint allokálja a cache tömböt, és feltölti a -128 és 127 közötti egész értékekkel. Ennél egy kicsit bonyolultabba dolog, mert lehet definiálni a java.lang.Integer.IntegerCache.high system property-t, és ha ez definiálva van, akkor a high értéke nem 127, hanem a megadott lesz. A low értékének megváltoztatására azonban, még osztálybetöltési időben sincs a Java által támogatott lehetőség.
Ezen kívül fontos megjegyezni, hogy a low, high és a cache változók az IntegerCache osztály statikus és final változói (ez még fontos lesz).
Mit tegyünk
Essünk neki ennek az osztálynak reflection-nel! Valójában persze nem, csak a tanulság kevéért, és az itt bemutatott példát ha partner cégnél éles kódban meglátom, vagy akár csak hasonlót, akkor soha többet szóba nem állok, szerződést nem kötök.
Készítsünk egy olyan kis metódust (legyen ez egy egyetlen statikus metódus egy utility osztályban), amelyik átírja a low, high és cache mezőket a kedvünk szerint. Valahogy így:
public static void setIntegerCacheLimits(int low, int high)
throws NoSuchFieldException, SecurityException,
IllegalArgumentException, IllegalAccessException {
Class<?>[] classes = Integer.class.getDeclaredClasses();
Class<?> cacheClass = null;
for (Class<?> klass : classes) {
if (klass.getSimpleName().equals("IntegerCache")) {
cacheClass = klass;
}
}
Field lowField = cacheClass.getDeclaredField("low");
lowField.setAccessible(true);
Field highField = cacheClass.getDeclaredField("high");
highField.setAccessible(true);
Field cacheField = cacheClass.getDeclaredField("cache");
cacheField.setAccessible(true);
Integer[] cache;
cache = new Integer[(high - low) + 1];
int j = low;
for (int k = 0; k < cache.length; k++)
cache[k] = Integer.valueOf(j++);
cacheField.set(null, cache);
highField.set(null, high);
lowField.set(null, low);
}
Működik? Nem! Bummer. Mit mond?
java.lang.IllegalAccessException: Can not set static final [Ljava.lang.Integer; field java.lang.Integer$IntegerCache.cache to [Ljava.lang.Integer;
at sun.reflect.UnsafeFieldAccessorImpl.throwFinalFieldIllegalAccessException(UnsafeFieldAccessorImpl.java:73)
at sun.reflect.UnsafeFieldAccessorImpl.throwFinalFieldIllegalAccessException(UnsafeFieldAccessorImpl.java:77)
at sun.reflect.UnsafeQualifiedStaticObjectFieldAccessorImpl.set(UnsafeQualifiedStaticObjectFieldAccessorImpl.java:77)
at java.lang.reflect.Field.set(Field.java:680)
... itt még folytatódik a stack trace, nem érdekes
Miért nem megy? Hát megadtuk, hogy setAccessible(true), hozzá kellene engednie!
Set the accessible flag for this object to the indicated boolean value. A value of true indicates that the reflected object should suppress Java language access checking when it is used. A value of false indicates that the reflected object should enforce Java language access checks.
Hát bizony ennek engednie kellene, és pár perc guglizás után ki is derül, hogy nem a private-tal van baja, hanem a final-lal. Az ugyanis nem hozzáférési jogosultság kérés, hanem egyszerűen a final az végleges, azt nem lehet átállítani. Persze, ami ott van a memóriában, ahhoz hozzá lehet férni, és aki elszánt, azt meg nem lehet állítani. Azért, mert a reflection Field osztályának set metódusa nem akarja átírni a final mezőket azért még ne adjuk fel. Verjük át a reflection osztályt, hitessük el vele, hogy ez a mező nem final.
Hogyan? Hát reflection-nel!
public static void setIntegerCacheLimits(int low, int high)
throws NoSuchFieldException, SecurityException,
IllegalArgumentException, IllegalAccessException {
Class<?>[] classes = Integer.class.getDeclaredClasses();
Class<?> cacheClass = null;
for (Class<?> klass : classes) {
if (klass.getSimpleName().equals("IntegerCache")) {
cacheClass = klass;
}
}
Field lowField = cacheClass.getDeclaredField("low");
lowField.setAccessible(true);
Field highField = cacheClass.getDeclaredField("high");
highField.setAccessible(true);
Field cacheField = cacheClass.getDeclaredField("cache");
cacheField.setAccessible(true);
Integer[] cache;
cache = new Integer[(high - low) + 1];
int j = low;
for (int k = 0; k < cache.length; k++)
cache[k] = Integer.valueOf(j++);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(cacheField, cacheField.getModifiers()
& ~Modifier.FINAL);
cacheField.set(null, cache);
modifiersField.setInt(highField, highField.getModifiers()
& ~Modifier.FINAL);
highField.set(null, high);
modifiersField.setInt(lowField, lowField.getModifiers()
& ~Modifier.FINAL);
lowField.set(null, low);
}
Mit is csinálunk? Elkérjük reflection-nel a java.lang.reflect.Field osztály modifiers mezőjét, és az átállítani kívánt három mező esetében átállítjuk a FINAL bitet. (Sajnos a java.lang.reflect.Field osztályban a modifiers mező nem final, pedig lehetne az, és ezért ezt a trükköt meg lehet lépni.)
Innen kezdve a reflection azt hiszi, hogy ez a három mező az IntegerCache osztályban nem final és hajlandó asszisztálni abban, hogy tűkön lőjük magunkat. A metódus hiba nélkül lefut, öröm és boldogság…
… egészen addig, míg meg nem látjuk a kis teszt programunknak
package tifyty;
import java.lang.reflect.Field;
import org.junit.Test;
public class TestSetIntegerCache {
@Test
public void testSetIntegerCache() throws NoSuchFieldException,
SecurityException, IllegalArgumentException, IllegalAccessException {
SetIntegerCache.setIntegerCacheLimits(-3000, 3000);
Class<?>[] classes = Integer.class.getDeclaredClasses();
Class<?> cacheClass = null;
for (Class<?> klass : classes) {
if (klass.getSimpleName().equals("IntegerCache")) {
cacheClass = klass;
}
}
Field lowField = cacheClass.getDeclaredField("low");
lowField.setAccessible(true);
Field highField = cacheClass.getDeclaredField("high");
highField.setAccessible(true);
System.out.println("low =" + lowField.getInt(null));
System.out.println("high=" + highField.getInt(null));
for (int i = -3001; i < 3002; i += 1000) {
Integer a = i;
Integer b = i;
System.out.println((a == b ? "==" : "!=") + i + "\n");
}
}
}
if (i >= IntegerCache.low && i <= IntegerCache.high)
sor, debuggolom, a low át van írva, a high át van írva, i a kettő között van, és mégis, mégis (sírógörcs, csodálkozás, és ámulás) az egész kifejezés értéke false, új Integer objektum jön létre.
Ilyenkor jön az, hogy elő a bájt kóddal. Csak oroszlánszívűeknek.
$ mkdir tmp
$ cd tmp
$ jar xvf /Library/Java/JavaVirtualMachines/1.7.0.jdk/Contents/Home/jre/lib/rt.jar
$ javap -c java/lang/Integer.class >Integer.txt
$ less Integer.txt
Mit látunk a 23. sorban? Azt, hogy bipush -128. Ezt a final értéket bizony a javac befordította a kódba. Hiába létezik az IntegerCache osztályban a statikus low változó (mert létezik, aki nem hiszi, fejtse vissza azt is, és látni fogja, hogy statikus initializer a
50: bipush -128
52: istore_2
sorokkal tárolja ezt az értéket). Hiába, mert a valueOf(int) NEM olvassa azt a változót. Minek is olvasná? Az konstans 127, nem is lehet más, hiszen final. És ha bármilyen final változót átállítunk reflection-nel, akkor fel lehetünk rá készülve, hogy bárhol a kódban, ha valamelyik programrészlet, JIT által generált kód darab, szál, vagy akár egy tündér, vagy törp tartogat magánál egy másolatot, eszébe nem fog jutni megnézni soha, hogy esetleg megváltozott-e a megváltoztathatatlan.
Ezért kellene, hogy a Field osztályban a modifiers mező is final legyen. És nem csak azért nem írtam bele az előző szavazásba, hogy oldjuk meg reflection-nel, mert féltem, hogy valaki arra szavaz, hanem, mert nem lehet. Meg nem is kell.
Tudom, rég volt, de most találtam: http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.5.3
final fields can be changed via reflection […] If a final field is initialized to a compile-time constant expression (§15.28) in the field declaration, changes to the final field may not be observed, since uses of that final field are replaced at compile time with the value of the constant expression.
Miért érzem úgy, hogy a következő rész az ASM-ről vagy a Javassist-ról fog szólni?
Visszajelzés:final, fin ül « tifyty
Tudom, rég volt, de most találtam:
http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.5.3
final fields can be changed via reflection […] If a final field is initialized to a compile-time constant expression (§15.28) in the field declaration, changes to the final field may not be observed, since uses of that final field are replaced at compile time with the value of the constant expression.