Riflessione con Kotlin

1. Introduzione

Reflection è il nome della capacità di ispezionare, caricare e interagire con classi, campi e metodi in fase di esecuzione. Possiamo farlo anche quando non sappiamo cosa siano in fase di compilazione.

Questo ha un gran numero di usi, a seconda di cosa stiamo sviluppando. Ad esempio, framework come Spring ne fanno un uso intensivo.

Il supporto per questo è integrato nella JVM e quindi è implicitamente disponibile per tutti i linguaggi basati su JVM. Tuttavia, alcuni linguaggi JVM hanno un supporto extra oltre a quello già disponibile.

2. Java Reflection

Tutti i costrutti Java Reflection standard sono disponibili e funzionano perfettamente con il nostro codice Kotlin . Ciò include la classe java.lang.Class e tutto ciò che è contenuto nel pacchetto java.lang.reflect .

Se vogliamo utilizzare le API Java Reflection standard per qualsiasi motivo, possiamo farlo esattamente nello stesso modo in cui lo faremmo in Java. Ad esempio, per ottenere un elenco di tutti i metodi pubblici in una classe Kotlin dovremmo fare:

MyClass::class.java.methods

Questo si suddivide nei seguenti costrutti:

  • MyClass :: class ci fornisce la rappresentazione della classe Kotlin per la classe MyClass
  • .java ci fornisce l' equivalente java.lang.Class
  • .methods è una chiamata al metodo di accesso java.lang.Class.getMethods ()

Funzionerà esattamente allo stesso modo sia che venga chiamato da Java o Kotlin, sia che venga chiamato su una classe Java o Kotlin . Ciò include costrutti specifici di Kotlin, come le classi di dati.

data class ExampleDataClass( val name: String, var enabled: Boolean) ExampleDataClass::class.java.methods.forEach(::println)

Kotlin converte anche i tipi restituiti nelle rappresentazioni di Kotlin.

In quanto sopra, otteniamo un kotlin.Array su cui possiamo chiamare forEach ().

3. Miglioramenti alla riflessione di Kotlin

Sebbene possiamo utilizzare le API Java Reflection standard, non è a conoscenza di tutte le estensioni che Kotlin porta sulla piattaforma .

Inoltre, a volte può essere un po 'scomodo da usare in alcune situazioni. Kotlin offre la propria API di riflessione che possiamo utilizzare per risolvere questi problemi.

Tutti i punti di ingresso nell'API Kotlin Reflection utilizzano i riferimenti. In precedenza, abbiamo visto l'uso di :: class per fornire un riferimento alla definizione della classe. Potremo anche usarlo per ottenere riferimenti a metodi e proprietà.

3.1. Riferimenti alle classi Kotlin

L'API Reflection di Kotlin consente l'accesso a un riferimento di classe. Questo può quindi essere utilizzato per esaminare i dettagli completi della classe Kotlin . Questo dà accesso al riferimento alla classe Java - l' oggetto java.lang.Class - ma anche a tutti i dettagli specifici di Kotlin.

L'API Kotlin per i dettagli della classe è incentrata sulla classe kotlin.reflect.KClass . È possibile accedervi utilizzando l' operatore :: da qualsiasi nome di classe o istanza , ad esempio String :: class.

In alternativa, è possibile accedervi utilizzando il metodo di estensione java.lang.Class.kotlin se è disponibile un'istanza di classe Java :

val listClass: KClass = List::class val name = "Baeldung" val stringClass: KClass = name::class val someClass: Class val kotlinClass: KClass = someClass.kotlin

Una volta ottenuto un oggetto KClass , ci sono alcune semplici cose che può dirci sulla classe in questione . Alcuni di questi sono concetti Java standard e altri sono concetti specifici di Kotlin.

Ad esempio, possiamo facilmente scoprire se una Classe è Astratta o Finale, ma possiamo anche scoprire se la Classe è una Classe Dati o una Classe Companion:

val stringClass = String::class assertEquals("kotlin.String", stringClass.qualifiedName) assertFalse(stringClass.isData) assertFalse(stringClass.isCompanion) assertFalse(stringClass.isAbstract) assertTrue(stringClass.isFinal) assertFalse(stringClass.isSealed)

Abbiamo anche modi per muoverci nella gerarchia delle classi. In Java, possiamo già passare da una classe alla sua superclasse, alle interfacce e alla classe esterna in cui è racchiusa, se appropriato.

Kotlin aggiunge a questo la possibilità di ottenere il Companion Object per una classe arbitraria e l' istanza Object per una classe Object:

println(TestWithCompanion::class.companionObject) println(TestWithCompanion::class.companionObjectInstance) println(TestObject::class.objectInstance)

Possiamo anche creare nuove istanze di una classe da un riferimento di classe , più o meno allo stesso modo di Java:

val listClass = ArrayList::class val list = listClass.createInstance() assertTrue(list is ArrayList)

In alternativa, possiamo accedere ai costruttori e usarne uno esplicito se necessario. Questi sono tutti riferimenti a metodi come discusso nella sezione successiva.

In un modo molto simile, possiamo accedere a tutti i metodi, proprietà, estensioni e altri membri della classe:

val bigDecimalClass = BigDecimal::class println(bigDecimalClass.constructors) println(bigDecimalClass.functions) println(bigDecimalClass.memberProperties) println(bigDecimalClass.memberExtensionFunctions)

3.2. Riferimenti al metodo Kotlin

Oltre a poter interagire con le classi, possiamo anche interagire con metodi e proprietà .

Ciò include le proprietà della classe, definite con val o var , metodi di classe standard e funzioni di primo livello. Come prima, funziona altrettanto bene sul codice scritto in Java standard come sul codice scritto in Kotlin.

Esattamente allo stesso modo delle classi, possiamo ottenere un riferimento a un metodo o una proprietà utilizzando l' operatore :: .

Questo sembra esattamente lo stesso di Java 8 per ottenere un riferimento al metodo e possiamo usarlo esattamente allo stesso modo. Tuttavia, in Kotlin questo metodo di riferimento può essere utilizzato anche per ottenere informazioni di riflessione sul bersaglio.

Una volta ottenuto un riferimento al metodo, possiamo chiamarlo come se fosse davvero il metodo in questione . Questo è noto come riferimento richiamabile:

val str = "Hello" val lengthMethod = str::length assertEquals(5, lengthMethod())

We can also get more details about the method itself, in the same way, that we can for classes. This includes both standard Java details as well as Kotlin specific details such as if the method is an operator or if it's inline:

val byteInputStream = String::byteInputStream assertEquals("byteInputStream", byteInputStream.name) assertFalse(byteInputStream.isSuspend) assertFalse(byteInputStream.isExternal) assertTrue(byteInputStream.isInline) assertFalse(byteInputStream.isOperator)

In addition to this, we can get more information about the inputs and outputs of the method through this reference.

This includes details about the return type and the parameters, including Kotlin specific details – such as nullability and optionality.

val str = "Hello" val method = str::byteInputStream assertEquals( ByteArrayInputStream::class.starProjectedType, method.returnType) assertFalse(method.returnType.isMarkedNullable) assertEquals(1, method.parameters.size) assertTrue(method.parameters[0].isOptional) assertFalse(method.parameters[0].isVararg) assertEquals( Charset::class.starProjectedType, method.parameters[0].type)

3.3. Kotlin Property References

This works exactly the same for Properties as well, though obviously, the details that can be obtained are different. Properties instead can inform us if they are constants, late initialized or mutable:

lateinit var mutableProperty: String val mProperty = this::mutableProperty assertEquals("mutableProperty", mProperty.name) assertTrue(mProperty.isLateinit) assertFalse(mProperty.isConst) assertTrue(mProperty is KMutableProperty)

Note that the concept of Properties also works in any non-Kotlin code. These are identified by fields that follow the JavaBeans conventions regarding getter and setter methods.

This includes classes in the Java standard library. For example, the Throwable class has a Property Throwable.message by virtue of the fact that there is a method getMessage() defined in it.

We can access the actual Property through Method references that are exposed – the getter and setter methods. The setter is only available if we are working with a KMutableProperty – i.e. the property was declared as var, whereas the getter is always available.

These are exposed in an easier to use way via the get() and set() methods. The getter and setter values are actual method references, allowing us to work with them exactly the same as any other method reference:

val prop = this::mutableProperty assertEquals( String::class.starProjectedType, prop.getter.returnType) prop.set("Hello") assertEquals("Hello", prop.get()) prop.setter("World") assertEquals("World", prop.getter())

4. Summary

Questo articolo offre una panoramica di alcune delle cose che possono essere ottenute con la riflessione in Kotlin, incluso il modo in cui interagisce e differisce dalle capacità di riflessione incorporate nel linguaggio Java standard.

Tutti gli esempi sono disponibili su GitHub.