Kotlin is a powerful and expressive language that makes coding both enjoyable and efficient. One of the core concepts in Kotlin (and programming in general) is subtype relationships. Understanding how subtypes work in Kotlin can help you write cleaner, more flexible, and reusable code. In this blog post, we’ll break down this concept in an easy-to-understand manner with examples and explanations.
What Are Subtype Relationships in Kotlin?
In Kotlin, the type of a variable specifies the possible values it can hold. The terms “type” and “class” are sometimes used interchangeably, but they have distinct meanings. In the case of a non-generic class, the class name can be used directly as a type. For example, var x: String
declares a variable that can hold instances of the String
class. However, the same class name can also be used to declare a nullable type, such as var x: String?
which indicates that the variable can hold either a String
or null
. So each Kotlin class can be used to construct at least two types.
When it comes to generic classes, things get more complex. To form a valid type, you need to substitute a specific type as a type argument for the class’s type parameter. For example, List
is a class, not a type itself, but the following substitutions are valid types: List<Int>
, List<String?>
, List<List<String>>
, and so on. Each generic class can generate an infinite number of potential types.
Subtyping Concept in Kotlin
To discuss the relationship between types, it’s important to understand the concept of subtyping. Type B is considered a subtype of type A if you can use a value of type B wherever a value of type A is expected. For example, Int
is a subtype of Number
, but Int
is not a subtype of String
. Note that a type is considered a subtype of itself. The term “supertype” is the opposite of subtype: if A is a subtype of B, then B is a supertype of A.

B is a subtype of A if you can use it when A is expected
Understanding subtype relationships is crucial because the compiler performs checks whenever you assign a value to a variable or pass an argument to a function. For example:
fun test(i: Int) {
val n: Number = i
fun f(s: String) { /*...*/ }
f(i)
}
Storing a value in a variable is only allowed if the value’s type is a subtype of the variable’s type. In this case, since Int
is a subtype of Number
, the declaration val n: Number = i
is valid. Similarly, passing an expression to a function is only allowed if the expression’s type is a subtype of the function’s parameter type. In the example, the type Int
of the argument i
is not a subtype of the function parameter type String
, so the invocation of the f
function does not compile.
In simpler cases, subtype is essentially the same as subclass. For example, Int
is a subclass of Number
, so the Int
type is a subtype of the Number
type. If a class implements an interface, its type is a subtype of the interface type. For instance, String
is a subtype of CharSequence
.
Subtype Relationships in Nullable Types
Nullable types introduce a scenario where subtype and subclass differ. A non-null type is a subtype of its corresponding nullable type, but they both correspond to the same class.

A non-null type A is a subtype of nullable A?, but not vice versa
You can store the value of a non-null type in a variable of a nullable type, but not vice versa. For example:
val s: String = "abc"
val t: String? = s
In this case, the value of the non-null type String
can be stored in a variable of the nullable type String?
. However, you cannot assign a nullable type to a non-null type because null
is not an acceptable value for a non-null type.
The distinction between subclasses and subtypes becomes particularly important when dealing with generic types. This brings us back to the question from the previous section: is it safe to pass a variable of type List<String>
to a function expecting List<Any>
? We’ve already seen that treating MutableList<String>
as a subtype of MutableList<Any>
is not safe. Similarly, MutableList<Any>
is not a subtype of MutableList<String>
either.
A generic class, such as MutableList
, is called invariant on the type parameter if, for any two different types A
and B
, MutableList<A>
is neither a subtype nor a supertype of MutableList<B>
. In Java, all classes are invariant, although specific uses of those classes can be marked as non-invariant.
In List
, where the subtyping rules are different. The List
interface in Kotlin represents a read-only collection. If type A
is a subtype of type B
, then List<A>
is a subtype of List<B>
. Such classes or interfaces are called covariant.
Why Understanding Subtype Relationships Matters
Understanding subtype relationships in Kotlin allows you to:
- Write more reusable and maintainable code.
- Use polymorphism effectively.
- Work efficiently with interfaces and generics.
- Avoid type-related errors in large applications.
By leveraging inheritance, interfaces, and variance in generics, you can take full advantage of Kotlin’s type system and build flexible applications.
This is just a part..! Get the full insights here: [Main Article URL]
Conclusion
Subtype relationships in Kotlin are a fundamental (which form the backbone of object-oriented and generic programming) yet powerful concept that enables flexible, reusable, and type-safe code. Whether using class inheritance, interfaces, or variance in generics, understanding how subtypes work can help you write cleaner and more efficient Kotlin applications.
By mastering subtype relationships in Kotlin, you’ll unlock a deeper understanding of type hierarchy, improve your code structure, and avoid common pitfalls.