• Templates
  • Pricing
Log In
  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 it 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, preventing unpredictable behaviors and removing the need for explicit type casts.

Like any modern language, Kotlin has supported generics since its inception. Implementation and supported features are similar to Java’s, with some groundbreaking improvements that make generics much easier to grasp.

One of the most significant changes Kotlin brought to the table is how we describe wildcard types. In fact, Kotlin has no support for wildcard types, but rather has declaration-site and use-site variance (type projections). However, before we do a deep dive into these, let’s cover some essential concepts required to understand these terms.

Type Parameters & Type Arguments

 

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

To get a better picture of what this means, let’s look at the following example:

Variance & Java Wildcard Types

 

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

In simpler terms, variance describes how classes with the same base type and different type arguments relate to each other. There are 4 types of variance:

Invariance

 

Where neither sub-type nor super-type could be assigned to 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 is very restrictive and limits the possibilities of generics, so java supports a mechanism of upper and lower bounded wildcard types.

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 specified type or anything that extends or implements that same type.

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

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.

Bivariance: Unbounded Wildcards

 

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

Java doesn’t have a specific implementation for bivariance, but we can achieve something similar with unbounded wildcards (Star projections in Kotlin).

This is a very special mechanism that deserves a blog post on its own so we won’t go into more detail here.

Kotlin Mixed-site Variance

 

Generics were a welcome addition to Java when they were introduced in 2004. However, nowadays, many consider them a failure mainly because of type erasure, the fact they are enforced only at compile-time, tough to grasp, and cumbersome to write. For example, every time you want to use a bounded wildcard, you must specify what kind of sub-typing behavior you need.

Specifying variance modifiers at usage places is called use-site variance and leads to code duplication and expressiveness that’s not required. 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, “declaration-site variance.”

Declaration Site Variance

 

In Java, upper bounded wildcards have a specific restriction that prevents us from calling any method that “consumes” type parameters, which is why these methods are treated as unsafe. Let’s look at the following example to understand why:

It’s safe to read from these lists because they can be upcasted to Dog, but we can’t add any of the items because the compiler doesn’t remember what type is contained in the list.

We can notice that we can only return or “produce” values with Java upper bounded wildcards, but not consume them.

With Kotlin declaration site variance, we can achieve the same thing much simpler.

Similarly, Java prevents us from using anything that returns type parameters for contravariance, but it’s okay to “consume” them.

To mark type parameters that are okay to be consumed, we can use IN modifier

Use Site Variance

 

In some instances, you don’t have access to a generic type, or the type is neither covariant, nor contravariant, so you can’t use declaration site variance. For this purpose, Kotlin supports use-site variance that 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 <? extends Dog> in Java.
  • When you want to restrict to read-only usages 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 to write-only usages 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