`internal` láthatóság Kotlinban
Egy ideje játszadozom a Kotlin programozási nyelvvel (aminek az 1.0-s kiadása lassan ünnepli az egyéves kort), s az utóbbi időben elkezdtem gondolkozni azon, vajon hogyan valósították meg az internal modifiert a JVM-en.
Olyan szempontból érdekelt a kérdés, hogy a Java nyelvben jelenleg nem szerepel az internal modifier (legjobb tudomásom szerint nincs is hasonló tervben - igen, Jigsaw, lásd később), s én nem is tudnám hirtelen leimplementálni a JVM-re. Ami engem érdekelt, hogy a JetBrainsnél sikerült-e ezt beleerőszakolni a JVM-be, s ha igen, hogyan.
Hogy mit is csinál ez a láthatóság? Idézem a Kotlin dokumentáció megfelelő sorait:
The internal visibility modifier means that the member is visible with the same module. More specifically, a module is a set of Kotlin files compiled together:
- an IntelliJ IDEA module;
- a Maven or Gradle project;
- a set of files compiled with one invocation of the Ant task.
Ehhez képest a Java dokumentációja nem tesz még csak modulról sem említést, nemhogy ehhez hasonló láthatóságról:
Access Levels
Modifier | Class | Package | Subclass | World | |||
public | Y | Y | Y | Y | |||
protected | Y | Y | Y | N | |||
no modifier | Y | Y | N | N | |||
private | Y | N | N | N |
Sőt, jelenleg a Javaban még csak a modul fogalma sem teljesen definiált. Beszélünk Maven modulokról, Gradle projektekről, de ezek végeredményben csak egy adag, lefordított Java class fájlt eredményeznek - amit esetleg becsomagolunk egy jar, war vagy ear fájlba. Itt egy kicsit csúsztatok, mert a Java dokumentációja, de még maga a Java nyelv sem tekinthető a JVM specifikációjának - de ezt most nézzük el nekem, s higgyük el, hogy tényleg nincsenek ilyenek még a JVM-ben. :)
A Java 9, s a Project Jigsaw meghozza majd a Java világába is a modul fogalmát, s bizonyos értelemben a modulon kívülre, s a csak belső használatra szánt osztályok/metódusok megkülönböztetését. De:
- A Java 9 még nincs itt,
- a Kotlin Java 6-tal kompatibilis bátjkódot fordít,
- s a Jigsaw-ban kicsit kevésbé finoman lehet majd megszabni egy osztály/metódus láthatóságát.
A félreértések elkerülése végett, posztban én a Kotlin által definiált modul fogalmat használom. A legpontosabb, s legtöbb, amit a témában találtam, az ez JetBrains blogposzt. Röviden összefoglalva, két dolgot állít a fenti blogposzt:
- az internal láthatóságú függvények a Java public láthatóságának megfelelően kerülnek bele a lefordított végeredménybe,
- és hogy a metódus/osztály neve megváltozik (name mangling).
- ennek egy apró mellékhatása, hogy Java-ból, egy internal láthatóságú Kotlin metódust sem tudunk meghívni, még ha egy modulban vannak, akkor se.
Az fenti két megállapítás közül az első eléggé egyértelmű: a Kotlin specifikációja szerint az internal láthatóságú metódus/osztályt tágabb körben látja bármi, mint ami a JVM-en a második legkorlátozóbb láthatóságnál (protected) adott.
A második viszont érdekes kérdéseket vet fel:
- Miért van szükség arra, hogy megváltoztassuk a lefordított metódus/osztály nevét?
- S ha kitalálom valahogyan a nevét az osztálynak/metódusnak, akkor tudom-e egy más modulban lévő Java/Kotlin kódból hívni.
Az elsőre egyszerű válasz lehetne, hogy azért, változtatjuk meg a metódus nevét, mert nem akarom, hogy azok, akik használják az én modulomat, megtalálják egyszerű név alapján a metódust :). De nem pontosan ez a válasz, s ezt a kérdést a linkelt JetBrainses blogban részletesen tárgyalják.
Mikor elkezdtem vizsgálni, hogy mégiscsak tudom-e használni egy másik Java/Kotlin modulból az internal produktomomat, akkor érdekes eredményre jutottam: Kotlinból már példányosítani sem tudom az internal osztályomat, se meghívni egy internal metódust. Ez várható volt, hiszen a Kotlin fordító fel van készítve arra, hogy Kotlinból fordított osztályokkal dolgozzon - s el tud helyezni magának meta-információkat a fordított class fájlba, amit utána értelmezhez.
Azonban Javaból gond nélkül tudok példányosítani egy másik modulbeli osztályt - s egy kis trükkel, még egy internal metódusra is rá tudok hívni.
A kipróbálásra használt forráskód faék egyszerű:
Kicsit vágtam a képen, de nagyjából átlátható, remélem. Adott két modul: X és Y. Y modul Foo osztálya internal láthatósággal, s az X modulból ezt próbálom meg kétféleképpen (Kotlin/Java) is használni. Töltsd le Githubról, s nézd végig, ott részletesebben kommentáltam a kóddal kapcsolatos dolgokat.
Na, de nézzünk a motorháztető alá! A Foo osztály valahogy így néz ki:
internal class Foo {
init {
println("Foo constructor")
}
internal fun foo() {
println("Foo::foo method")
}
}
Ebből a következő Foo.class fájl fordul:
$ javap -c -p Foo.class
Compiled from "Foo.kt"
public final class Foo {
public final void foo$y_main();
Code:
0: ldc #8 // String Foo::foo method
2: astore_1
3: nop
4: getstatic #14 // Field java/lang/System.out:Ljava/io/PrintStream;
7: aload_1
8: invokevirtual #20 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
11: return
public Foo();
Code:
0: aload_0
1: invokespecial #25 // Method java/lang/Object."<init>":()V
4: ldc #27 // String Foo constructor
6: astore_1
7: nop
8: getstatic #14 // Field java/lang/System.out:Ljava/io/PrintStream;
11: aload_1
12: invokevirtual #20 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
15: return
}
Azt már a fájlnévből látjuk, hogy az osztály neve nem módosul - ez egyébként egy érdekes kérdés, miért nem oszt neki random nevet a Kotlin compiler, s én nem is tudnám megindokolni. De a metódus neve változik.
Ha egy, az Y modulon kívüli (de akár belüli) Java osztályban próbálom meghívni ezt a metódust, akkor az osztály lefordul, lefut.
Ami még érdekes lehet, hogy verbose módban a javap azt is megmutatja, hogy a Foo osztályra kerül egy kotlin.Metadata annotáció. Egy kicsit okosabb decompilerrel vissza lehet akár ezt is fordítani (vagy kézzel a constant pool table-ből kikeresve az értékeket), s tovább vizsgálni, mi is történhet a háttérben.
Összefoglalva: az internal láthatóság egy nagyon jó iránynak indul, valós problémákat oldana fel (khm, sun.misc.Unsafe, khm), s végre elfelejti a Javaból örökölt package alapú megoldást - aminek sok haszna nincs. Reményeim szerint a Java 9, s a Project Jigsaw után még csavarnak egyet ezen, s kapunk egy ténylegesen internal láthatóságot. Nem merném azt mondani, hogy jelenleg ez felesleges. De szerintem nem árt tisztában lenni azzal, hogy a JVM-en a Kotlin internal láthatósága nem jelent teljes védelmet.