I had an interesting conversation with a colleague recently about the use of annotations in Java/Kotlin code. He had worked in organizations where the use of some language features (in this case, meta-programming via annotations) was discouraged as being "too magical" and "difficult to understand." While it's reasonable to choose to avoid some features of a framework or language because they are generally considered harmful, one developer's "magic" is another developer's everyday productivity tool.
I'll restate the objection to "magic" in a different way:
I don't understand this feature of the language, and therefore I choose to not use it--without regard to its intended usage or possible productivity benefits.
As developers, it's our job to understand both when and how to use the tools we have at our disposal to solve the problems we solve with software. Therefore, keeping an open mind to leverage the features of the language and frameworks we use can be a great productivity aid. This article covers some of the applications of annotations in Kotlin and Java and proposes a few ways to categorize the annotations you see so that you can infer their intended usage.
What are Annotations For?
At a high level, annotations give the compiler information about the code that it is compiling so that the programmer can change its behavior (which is why we use the term "metaprogramming") or to provide extra information about the running code at runtime. Some annotations can serve to generate compilation errors or warnings or even to generate more code for the compiler to compile when combined with annotation processors (which operate at compile time).
Because of this, the properties of the annotations can only be other annotations or values (primitives, strings, arrays of primitives/strings, classes).
What Generic Benefit do Annotations provide?
With annotations, you can write code that is more concise and declarative, and likely less error-prone. They can also enhance the readability of the code by indicating to the developer how the code should be used.
How are Annotations used?
As far as I can tell, their usages can be broken down into the following three categories (I'm using my own nomenclature):
- Markers (BINARY/CLASS)
- Enrichers (RUNTIME)
- Generators (SOURCE)
Markers
Markers are not very sexy, but they are still important. They both inform the reader (of the code) about the nature of the language element (think class, type, field/property. etc) that has been annotated and inform static analysis tools or the compiler about an annotated element. They always included in the class files (this is what is meant by the AnnotationRetention.BINARY
/RetentionPolicy.CLASS
enum) so that their usages are published with any jar file that is created from the code.
@Deprecated
in Java is a marker that is intended to tell a developer that this Element is going to go away, and writing any new code consuming that Element could possibly add more work for you in the future as you will be forced to migrate (usually upon a major version update). Some IDEs use this annotation to make it plain to the user that the element is deprecated. @Deprecated
in Kotlin has been enhanced to allow the developer to provide a @ReplaceWith
member that will allow some IDEs (IntelliJ/Android Studio) to intelligently replace deprecated code with non-deprecated code. But mostly, this is a tool for the reader of the code.
Another useful marker annotation in Java (even though it has RetentionPolicy.RUNTIME) is @Nonnull
(which is part of the outcome of JSR-305). If you use google's implementation, this annotation happens to be useful in Java/Kotlin interop because your Kotlin code will be able to tell that @Nonnull String foo
is String
in Kotlin and not String?
. This also aids in static analysis tooling (such as findbugs) so that null-checks may be skipped. In case you were wondering, this annotation has a companion in @Nullable
.
Enrichers
Enrichers add information to a language Element that is readable during runtime (thus, these annotations have AnnotationRetention.RUNTIME
/RetentionPolicy.RUNTIME
). Say, for example, that you were developing a custom XML serialization/deserialization library with the intention of avoiding your consumers needing to write custom serializer/deserializer classes. Then you could use a small set of annotations combined with a single serializer/deserializer that reads the information contained within the annotations on the Element(s) of the classes its intended to serialize to/from. There is a library that does this: http://simple.sourceforge.net/.
My personal favorite library that leverages runtime annotations is (Retrofit2)[https://square.github.io/retrofit/]. Retrofit2 uses reflection to generate Java Proxy objects capable of performing a network call based solely on your method declaration. For example,
public interface UserService {
@POST("users/new")
Call<User> createUser(@Body User user);
}
The @POST
and @Body
annotations are read at runtime when the call is made, and when this call is executed, retrofit will assemble a URL that corresponds to the endpoint in the @POST
annotation and create an OkHttp request that has some serialized form of the User
object in the body of the request. (If you don't know how to configure retrofit to do this, I suggest reading the documentation).
That's pretty great, right? In particular on Android, AnnotationRetention.RUNTIME
/RetentionPolicy.RUNTIME
annotations have a cost: https://blog.nimbledroid.com/2016/02/23/slow-Android-reflection.html. So before you go wild using them for everything--at least on an Android project, be aware of the costs involved.
Generators
Both Kotlin and Java have annotation processing capability. Annotation processors run when your project is compiled (and thus, they alone have visibility into annotations with AnnotationRetention.SOURCE
/RetentionPolicy.SOURCE
). They can do things as simple as add notes that log output to the console or as complicated as generate code to be compiled. Generators are particularly applicable in situations where traditional reuse patterns break down, and are useful in eliminating boilerplate code.
For example, if you have ever implemented the builder pattern to make instantiation of a class that has many fields easier to understand and more self-documenting, then you're probably familiar with the boilerplate associated with it. You'll find yourself going through a copy-paste-change flow quite often. After 10 or 20 repetitions of this flow, you'll probably have made a mistake somewhere. Then, in order to write reasonable equals
, hashCode
, and toString
methods, you'll have to repeat that flow three more times. You'll also have to write a constructor that understands how to apply the builder to construct your object without accidentally assigning the wrong builder field to the wrong class field.
Enter Google's awesome AutoValue annotation processor: https://github.com/google/auto/blob/master/value/userguide/builders.md. This tool enables you to declare an abstract version of your class and an abstract version of the builder for the class, delegating the implementation details to the annotation processor. Additionally, the equals
, hashCode
, and toString
, methods and the constructor for the class are also generated.
Suppose you now have to add a field to the class. Instead of worrying about keeping the implementation of the constructor, equals,
hashCode, and
toString` methods up to date, you just have to declare an abstract method for the class and its builder. Next time you compile your code, the actual implementation will be updated.
But that's not all. AutoValue has been designed to be open for extension, and many AutoValue extensions have been introduced to increase your productivity.
Serialization is a natural extension of AutoValue, and quite a few extensions generate serializers/deserializers the objects of classes that are generated by AutoValue. I'll highlight auto-value-moshi in this article as a representative library because there is an additional Kotlin Annotation Processor called moshi-codegen that will operate on and generate Kotlin classes to serialize/deserialize your Kotlin objects to/from JSON.
Conclusion
It's important to learn how to apply the tools at your disposal to solve the problems that you face as a software developer. Rather than avoid some tools as "magic," you should apply a little learning to demystify them.
Annotations are a useful tool to increase your productivity in Kotlin/Java and increase the quality of your code when used appropriately. So the next time you see some Java/Kotlin that has an @
, I hope you'll be more capable of understanding
Top comments (0)