The Programmers Guide To Kotlin - Annotation & Reflection
Written by Mike James   
Monday, 08 January 2018

Kotlin supports Java-like annotations but they don't really make much sense without the help of reflection. So let's take a look at both of them and see how they work together.

 

Annotations

Annotations are a strange idea when you first meet them.

At their simplest you can regard them as instructions to the compiler. For example. you can change the name of the default class used to host functions that are not explicitly declared as methods using an annotation:

@file:JvmName("class name")

This is an instruction to the compiler to change the name and you cannot hope to implement custom annotations with a similar power unless you are prepared to modify the compiler.

There are a few standard annotations and you can create your own custom annotations, although as indicated these don't integrate with the compiler in quite the same way.

Custom annotations are generally used to pass information either to another programmer or to the code itself.

For example the @Deprecated annotation marks a function as being one to avoid using because it is going to be removed in the future:

@Deprecated("Do not Use")
fun div(a:Int,b:Int):Int=a/b

If you try to use the deprecated function then the compiler will show that there is a problem by showing it in struck out and display a suitable message:

In this case you can see that the annotation is of use to any programmer building a library for others to use. It communicates information about how the function should be used to other programmers. Some annotations also indicate conditions to code that makes use of it, but to follow how this works we need to know about reflection and so this topic is deferred until later in the chapter.

Annotations can be applied to classes, functions, properties and so on.

There is a particular problem with using annotations in Kotlin because of the need to remain compatible with Java annotations. The problem is that Kotlin doesn't always expose all of the equivalent Java elements that an annotation may be applied to, and you may well want to annotate these “hidden” elements.

To get around this problem Kotlin has some rules for writing annotations:

To apply an annotation to a primary constructor you have to use the constructor keyword in the declaration:

class MyClass @Deprecated("Do not use")
constructor(val MyProp:Int)

You can annotate a lambda by writing the annotation in front of the opening curly bracket:

@Deprecated("Do not use") { rest of lambda}

To attach an Annotation to a Java entity you have to specify the entity before writing the annotation. The supported entities are:

  • file

  • property (annotations with this target are not visible to Java)

  • field

  • get (property getter)

  • set (property setter)

  • receiver (receiver parameter of an extension function or property)

  • param (constructor parameter)

  • setparam (property setter parameter)

  • delegate (the field storing the delegate instance for a delegated property)

So for example to annotate the setter of a Kotlin property you would use:

class MyClass{
 @set:Deprecated("Do Not Use")
 var myProp
}

You can also set multiple annotations on an entity by following the entity name by a list of annotations in square brackets.

Annotations are useful when you want to associate data, usually referred to as meta data, to your code. To do this you have to create custom annotation classes and to make use of them you have to resort to reflection.

So first we need to look at reflection and then return to custom annotations.

Reflection

Reflection is an advanced technique that gives you access to things that you don't normally have access to.

You can use it to find out details of the classes and their methods and properties – things that you might suppose would not be accessible at runtime. As you might expect, Kotlin's reflection features are somewhat different to Java, but as always there are ways of working with Java reflection.

The most basic form of reflection is to get an object, an instance of KClass, that corresponds to a Kotlin class.

To retrieve the class as a KClass object all you have to do is:

val classobj=MyClass::class

What you do next is to access any of the class details you care to, using the methods and properties of the classobj.

For example suppose MyClass is:

class MyClass{
    fun myMethod1(){}
    fun myMethod2(){}
}

Then you can discover what member functions the class has using:

val classobj=MyClass::class
for(m in classobj.declaredMemberFunctions){
  println(m.name)
}

This lists the methods by name. Methods and functions in general are represented by KCallable objects.

You can do some things you might not expect to be able to do in a statically typed language like Kotlin (or Java).

For example, once you have the KClass object you can create an instance:

var myObject=classobj.createInstance()

and you can call methods on that instance:

myObject.myMethod1()

You can even call a method that you have obtained by reflection. The only thing you need to remember is that you have to supply the parameters correctly and the first parameter is this i.e. the call context or receiver.

For example:

for(m in classobj.declaredMemberFunctions){
        println(m.name)
        m.call(myObject)
    }

The this that you pass has to be the correct type of object.

You can also get a KClass object from an instance of the class:

val c=myObject::class

Properties can also be manipulated and in this case the objects you need to look up are the KProperty and KMutableProperty classes.

We have already seen the use of the :: operator to get a reference to a function. It can also be used to get a reference to a bound method.

For example:

val myFunction=::myObject.myMethod

stores a reference to myMethod bound to myObject. Now when you call myFunction() it calls myMethod with this set to myObject.

You can also gain access to the Java reflection methods using the Kotlin KClass java property.

When you first meet reflection it seems to give you great power, but with great power come great bugs.

Use reflection sparingly if at all. Most of the difficulty is finding the method or property of the reflection classes that provides what you need. It may be obvious but it is worth saying that you cannot use reflection to change the definition of a class. You cannot add methods or properties at runtime. You can only inquire about and make use of members that were added at compile time.

kotlinlogo

Custom Annotations

Now that we have covered some aspects of reflection we can deal more easily with custom annotations.

It is very easy to create a custom annotation. All you need to is create a class with the modifier annotation:

annotation  class MyAnnotation

Now you have an annotation ready to use:

@MyAnnotation class MyClass{...}

All you can really do with such a simple annotation class is to check to see if some entity has the annotation at runtime using reflection.

For example the KClass object has an annotations property that returns a list of annotations:

val myClassObject = MyClass::class
for (a in myClassObject.annotations) {
      println(a.annotationClass.simpleName)
}

This just prints MyAnnotation.

The only complication is that an annotation can be attached to a class or any member of a class/object. You have to use reflection to retrieve an object that represents that entity, and then use annotations to retrieve its annotations.

If you just want to check that a single type of annotation has been applied then you can use findAnnotation which returns any annotation object of the specified type or null if none exist:

val myAnnotationObject=
   myClassObject.findAnnotation<MyAnnotation>()

Notice that as a nullable type you have to continue to check for null in the returned value, e.g.:

println(myAnnotationObject?.annotationClass
                                     ?.simpleName)

As well as simple bare annotations you can also include some data as Annotation classes can have a primary constructor.

For example, you might want to record the name of the programmer who created the entity:

annotation class MyAnnotation(
             val programmerName:String="Unknown")

By providing a default value, the user of the annotation doesn't have to supply the information:

@MyAnnotation("Mike") class MyClass {...}

You can retrieve the data as a property of the annotation class retrieved by either annotations or findAnnotation:

val myAnnotationObject=
   myClassObject.findAnnotation<MyAnnotation>()
println(myAnnotationObject?.programmerName)

There are also a range of annotations that you can apply to custom annotations:

  • @Target specifies the possible kinds of elements which can be annotated with the annotation (classes, functions, properties, expressions etc.);

  • @Retention specifies whether the annotation is stored in the compiled class files and whether it's visible through reflection at runtime (by default, both are true);

  • @Repeatable allows using the same annotation on a single element multiple times;

  • @MustBeDocumented specifies that the annotation is part of the public API and should be included in the class or method signature shown in the generated API documentation.

The one you are likely to use most often is @Target to restrict the range of entities that the annotation can be applied to. For example:

@Target(AnnotationTarget.CLASS) annotation class
  MyAnnotation(
    val programmerName:String="Unknown")

sets MyAnnotation as usable only on classes. If you try to use it on something other than a class you generate a compiler error message:

You can include multiple targets in the @Target annotation.

Annotations can include annotations as properties set in their primary constructors and things can become complicated. Kotlin annotations also work with Java annotations.

In general you need to think carefully about using annotations. They work well where you need to attach information to classes that isn't going to change at the instance level.

You also need to think about the two parts that make an annotation work. There is the annotation class itself which the programmer uses to attach data to the classes and other entities they create. Then there is the processing machinery that consumes the annotations. This machinery is usually part of a framework that the user is allowed to extend by adding classes which have to declare their characteristics to the existing classes. This is generally a big undertaking.

In short, custom annotations are not a facility that every project needs to undertake.

 Summary

  • Annotations are often used as instructions to the compiler.

  • Custom annotations are generally used to pass information either to another programmer or to the code itself.

  • Kotlin has some rules for writing annotations that attach to entities that are really only visible in Java.

  • Reflection is an advanced technique that gives you access to things that you don't normally have access to at runtime like class and methods.

  • You can also gain access to the Java reflection methods using the Kotlin KClass java property.

  • All you need to create custom annotations is a class with the modifier annotation

  • To work with annotations at runtime you have to use reflection to retrieve an object which represents that entity, and then use its annotations property to retrieve its annotations as an array.

  • There are a number of annotations which can be applied to a custom annotation to control how it is used.

 This article is an extract from: 

Programmer's Guide To Kotlin Third Edition

kotlin3e360

You can buy it from: Amazon

Contents

  1. What makes Kotlin Special
  2. The Basics:Variables,Primitive Types and Functions 
  3. Control
         Extract: If and When 
  4. Strings and Arrays
  5. The Class & The Object ***NEW!
  6. Inheritance
  7. The Type Hierarchy
  8. Generics
  9. Collections, Iterators, Sequences & Ranges
        Extract: Iterators & Sequences 
  10. Advanced functions 
  11. Anonymous, Lamdas & Inline Functions
  12. Data classes, enums and destructuring
        Extract: Destructuring 
  13. Exceptions, Annotations & Reflection
  14. Coroutines
        Extract: Coroutines 
  15. Working with Java
        Extract: Using Swing
  16. Compose Multiplatform
        Extract: Compose Layout 

<ASIN:B0D8H4N8SK>

To be informed about new articles on I Programmer, sign up for our weekly newsletter, subscribe to the RSS feed and follow us on Twitter, Facebook or Linkedin.

 

Banner


Data Wrangler Gets Copilot Integration
11/11/2024

Microsoft has announced that Copilot is being integrated into Data Wrangler. The move will give data scientists the ability to use natural language to clean and transform data, and to get help with fi [ ... ]



AI Breakthrough For Robot Surgery
17/11/2024

Using imitation learning, a robot has learned to perform surgical procedures as skillfully as human surgeons, bringing the field of robotic surgery closer to true autonomy.


More News

espbook

 

Comments




or email your comment to: comments@i-programmer.info

 <ASIN:1871962536>

<ASIN:1871962544>

Last Updated ( Monday, 08 January 2018 )