DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Because the DevOps movement has redefined engineering responsibilities, SREs now have to become stewards of observability strategy.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Related

  • Testcontainers With Kotlin and Spring Data R2DBC
  • Filtering Java Collections via Annotation-Driven Introspection
  • Why You Should Migrate Microservices From Java to Kotlin: Experience and Insights
  • Mastering Spring: Synchronizing @Transactional and @Async Annotations With Various Propagation Strategies

Trending

  • Agentic AI Systems: Smarter Automation With LangChain and LangGraph
  • Proactive Security in Distributed Systems: A Developer’s Approach
  • How to Format Articles for DZone
  • Using Python Libraries in Java
  1. DZone
  2. Coding
  3. Languages
  4. Java-Friendly Kotlin: Default Arguments

Java-Friendly Kotlin: Default Arguments

By 
Dan Newton user avatar
Dan Newton
·
Jul. 13, 20 · Tutorial
Likes (1)
Comment
Save
Tweet
Share
12.3K Views

Join the DZone community and get the full member experience.

Join For Free

Kotlin functions and constructors can define default arguments, allowing calls to them to skip any argument that has a default value.

This allows the following function to be called in a number of ways:

Kotlin
xxxxxxxxxx
1
14
 
1
fun doStuff(
2
  a: String = "Default value",
3
  b: Int = 1,
4
  c: Boolean = false
5
)
6
7
doStuff()
8
doStuff("here's a value")
9
doStuff("here's a value", 2)
10
doStuff("here's a value", 2, true)
11
doStuff("here's a value", c = true)
12
doStuff(b = 2)
13
doStuff(c = true)
14
doStuff(b = 2, c = true)


More information can be found in the Kotlin documentation.

For the rest of this post, we will look at how you can include default arguments in your API while still providing excellent Java compatibility.

Trying to Call an Unfriendly Kotlin Function

You are not going to have a good time calling a function or constructor with default arguments from Java. The only way to call the function shown previously from Java is to provide every argument that it asks for:

Java
xxxxxxxxxx
1
 
1
doStuff("here's a value", 2, true);


Without some help, there is no way for Java to understand the concept of default arguments.

Applying the @JvmOverloads Annotation

The @JvmOverloads annotation can be applied to functions and constructors to tell the compiler to generate extra overloads depending on their default arguments. The overloads are placed in the compiled byte code. You can then execute them from Java as you would with any other function or constructor.

Let’s have a look at the KDoc for @JvmOverloads which has a precise declaration of how it generates overloads:

Kotlin
xxxxxxxxxx
1
12
 
1
/**
2
 * Instructs the Kotlin compiler to generate overloads for this function that 
3
 * substitute default parameter values.
4
 *
5
 * If a method has N parameters and M of which have default values, M overloads 
6
 * are generated: the first one takes N-1 parameters (all but the last one that 
7
 * takes a default value), the second takes N-2 parameters, and so on.
8
 */
9
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CONSTRUCTOR)
10
@Retention(AnnotationRetention.BINARY)
11
@MustBeDocumented
12
public actual annotation class JvmOverloads


In other words:

  • Starts at the last argument.
  • If the argument has a default value, an overload is generated without that argument, who uses the default value in its implementation.
  • If the penultimate argument has a default value, an overload without the last 2 arguments is generated.
  • Continues to apply this logic until it reaches the first argument or hits an argument that does not have a default value.

Some examples should help grasp this definition.

Below are the different ways to call the doStuff function from earlier in Java:

Kotlin
xxxxxxxxxx
1
 
1
// All arguments have default values
2
@JvmOverloads
3
fun doStuff(
4
  a: String = "Default value",
5
  b: Int = 1,
6
  c: Boolean = false
7
)
Java
xxxxxxxxxx
1
 
1
// Ways to call [doStuff] from Java
2
doStuff();
3
doStuff("here's a value");
4
doStuff("here's a value", 2);
5
doStuff("here's a value", 2, true);


There are now four options for Java to trigger (instead of 1 without the annotation). All the arguments have been removed, one by one, leaving 3 overloads where 1 requires no inputs at all.

It we remove one of the default values, then the generated options will change:

Kotlin
xxxxxxxxxx
1
 
1
// Remove default value from [b]
2
@JvmOverloads
3
fun doStuff(
4
  a: String = "Default value",
5
  b: Int,
6
  c: Boolean = false
7
)
Java
xxxxxxxxxx
1
 
1
// Ways to call [doStuff] from Java
2
doStuff("here's a value", 2);
3
doStuff("here's a value", 2, true);


Now, when creating overloads, the compiler hits b (second argument) and stops. It does not matter that a has a default value, the compiler will not progress from this point. Therefore, only a single extra overload is available to Java.

Applying the annotation to a class’ constructor requires a slight manipulation to its structure:

Kotlin
xxxxxxxxxx
1
 
1
class MyJvmOverloadsClass @JvmOverloads constructor(
2
  private val a: String = "Default value",
3
  private val b: Int = 1,
4
  private val c: Boolean = false
5
) {
6
7
  // Rest of the class
8
}


The annotation must be applied directly to one of the class’ constructor. In this case, as there is only a single constructor, the constructor keyword must be added (this can typically be omitted in a Kotlin class). Applying the annotation generates overloads in the same way that it does to a function.

The Annotation Isn’t Perfect

The issue with the @JvmOverloads annotation is that it does not generate every possible combination of arguments. Kotlin can bypass this as it has access to named arguments.

Java does not have this ability. Even with the @JvmOverloads annotation, this is what prevents Java from accessing all the same options as Kotlin.

It makes sense when you think about it.

A function or constructor can only be overloaded when it has different arguments from all other versions, for example (written in Kotlin but Java follows the same rules):

Kotlin
xxxxxxxxxx
1
 
1
fun doStuff(a: String = "Default value"): String
2
fun doStuff(a: String = "Default value", b: Int = 1): String
3
fun doStuff(a: String = "Default value", b: Int = 1, c: Boolean = false): String
4
// Return type can be different as long as it has different arguments
5
fun doStuff(a: String = "Default value", b: Int = 1, c: Boolean = false, d: Long = 2): Int


The difference between each overload is clear because the argument types are different. But if they were all the same type it becomes harder to use:

Kotlin
xxxxxxxxxx
1
 
1
fun doStuff(a: String = "Default value"): String
2
fun doStuff(a: String = "Default value", b: String = "Another value"): String
3
fun doStuff(a: String = "Default value", b: String = "Another value", c: String = "Another one"): String


Named arguments makes this bearable (assuming they are named better than a, b and c). Allowing you to omit some arguments due to their default values.

Without the use of named arguments, the is no way for the compiler to distinguish between the objects that you pass to this function. So all it can do is pass them in, one by one, in order, into the function. This is the problem that @JvmOverloads faces. It cannot generate all possible iterations because there is not enough information to differentiate the overloads from each other. If all the arguments have different types, then technically the compiler can do it, but having a rule that is loosely applied would become confusing.

This is the reason that @JvmOverloads only generates overloads starting from the last argument, who must also have a default value, and then moves onto the next (backwards towards the first argument).

Cooperating With the Annotation

You can work around the limitations of @JmvOverloads. Doing so requires you to think about how your functions will be used and what combinations of parameters will be frequently used.

Below are some points for you to consider when writing a function annotated with @JvmOverloads:

  1. Order the arguments in order of importance, the first as the most important and decreasing as they go on.

    Kotlin
    xxxxxxxxxx
    1
     
    1
    @JvmOverloads
    2
    fun doStuff(
    3
      superImportant: String = "I need this argument!",
    4
      reallyImportant: Int = 1,
    5
      somewhatImportant: Boolean = false,
    6
      dontReallyCareAboutThisOne: Long = 2
    7
    )
  2. Do not mix arguments with default values with ones that do not (easier to understand with the example).

    Kotlin
    xxxxxxxxxx
    1
    18
     
    1
    // Stops generating overloads when [doesNotHaveDefaultValue] is reached
    2
    @JvmOverloads
    3
    fun doStuff(
    4
      hasDefaultValue: String = "I need this argument!",
    5
      doesNotHaveDefaultValue: Int,
    6
      hasDefaultValue2: Boolean = false,
    7
      hasDefaultValue3: Long = 2
    8
    )
    9
    10
    11
    // No overloads can be generated
    12
    @JvmOverloads
    13
    fun doStuff(
    14
      hasDefaultValue: String = "I need this argument!",
    15
      doesNotHaveDefaultValue: Int,
    16
      hasDefaultValue2: Boolean = false,
    17
      doesNotHaveDefaultValue2: Long
    18
    )
  3. Manually create overloads if there are still combinations missing that you deem useful.

    Kotlin
    x
    16
     
    1
    // Original
    2
    @JvmOverloads
    3
    fun doStuff(
    4
      a: String = "I need this argument!",
    5
      b: Int = 1,
    6
      c: Boolean = false,
    7
      d: Long = 2
    8
    )
    9
    10
    // Calling with just [a] and [c] is useful
    11
    fun doStuff(a: String, c: Boolean) {
    12
      // Add in the missing parameters
    13
      // Copy the default values or maybe they are taken from properties of a class
    14
      // instead of being passed into the function
    15
      return doStuff(a, 1, c, 2)
    16
    }

The main piece of advice I want to give you here is to really think about the functions you create. When creating a public API as part of a library, especially when you want it to play well with Java, spending time considering how your functions will be leveraged will make everyone happier.

Developers consuming a Kotlin API when using the language themselves can use its features to get around potential problems in your code. Java, on the other hand, requires you to apply some of your brainpower to write functions that are friendly to use.

Summary

Adding the @JvmOverloads annotation to your functions and constructors persuades them to play nicer with Java. It does so by generating additional overloads and placing them in the bytecode that Java interacts with. Adding the annotation does not always make your API easily accessible to Java, in those situations, it is on you to put the work in and craft a well thought out API.


If you enjoyed this post or found it helpful (or both) then please feel free to follow me on Twitter at @LankyDanDev and remember to share with anyone else who might find this useful!

Kotlin (programming language) Java (programming language) Annotation

Published at DZone with permission of Dan Newton, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Testcontainers With Kotlin and Spring Data R2DBC
  • Filtering Java Collections via Annotation-Driven Introspection
  • Why You Should Migrate Microservices From Java to Kotlin: Experience and Insights
  • Mastering Spring: Synchronizing @Transactional and @Async Annotations With Various Propagation Strategies

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!