1. Overview

Coming from a Java background, we may be familiar with the Deprecated annotation and how it works in Java land.

In this tutorial, we’re going to see how Kotlin takes this simple deprecation idea from Java and takes it to a whole new level.

2. Deprecation

When it comes to deprecation in the JVM ecosystem, the lovely java.util.Date class is always an honorable mention with all of its deprecated methods!

To repeat this ancient mistake, let’s implement this date in Kotlin:

data class Date(val millisSinceEpoch: Long) {

    private val internal = LocalDateTime.ofInstant(Instant.ofEpochMilli(millisSinceEpoch), ZoneId.of("UTC"))

    fun monthNumber(): Int = internal.get(ChronoField.MONTH_OF_YEAR)
}

As shown above, the Date data class is supposed to encapsulate the date-time information in Kotlin. Here the monthNumber() method returns the month number of the current date:

val epoch = Date(0)
println(epoch.monthNumber()) // prints 1

After a while, to avoid the zero-based vs. one-based month number confusion, we’ll add a new method to return an enum instead of a number:

fun month(): Month = internal.month

Also, to prevent developers from using the monthNumber() method, we can deprecate it. To do that in Kotlin, we can annotate the method with the @Deprecated annotation:

@Deprecated("Use the new month() method")
fun monthNumber(): Int = internal.get(ChronoField.MONTH_OF_YEAR)

The message property is always mandatory in the Deprecated annotation. Now if we use the old and now deprecated method, the Kotlin compiler will warn us about the deprecation:

'monthNumber(): Int' is deprecated. Use the new month() method

As shown above, the compiler displays the message along with an auto-generated description of the deprecation.

3. Deprecation Levels

By default, the Kotlin compiler only prints a warning message if we use a deprecated method. This compiler action, however, can be configured via the level annotation property.

The default level is equal to Deprecation.WARNING. So the program will continue to compile but with annoying warning messages.

In addition to simple warnings, it’s possible to instruct the compiler to generate an error during compilation for deprecated methods. In order to do that, we can use DeprecationLevel.ERROR value:

@Deprecated("Use the new month() method", level = DeprecationLevel.ERROR)
fun monthNumber(): Int = internal.get(ChronoField.MONTH_OF_YEAR)

Now if we use the deprecated method, the Kotlin compiler fails with this error message:

Using 'monthNumber(): Int' is an error. Use the new month() method

It’s also possible to hide a deprecated method altogether as if the method doesn’t exist in the first place:

@Deprecated("Use the new month() method", level = DeprecationLevel.HIDDEN)
fun monthNumber(): Int = internal.get(ChronoField.MONTH_OF_YEAR)

When the level is the DeprecationLevel.HIDDEN, the compiler can’t even find the deprecated method:

Unresolved reference: monthNumber

Even though the compiler won’t allow us to use HIDDEN or ERROR methods, they are present at the bytecode level. Let’s check this argument by peeking at the generated bytecode using the javap:

$ javap -c -p -v com.baeldung.deprecation.Date
// truncated
 public final int monthNumber();
    descriptor: ()I
    flags: (0x1011) ACC_PUBLIC, ACC_FINAL, ACC_SYNTHETIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: getfield      #27     // Field internal:LLocalDateTime;
         4: getstatic     #33     // Field ChronoField.MONTH_OF_YEAR:LChronoField;
         7: checkcast     #35     // class TemporalField
        10: invokevirtual #41     // Method LocalDateTime.get:(LTemporalField;)I
        13: ireturn
    Deprecated: true

This enables us to pretend the method doesn’t exist at the compile-time while maintaining the binary compatibility. Put simply, already-compiled code will continue to successfully call such deprecated methods. However, the compiler won’t allow us to use these methods in new code.

4. Replacements

It’s even possible to specify the replacement for a deprecated method using the replaceWith annotation property. This value, if specified, represents a code fragment that should be used as a replacement for the deprecated API usage.

For instance, here we’re specifying the month() function as the replacement for the monthNumber():

@Deprecated("Use the new month() method", replaceWith = ReplaceWith("month()"))
fun monthNumber(): Int = internal.get(ChronoField.MONTH_OF_YEAR)

IDEs and other tools can use this information and automatically apply the replacement. For example, on IntelliJ IDEA:

Replace With

We’ll see that it suggests replacing the monthNumber() usage with the new month() function.

By default, the replacement expression is interpreted in the context of the symbol being used and can reference members of the enclosing class. To be more specific, when we say ReplaceWith(“month()”), we mean that month() should be called on the same object instance.

Despite being interpreted in the same context, the import statements in the file containing the deprecated method aren’t accessible for the replacement expression. Therefore, if we need any of those import statements, we should use the imports annotation property in ReplaceWith. For example:

companion object {

    @Deprecated("Use java.time instead", 
      replaceWith = ReplaceWith("LocalDateTime.now()", imports = ["java.time.LocalDateTime"]))
    fun now(): Date = Date(0)
}

Here we suggest using the LocalDateTime.now() instead of Date.now(). Also, since the LocalDateTime isn’t available in the current context, we’re specifying the required imports using the imports property — java.time.LocalDateTime in this case.

Now if we apply the replacement on IntelliJ IDEA, it’ll add the specified import in addition to replacing the deprecated function call:

imports

As mentioned earlier, the replaceWith property is there to provide better tooling opportunities, such as the tooling integration we saw in IntelliJ IDEA.

5. Flexible Targets

So far, we only deprecated the functions or methods in Kotlin. In addition to methods, we can apply deprecation to many other constructs in Kotlin.

For instance, we can deprecate instance variables:

@Deprecated("No longer valid")
private val zeroBased = true

It’s also possible to deprecate type aliases:

@Deprecated("Use plain string instead")
typealias DateFormat = String

Let’s take a look at the source code of the Deprecated annotation to see all the possible targets.

@Target(CLASS, FUNCTION, PROPERTY, ANNOTATION_CLASS, CONSTRUCTOR, PROPERTY_SETTER, PROPERTY_GETTER, TYPEALIAS)
public annotation class Deprecated(
    val message: String,
    val replaceWith: ReplaceWith = ReplaceWith(""),
    val level: DeprecationLevel = DeprecationLevel.WARNING
)

So, in addition to the mentioned targets, we can see that this annotation is also applicable to classes, other annotations, setters/getters, functions, and constructors.

6. Conclusion

In this tutorial, we saw how to deprecate different programming constructs in Kotlin. We also learned that we could control the level of deprecation and even suggest replacements for a particular deprecated construct.

As usual, all the examples are available over on GitHub.

guest
0 Comments
Inline Feedbacks
View all comments