Generics in Kotlin
Overview
Kotlin offers a rich set of features, among which generics stand out as a powerful tool for enhancing code flexibility and safety. Generics in Kotlin enable developers to write functions and classes that work seamlessly with various data types, making their code more versatile and reusable. In this article, we will explore generics in Kotlin, discussing their essential concepts and practical applications. Whether you're a novice programmer or a seasoned Kotlin developer, this guide will equip you with the knowledge and skills to harness the full potential of generics in your Kotlin projects and write more robust, adaptable, and efficient code.
Advantages of Generics in Kotlin
Generics in Kotlin bring significant benefits to your code:
Type Casting isn’t Required:
Generics in Kotlin remove the need for explicit type casting when working with objects. This means you can write code that naturally deals with specific types without casting them.
Type Safety:
When you use generics in Kotlin, your code enforces that it can handle only a single type of object at any given time. This restriction ensures that you won't mistakenly work with the wrong data type, increasing type safety.
Compile Time Checking:
Generics in Kotlin offer compile-time safety by rigorously checking the code for parameterized types at compile time. This careful examination prevents runtime errors related to type mismatches. It ensures that your code behaves as expected without unexpected type-related issues when executed.
Variance in Kotlin
Kotlin differs from Java in the sense that it makes arrays invariant by default. Similarly, Kotlin's generic types exhibit this invariance by default. However, this behavior can be controlled using the out and in keywords. Invariance, in this context, means that a generic function or class, once defined for a specific data type, cannot work with or yield a different data type.
It's essential to understand that Any serves as the supertype of all other data types in Kotlin. Variance can be categorized into two main types:
- Declaration-site variance(using in and out)
- Use-site variance: Type projection
Kotlin "out" and "in" Keywords
"out" Keyword
The "out" keyword in Kotlin provides a way to specify that a generic type can be assigned to a reference of any of its supertypes. This means you can retrieve (produce) values from this type, but you cannot insert (consume) them.
Let's illustrate this with an example:
Code:
In this class, we've used the out keyword to indicate that it can produce values of type T. Consequently, you can safely assign an instance of OutputContainer to a reference with a supertype of T:
Code:
Note:
It's important to note that if we hadn't employed the out modifier, the above assignment would lead to a compiler error. By using out, we maintain type safety while ensuring that values can be produced without violating type constraints.
"in" Keyword
The "in" keyword in Kotlin allows us to specify that a generic type can be assigned to a reference of its subtype. This means it can be used when values are consumed but not when they are produced.
Here's an example:
Code:
In this class, we've declared a stringifyValue() method that consumes a value of type T. Consequently, you can safely assign a reference of type InputContainer<Number> to a reference of its subtype, InputContainer<Int>:
Code:
Note:
It's important to note that if we hadn't used the in modifier in the class, the above assignment would result in a compiler error. By using in, we ensure that values can be consumed without violating type constraints and maintain type safety.
Covariance
Covariance indicates that substituting subtypes is permissible, whereas replacing with supertypes is not allowed. In other words, a generic function or class can accept subtypes of the data type it's originally defined for. For instance, a generic class designed for "Number" can handle "Int", but a generic class designed for "Int" cannot accommodate "Number". In Kotlin, you can implement this using the "out" keyword, as illustrated below:
Code:
To allow covariance directly, you append the out keyword to the declaration site. Here's an alternative code snippet that functions similarly:
Code:
In both cases, the out keyword enables covariance by indicating that the generic class can accept subtypes of the specified type while maintaining type safety.
Contracovariance
Contracovariance, or contravariance, involves the substitution of a supertype value with subtypes. This means that a generic function or class can accept supertypes of the data type it was initially defined for. For example, a generic class designed for "Number" cannot accept "Int", but a generic class designed for "Int" can accept "Number". In Kotlin, this behavior is implemented using the in keyword, as shown below:
Code:
In this contravariant scenario, you can see that a can be assigned a Storage<Animal>, even though it is originally declared as a Storage<Dog>. This reflects the contravariant behavior where supertypes can be accepted. However, assigning a Storage<Dog> to b, which is declared as a Storage<Animal>, results in a compilation error. This demonstrates that the original contravariant intention is maintained by the in keyword in Kotlin, ensuring that supertype values can be accepted while preserving type safety.
Type Projections
Type projections enable the copying of elements from an array of a specific type into an array of a more general type, such as Any. To achieve this and ensure the code compiles, we must annotate the input parameter with the out keyword. This annotation informs the compiler that the input argument can be of any type that is a subtype of Any.
Here's a Kotlin program that demonstrates the copying of elements from one array into another:
Code:
In this code, we have created a function, copyElements, that takes an array of any subtype of Any and a target array of type Any. It copies elements from the source array to the target array. In the main function, we demonstrate this by copying elements from an array of integers into an array of a more general Any type.
Output:
The out keyword allows type projections in the input parameter, facilitating the copying of elements across arrays with different element types while maintaining type safety.
Star Projections
When the specific type of a value is unknown, and the goal is to print all elements of an array without type constraints, we employ the star (*) projection in Kotlin.
Here's a Kotlin program illustrating the usage of star projections:
Code:
In this code, the displayArrayContents() function accepts an array with a star (*) projection, allowing it to print all the elements without requiring knowledge of their specific types.
Output:
The star projection offers flexibility when dealing with arrays of unknown types, making it possible to work with heterogeneous data without imposing strict type constraints.
Conclusion
Here are the key conclusions of this article on Generics in Kotlin:
- Generics in Kotlin provide type safety by enabling you to write code that is parameterized over types. This ensures that the compiler catches type-related errors at compile time, reducing runtime issues.
- Generics allow you to write code that works with various data types while maintaining strong typing, making your code more adaptable and reusable.
- Kotlin supports covariance and contravariance through the out and in keywords. Covariance allows subtypes to be used where supertypes are expected, while contravariance enables the use of supertypes in situations where subtypes are anticipated.
- Type projections, like star (*) projections, enable you to work with generic types when the specific type is unknown. This is useful when you need to handle data of varying types.
- You can create generic classes and functions in Kotlin. This allows you to write reusable and type-safe code that can be applied to different data types.
- Generics in Kotlin reduce the need for explicit type casting and repetitive code, making your code more concise and expressive. They promote clean and efficient code design, enhancing code maintainability.