Home
Tools
Documents
Search
  • Pricing
  1. Home
  2. Blog
  3. Engineering
  4. Kotlin Generics INs & OUTs
09-09-22_kotlin-generics-ins-and-outs_blog-header

Kotlin Generics INs & OUTs

by Ivan Milisavljević

Generics might seem complicated, but there are ways to make them simpler. In this article, we take a look at Kotlin INs and OUTs and when to use which

Generics give us the ability to write flexible implementations with the same underlying behavior for many input types. In practice, we can create reusable implementations with the benefit of type safety by eliminating unforeseen outcomes and removing the need for explicit type casts.

Like all modern programming language, Kotlin has supported generics since it was created. Its implementation and supported features are similar to Java’s, but with remarkable improvements that make generics much easier to understand.

One of Kotlin’s notable improvements is its approach to describing wildcard types. It’s worth noting that Kotlin doesn’t actually support wildcard types, but instead has declaration-site and use-site variance (type projections). However, before we delve deeper into these, we should first cover some fundamental concepts required to understand these terms.

Type Parameters & Type Arguments

 

The primary constructs of generics are type parameters and type arguments. Type parameters serve as placeholders that are substituted with actual type arguments when a generic type is instantiated.

To get a better picture of what this means, consider the following example:

Variance & Java Wildcard Types

 

Variance explains the inheritance relationship between more complex types and their component types (sub-types).

Put simply, variance describes the relationship between classes that have the same base type but different type arguments. There are 4 types of variance:

1. Invariance

 

Where neither sub-type nor super-type could be assigned to a type in the same type hierarchy.

In simpler terms, “MyType” is not the same as “MyType

By default, all generic types in Java are invariant. This restricts the potential of generics. To overcome this limitation, Java uses a mechanism of upper and lower bound wildcard types.

2. Covariance: Upper Bounded Wildcards

 

Where we’re allowed to assign sub-types but not super-types.

To do this, we can create a type with a wildcard type argument that can be substituted by a defined type or anything that extends or implements that same type.

This basically defines the upper limit of allowed types in the Hierarchy Tree.

3. Contravariance: Lower Bounded Wildcards

 

Where we’re allowed to assign supertypes but no subtypes.

Similarly, as with covariance, we can create a type with a wildcard type argument that can be substituted by a specified type and all its parents, essentially defining the lower boundary of allowed items.

4. Bivariance: Unbounded Wildcards

 

Where we’re allowed to assign both subtypes and supertypes.

Java doesn’t have a specific implementation for bivariance. However, we can attain a comparable outcome with unbound wildcards (star projections in Kotlin).

This mechanism is unique and warrants a dedicated blog post, so we won’t discuss it in further detail here.

Kotlin Mixed-site Variance

 

Generics were a welcome addition to Java when they were introduced in 2004. Nevertheless, some currently regard them as unsuccessful, primarily due of type erasure, the fact that they are enforced only during compilation, difficult to comprehend, and somewhat awkward to write. For example, every time you want to use a bound wildcard, you must specify what kind of sub-typing behavior you need.

Specifying variance modifiers at usage places is known as use-site variance. It leads to unnecessary code duplication and expressiveness. The team over at JetBrains opted for a different approach. Instead of specifying wildcards every time, you declare them once, where the generic type is declared as “declaration-site variance.”

Declaration Site Variance

 

In Java, upper bound wildcards have a specific restriction preventing the invocation of any method that “consumes” type parameters. This is why such methods are deemed unsafe. Let’s look at the following example to understand why:

It’s safe to read from these lists because we can just upcast them to Dog, but we can’t add any of the items, since the compiler doesn’t recall the type contained in the list.

We can observe that Java upper bound wildcards only permit us to “produce” values, but not consume them.

With Kotlin declaration site variance, we can achieve the same outcome with greater ease.

Similarly, Java prohibits the usage of anything that returns type parameters for contravariance, but it’s okay to “consume” them.

The IN modifier can be utilized to indicate the type parameters that are permissible to be consumed.

Use Site Variance

 

In certain cases, you don’t have access to a generic type, or the type might not be either covariant, nor contravariant thereby rendering declaration site variance unusable. For this purpose, Kotlin supports use-site variance, which works similarly to declaration site variance.

Summary

 

Generics might seem a bit complicated, but luckily, we can follow a couple of rules to make our lives easier.

When To Use OUT?

 
  • When you have a function that’s “producing” a generic type parameter.
  • When you want to assign a subtype to a supertype or, in other words, when you want to achieve the same behavior as extend Dog in Java.
  • When you want to restrict read-only usage on your type.

When To Use IN?

 
  • When you have a function that’s only “consuming” a generic type parameter.
  • When you want to assign a supertype to a subtype when you want to achieve the same behavior as <? super Dog> in Java.
  • When you want to restrict write-only usage on your type.

Did you enjoy this article? Be sure to check out the Smallpdf Engineering blog for more!

1580510042420
Ivan Milisavljević
Staff Software Engineer @Smallpdf