Kotlin Reflection: Method and property references
Reflection in programming is a program's ability to introspect its own source code symbols at runtime. For example, it might be used to display all of a class’s properties, like in the displayPropertiesAsList
function below.
Reflection is often used by libraries that analyze our code and behave according to how it is constructed. Let's see a few examples.
Libraries like Gson use reflection to serialize and deserialize objects. These libraries often reference classes to check which properties they require and which constructors they offer in order to then use these constructors. Later in this chapter, we will implement our own serializer.
As another example, we can see the Koin dependency injection framework, which uses reflection to identify the type that should be injected and to create and inject an instance appropriately.
Reflection is extremely useful, so let's start learning how we can use it ourselves.
To use Kotlin reflection, we need to add the
kotlin-reflect
dependency to our project. It is not needed to reference an element, but we need it for the majority of operations on element references. If you seeKotlinReflectionNotSupportedError
, it means thekotlin-reflect
dependency is required. In the code examples in this chapter, I assume this dependency is included.
Hierarchy of classes
Before we get into the details, let's first review the general type hierarchy of element references.
Notice that all the types in this hierarchy start with the K
prefix. This indicates that this type is part of Kotlin Reflection and differentiates these classes from Java Reflection. The type Class
is part of Java Reflection, so Kotlin called its equivalent KClass
.
At the top of this hierarchy, you can find KAnnotatedElement
. Element is a term that includes classes, functions, and properties, so it includes everything we can reference. All elements can be annotated, which is why this interface includes the annotations
property, which we can use to get element annotations.
The next confusing thing you might have noticed is that there is no type to represent interfaces. This is because interfaces in reflection API nomenclature are also considered classes, so their references are of type KClass
. This might be confusing, but it is really convenient.
Now we can get into the details, which is not easy because everything is connected to nearly everything else. At the same time, using the reflection API is really intuitive and easy to learn. Nevertheless, I decided that to help you understand this API better I’ll do something I generally avoid doing: we will go through the essential classes and discuss their methods and properties. In between, I will show you some practical examples and explain some essential reflection concepts.
Function references
We reference functions using a double colon and a function name. Member references start with the type specified before colons.
The result type from the function reference is an appropriate KFunctionX
, where X indicates the number of parameters. This type also includes type parameters for each function parameter and the result. For instance, the printABC
reference type is KFunction0<Unit>
. For method references, a receiver is considered another parameter, so the Complex::double
type is KFunction1<Complex, Complex>
.
Alternatively, you can reference methods on concrete instances. These are so-called bounded function references, and they are represented with the same type but without additional parameters for the receiver.
All the specific types representing function references implement the KFunction
type, with only one type parameter representing the function result type (because every function must have a result type in Kotlin).
Now, what can we do with function references? In a previous book from this series, Functional Kotlin, I showed that they can be used instead of lambda expressions where a function type is expected, like in the example below, where function references are used as arguments to filterNot
, map
, and reduce
.
Using function references where a function type is expected is not "real" reflection because, under the hood, Kotlin transforms these references to lambda expressions. We use function references like this only for our own convenience.
Formally, this is possible because types that represent function references, like KFunction2<Int, Int, Int>
, implement function types; in this example, the implemented type is (Int, Int) -> Int
. So, these types also include the invoke
operator function, which lets the reference be called like a function.
The KFunction
by itself includes only a few properties that let us check some function-specific characteristics:
isInline: Boolean
-true
if this function isinline
.isExternal: Boolean
-true
if this function isexternal
.isOperator: Boolean
-true
if this function isoperator
.isInfix: Boolean
-true
if this function isinfix
.isSuspend: Boolean
-true
if this is a suspending function.
KCallable
has many more properties and a few functions. Let's start with the properties:
name: String
- The name of this callable as declared in the source code. If the callable has no name, a special invented name is created. Here are some atypical cases:- constructors have the name "
", - property accessors: the getter for a property named "foo" will have the name "
"; similarly the setter will have the name " ".
- constructors have the name "
parameters: List<KParameter>
- a list of references to the parameters of this callable. We will discuss parameter references in a dedicated section.returnType: KType
- the type that is expected as a result of this callable call. We will discuss theKType
type in a dedicated section.typeParameters: List<KTypeParameter>
- a list of generic type parameters of this callable. We will discuss theKTypeParameter
type in the section dedicated to class references.visibility: KVisibility?
- visibility of this callable, ornull
if its visibility cannot be represented in Kotlin.KVisibility
is an enum class with valuesPUBLIC
,PROTECTED
,INTERNAL
, andPRIVATE
.isFinal: Boolean
-true
if this callable isfinal
.isOpen: Boolean
-true
if this function isopen
.isAbstract: Boolean
-true
if this function isabstract
.isSuspend: Boolean
-true
if this is a suspending function (it is defined in bothKFunction
andKCallable
).
KCallable
also has two methods that can be used to call it. The first one, call
, accepts a vararg number of parameters of type Any?
and the result type R
, which is the only KCallable
type parameter. When we call the call
method, we need to provide a proper number of values with appropriate types, otherwise, it throws IllegalArgumentException
. Optional arguments must also have a value specified when we use the call
function.
The second function, callBy
, is used to call functions using named arguments. As an argument, it expects a map from KParameter
to Any?
that should include all non-optional arguments.
Parameter references
The KCallable
type has the parameters
property, with a list of references of type KParameter
. This type includes the following properties:
index: Int
- the index of this parameter.name: String?
- a simple parameter name, ornull
if the parameter has no name or its name is not available at runtime. Examples of nameless parameters include athis
instance for member functions, an extension receiver for extension functions or properties, and parameters of Java methods compiled without debug information.type: KType
- the type of this parameter.kind: Kind
- the kind of this parameter, which can be one of the following:VALUE
for regular parameters.EXTENSION_RECEIVER
for extension receivers.INSTANCE
for dispatch receivers, so instances needed to make member callable calls.
isOptional: Boolean
-true
if this parameter is optional, therefore it has a default argument specified.isVararg: Boolean
-true
if this parameter isvararg
.
As an example of how the parameters
property can be used, I created the callWithFakeArgs
function, which can be used to call a function reference with some constant values for the non-optional parameters of supported types. As you can see in the code below, this function takes parameters; it uses filterNot
to keep only parameters that are not optional, and it then associates a value with each of them . A constant value is provided by the fakeValueFor
function, which for Int
always returns 123
; for String
, it constructs a fake value that includes a parameter name (the typeOf
function will be described later in this chapter). The resulting map of parameters with associated values is used as an argument to callBy
. You can see how this callWithFakeArgs
can be used to execute different functions with the same arguments.
Property references
Property references are similar to function references, but they have a slightly more complicated type hierarchy.
All property references implement KProperty
, which implements KCallable
. Calling a property means calling its getter. Read-write property references implement KMutableProperty
, which implements KProperty
. There are also specific types like KProperty0
or KMutableProperty1
, which specify how many receivers property calls require:
- Read-only top-level properties implement
KProperty0
because their getter can be called without any receiver. - Read-only member or extension properties implement
KProperty1
because their getter needs a single receiver object. - Read-only member extension properties implement
KProperty2
because their getter needs two receivers: a dispatch receiver and an extension receiver. - Read-write top-level properties implement
KMutableProperty0
because their getter can be called without any receiver. - Read-write member or extension properties implement
KMutableProperty1
because their getter and setter need a single receiver object. - Read-write member extension properties implement
KMutableProperty2
because their getter and setter need two receivers: a dispatch receiver and an extension receiver.
Properties are referenced just like functions: use two colons before their name and an additional class name for member properties. There is no syntax to reference member extension functions or member extension properties; so, to show the KProperty2
example in the example below, I needed to find it in the class reference, which will be described in the next section.
The KProperty
type has a few property-specific properties:
isLateinit: Boolean
-true
if this property islateinit
.isConst: Boolean
-true
if this property isconst
.getter: Getter<V>
- a reference to an object representing a property getter.
KMutableProperty
only adds a single property:
setter: Setter<V>
- a reference to an object representing a property setter.
Types representing properties with a specific number of receivers additionally provide this property getter’s get
function, and mutable variants also provide the set
function for this property setter. Both get
and set
have an appropriate number of additional parameters for receivers. For instance, in KMutableProperty1
, the get
function expects a single argument for the receiver, and set
expects one argument for the receiver and one for a value. Additionally, more specific types that represent properties provide more specific references to getters and setters.
In the next part, we’ll cover class references and show how to implement object serialization. See you then!