Generics in Kotlin provide a powerful way to write reusable and type-safe code. However, on the Java Virtual Machine (JVM), generics are subject to type erasure, meaning that the specific type arguments used for instances of a generic class are not preserved at runtime. This limitation has implications for runtime type checks and casts. But fear not! Kotlin provides a solution: reified type parameters. In this blog post, we’ll delve into the world of reified type parameters and explore how they enable us to access and manipulate type information at runtime.
Understanding Type Erasure in Kotlin Generics
Generics in Kotlin are implemented using type erasure on the JVM. This means that the specific type arguments used for instances of a generic class are not preserved at runtime. In this section, we’ll explore the practical consequences of type erasure in Kotlin and learn how you can overcome its limitations by declaring a function as inline.
By declaring a function as inline, you can prevent the erasure of its type arguments. In Kotlin, this is achieved by using reified type parameters. Reified type parameters allow you to access and manipulate the actual type information of the generic arguments at runtime.
In simpler terms, when you mark a function as inline with a reified type parameter, you can retrieve and work with the specific types used as arguments when calling that function.
Now, let’s look at some examples to better understand the concept of reified type parameters and their usefulness.
Generics at runtime: type checks and casts
Generics in Kotlin, similar to Java, are erased at runtime. This means that the type arguments used to create an instance of a generic class are not preserved at runtime. For example, if you create a List<String> and put strings into it, at runtime, you will only see it as a List. You won’t be able to identify the specific type of elements the list was intended to contain. However, the compiler ensures that only elements of the correct type are stored in the list based on the type arguments provided during compilation.
At runtime, you don’t know whether list1 and list2 were declared as lists of strings or integers. Each of them is just a List
Even though the compiler recognizes list1 and list2 as distinct types, at execution time, they appear the same. However, you can generally rely on List<String> to contain only strings and List<Int> to contain only integers because the compiler knows the type arguments and enforces type safety. It is possible to deceive the compiler using type casts or Java raw types, but it requires a deliberate effort.
When it comes to checking the type information at runtime, the erased type information poses some limitations. You cannot directly check if a value is an instance of a specific erased type with type arguments. For example, the following code won’t compile:
Kotlin
if (valueis List<String>) { ... } // Error: Cannot check for instance of erased type
Even though you can determine at runtime that value is a List, you cannot determine whether it’s a list of strings, persons, or some other type. That information is erased.
Note that erasing generic type information has its benefits: the overall amount of memory used by your application is smaller; because less type information needs to be saved in memory.
As we stated earlier, Kotlin doesn’t let you use a generic type without specifying type arguments. Thus you may wonder how to check that the value is a list, rather than a set or another object
To check if a value is a List without specifying its type argument, you can use the star projection syntax:
Kotlin
if (valueis List<*>) { ... }
By using List<*>, you’re essentially treating it as a type with unknown type arguments, similar to Java’s List<?>. In this case, you can determine that the value is a List, but you won’t have any information about its element type.
Note that you can still use normal generic types in as and as? casts. However, these casts won’t fail if the class has the correct base type but a wrong type argument because the type argument is not known at runtime. The compiler will emit an “unchecked cast” warning for such casts. It’s important to understand that it’s only a warning, and you can still use the value as if it had the necessary type.
Here’s an example of using as? cast with a warning:
Kotlin
funprintSum(c: Collection<*>) {val intList = c as? List<Int> // Warning here. Unchecked cast: List<*> to List<Int> ?: throwIllegalArgumentException("List is expected")println(intList.sum())}
This code defines a function called printSum that takes a collection (c) as a parameter. Within the function, a cast is performed using the as? operator, attempting to cast c as a List<Int>. If the cast succeeds, the resulting value is assigned to the variable intList. However, if the cast fails (i.e., c is not a List<Int>), the as? operator returns null, and the code throws an IllegalArgumentException with the message “List is expected”. Finally, the sum of the integers in intList is printed.
Let’s see how this function behaves when called with different inputs:
Kotlin
printSum(listOf(1, 2, 3)) // o/p - 6
When called with a list of integers, the function works as expected. The sum of the integers is calculated and printed.
Now let’s change the input to a set:
Kotlin
printSum(setOf(1, 2, 3)) // o/p - IllegalArgumentException: List is expected
When called with a set of integers, the function throws an IllegalArgumentException because the input is not a List. The as? cast fails, resulting in a null value, and the IllegalArgumentException is thrown.
Now we pass String as input:
Kotlin
printSum(listOf("a", "b", "c")) // o/p - ClassCastException: String cannot be cast to Number
When called with a list of strings, the function successfully casts the list to a List<Int>, despite the wrong type argument. However, during the execution of intList.sum(), a ClassCastException occurs. This happens because the function tries to treat the strings as numbers, resulting in a runtime error.
The code examples above demonstrate that type casts (as and as?) in Kotlin may lead to runtime exceptions if the casted type and the actual type are incompatible. The compiler emits an “unchecked cast” warning to notify you about this potential risk. It’s important to understand the meaning of these warnings and be cautious when using type casts.
The code snippet below shows an alternative approach using an is check:
Kotlin
funprintSum(c: Collection<*>) {val intList = c as? List<Int> // Warning here. Unchecked cast: List<*> to List<Int> ?: throwIllegalArgumentException("List is expected")println(intList.sum())}
In this example, the printSum function takes a Collection<Int> as a parameter. Using the is operator, it checks if c is a List<Int>. If the check succeeds, the sum of the integers in the list is printed. This approach is possible because the compiler knows at compile time that c is a collection of integers.
So, Kotlin’s compiler helps you identify potentially dangerous type checks (forbidding is checks) and emits warnings for type casts (as and as?) that may cause issues at runtime. Understanding these warnings and knowing which operations are safe is essential when working with type casts in Kotlin.
Power of Reified Type Parameters in Inline Functions
In Kotlin, generics are typically erased at runtime, which means that you can’t determine the type arguments used when an instance of a generic class is created or when a generic function is called. However, there is an exception to this limitation when it comes to inline functions. By marking a function as inline, you can make its type parameters reified, which allows you to refer to the actual type arguments at runtime.
Let’s take a look at an example to illustrate this. Suppose we have a generic function called isA that checks if a given value is an instance of a specific type T:
Kotlin
fun <T> isA(value: Any) = valueis T
If we try to call this function with a specific type argument, like isA<String>("abc"), we would encounter an error because the type argument T is erased at runtime.
However, if we modify the function to beinline and mark the type parameter as reified, like this:
Kotlin
inlinefun <reifiedT> isA(value: Any) = valueis T
Now we can call isA<String>("abc") and isA<String>(123) without any errors. The reified type parameter allows us to check whether the value is an instance of T at runtime. In the first example, the output will be true because "abc" is indeed a String, while in the second example, the output will be false because 123 is not a String.
Another practical use of reified type parameters is demonstrated by the filterIsInstance function from the Kotlin standard library. This function takes a collection and selects instances of a specified class, returning only those instances. For example:
Kotlin
val items = listOf("one", 2, "three")println(items.filterIsInstance<String>())
In this case, we specify <String> as the type argument for filterIsInstance, indicating that we are interested in selecting only strings from the items list. The function’s return type is automatically inferred as List<String>, and the output will be [one, three].
Here’s a simplified version of the filterIsInstance function’s declaration from the Kotlin standard library:
Before coming to this code explanation, have you ever thought, Why reification works for inline functions only? How does this work? Why are you allowed to write element is T in an inline function but not in a regular class or function? Let’s see the answers to all these questions:
Reification works for inline functions because the compiler inserts the bytecode implementing the inline function directly at every place where it is called. This means that the compiler knows the exact type used as the type argument in each specific call to the inline function.
When you call an inline function with a reified type parameter, the compiler can generate a bytecode that references the specific class used as the type argument for that particular call. For example, in the case of the filterIsInstance<String>() call, the generated code would be equivalent to:
Kotlin
for (element inthis) {if (element is String) { destination.add(element) }}
The generated bytecode references the specific String class, not a type parameter, so it is not affected by the type-argument erasure that occurs at runtime. This allows the reified type parameter to be used for type checks and other operations at runtime.
It’s important to note that inline functions with reified type parameters cannot be called from Java code. Regular inline functions are accessible to Java as regular functions, meaning they can be called but are not inlined. However, functions with reified type parameters require additional processing to substitute the type argument values into the bytecode, and therefore they must always be inlined. This makes it impossible to call them in a regular way, as Java code does not support this mechanism.
Also, one more thing to note is that an inline function can have multiple reified type parameters and can also have non-reified type parameters alongside the reified ones. It’s important to keep in mind that marking a function as inline does not necessarily provide performance benefits in all cases. If the function becomes large, it’s recommended to extract the code that doesn’t depend on reified type parameters into separate non-inline functions for better performance.
Practical use cases of reified type parameters
Reified type parameters can be especially useful when working with APIs that expect parameters of type java.lang.Class. Let\’s explore two examples to demonstrate how reified type parameters simplify such scenarios.
Example 1
ServiceLoader The ServiceLoader API from the JDK is an example of an API that takes a java.lang.Class representing an interface or abstract class and returns an instance of a service class implementing that interface. Traditionally, in Kotlin, you would use the following syntax to load a service:
Kotlin
val serviceImpl = ServiceLoader.load(Service::class.java)
However, using a function with a reified type parameter, we can make this code shorter and more readable:
Kotlin
val serviceImpl = loadService<Service>()
To define the loadService function, we use the inline modifier and a reified type parameter:
Kotlin
inlinefun <reifiedT> loadService(): T {return ServiceLoader.load(T::class.java)}
Here, T::class.java retrieves the java.lang.Class corresponding to the class specified as the type parameter, allowing us to use it as needed. This approach simplifies the code by specifying the class as a type argument, which is shorter and easier to read compared to ::class.java syntax.
Example 2
Simplifying startActivity in Android In Android development, when launching activities, instead of passing the class of the activity as a java.lang.Class, you can use a reified type parameter to make the code more concise. For instance:
With this inline function, you can start an activity by specifying the activity class as a type argument:
Kotlin
startActivity<DetailActivity>()
This simplifies the code by eliminating the need to pass the activity class as a java.lang.Class instance explicitly.
Reified type parameters allow us to work with class references directly, making the code more readable and concise. They are particularly useful in scenarios where APIs expect java.lang.Class parameters, such as ServiceLoader in Java or starting activities in Android.
Restrictions on Reified Type Parameters
Reified Type parameters in Kotlin have certain restrictions that you need to be aware of. Some of these restrictions are inherent to the concept itself, while others are determined by the implementation of Kotlin and may change in future Kotlin versions. Here’s a summary of how you can use reified type parameters and what you cannot do:
You can use a reified type parameter in the following ways:
Type checks and casts (is, !is, as, as?)
Reified type parameters can be used in type checks and casts. You can check if an object is of a specific type or perform a type cast using the reified type parameter. Here’s an example:
Kotlin
inlinefun <reifiedT> checkType(obj: Any) {if (obj is T) {println("Object is of type T") } else {println("Object is not of type T") }val castedObj = obj as? T// Perform operations with the casted object}
Kotlin reflection APIs (::class)
Reified type parameters can be used with Kotlin reflection APIs, such as ::class, to access runtime information about the type. It allows you to retrieve the KClass object representing the type parameter. Here’s an example:
Getting the corresponding java.lang.Class (::class.java)
Reified type parameters can also be used to obtain the corresponding java.lang.Class object of the type using the ::class.java syntax. This can be useful when interoperating with Java APIs that require Class objects. Here’s an example:
Using reified type parameter as a type argument when calling other functions
Reified type parameters can be used as type arguments when calling other functions. This allows you to propagate the type information to other functions without losing it due to type erasure. Here’s an example:
Kotlin
inlinefun <reifiedT> processList(list: List<T>) {// Process the list of type Tfor (item in list) {// ... }}funmain() {val myList = listOf("Hello", "World")processList<String>(myList)}
These examples demonstrate the various ways in which reified type parameters can be utilized in Kotlin, including type checks, reflection APIs, obtaining java.lang.Class, and passing the type information to other functions as type arguments.
However, there are certain things you cannot do with reified type parameters:
Creating new instances of the class specified as a type parameter
Reified type parameters cannot be used to create new instances of the class directly. You can only access the type information using reified type parameters. To create new instances, you would need to use other means such as reflection or factory methods. Here’s an example:
Kotlin
inlinefun <reifiedT> createInstance(): T {// Error: Cannot create an instance of the type parameter TreturnT()}
Calling methods on the companion object of the type parameter class
Reified type parameters cannot directly access the companion object of the type parameter class. However, you can access the class itself using T::class syntax. To call methods on the companion object, you would need to access it through the class reference. Here’s an example:
Kotlin
inlinefun <reifiedT> callCompanionMethod(): String {// Error: Cannot access the companion object of the type parameter Treturn T.Companion.someMethod()}
Using a non-reified type parameter as a type argument
When calling a function with a reified type parameter, you cannot use a non-reified type parameter as a type argument. Reified type parameters can only be used as type arguments themselves. Here’s an example:
Kotlin
inlinefun <reifiedT> reifiedFunction() {// Error: Non-reified type parameter cannot be used as a type argumentanotherFunction<T>()}fun <T> anotherFunction() {// ...}
Marking type parameters of classes, properties, or non-inline functions as reified
Reified type parameters can only be used in inline functions. You cannot mark type parameters of classes, properties, or non-inline functions as reified. Reified type parameters are limited to inline functions. Here’s an example:
Kotlin
classMyClass<T> { // Error: Type parameter cannot be marked as reified// ...}val <T> List<T>.property: T// Error: Type parameter cannot be marked as reifiedget() = TODO()fun <T> nonInlineFunction() { // Error: Type parameter cannot be marked as reified// ...}
These examples illustrate the restrictions on reified type parameters in Kotlin. By understanding these limitations, you can use reified type parameters effectively in inline functions while keeping in mind their specific usage scenarios.
Conclusion
Reified type parameters in Kotlin offer a powerful tool for overcoming the limitations of type erasure at runtime. By utilizing reified type parameters in inline functions, developers can access and manipulate precise type information, enabling type checks, casts, and interaction with reflection APIs. Understanding the benefits and restrictions of reified type parameters empowers Kotlin developers to write more expressive, type-safe, and concise code.
By embracing reified type parameters, Kotlin programmers can unleash the full potential of generics and enhance their runtime type-related operations. Start utilizing reified type parameters today and unlock a world of type-aware programming in Kotlin!
Kotlin, a modern and versatile programming language, offers various features to enhance developer productivity. One such powerful feature is function types, which enable you to treat functions as first-class citizens in your code. In this blog post, we will delve into function types in Kotlin, understand their types, explore their usage, and provide examples to solidify our understanding. So let’s dive in!
Recap: Higher-Order Functions
In Kotlin, a higher-order function is a function that can accept a lambda expression or a function reference as an argument or can return a lambda expression or a function reference. It allows functions to be treated as values and enables flexible and concise coding.
Let’s take the example of the filter function from the standard library, which takes a predicate function as an argument and is, therefore, a higher-order function:
Kotlin
list.filter { x > 0 }
Function types
In Kotlin, you can declare variables with function types. This means that the variables can hold references to functions. Let’s take a look at an example:
Kotlin
val sum: (Int, Int) -> Int = { x, y -> x + y } // Function that takes two Int parameters and returns an Int valueval action: () -> Unit = { println(42) } // Function that takes no arguments and doesn’t return a value
In this code, we have two variables: sum and action. The type of sum is a function that takes two Int parameters and returns an Int. The type of action is a function that takes no parameters and returns Unit, which represents a lack of meaningful value.
What are Function Types?
Function types allow you to treat functions as values. Just like any other variable, you can assign functions to variables, pass them as parameters to other functions, and even return them from functions. This feature provides flexibility and enables you to write more concise and expressive code.
Syntax
Function-type syntax in Kotlin
To declare a function type, you put the function parameter types in parentheses, followed by an arrow, and the return type of the function. In the above diagram, (Int, String) -> Unit specifies a function that takes Int and String parameters and returns a Unit.
The Unit type is used to indicate that a function doesn’t return a meaningful value. In regular function declarations, you can omit the Unit return type, but in function type declarations, it is always required. So, you can’t omit a Unit in this context.
Kotlin
val sum: (Int, Int) -> Int = { x, y -> x + y }
In the lambda expression { x, y -> x + y }, you might notice that the types of the parameters x and y are omitted. This is because the types are already specified in the function type declaration, so there’s no need to repeat them in the lambda itself.
You can also make the return type of a function type nullable by using the? symbol. For example:
Kotlin
var canReturnNull: (Int, Int) -> Int? = { null }
In this case, the function type (Int, Int) -> Int? represents a function that takes two Int parameters and returns an Int that can be nullable. This means the function can return either an Int value or null.
Furthermore, you can declare a nullable variable of a function type by enclosing the entire function type definition in parentheses and placing the question mark after the parentheses. For example:
Kotlin
var funOrNull: ((Int, Int) -> Int)? = null
Here, ((Int, Int) -> Int)? represents a nullable variable of a function type. The entire function type definition is enclosed in parentheses, and the question mark indicates that the variable itself is nullable.
It’s important to note the distinction between a function type with a nullable return type ((Int, Int) -> Int?) and a nullable variable of a function type (((Int, Int) -> Int)?). Omitting the parentheses will result in different meanings, so be cautious when specifying nullable function types.
Parameter names of function types
In Kotlin, you have the option to specify names for the parameters of a function type. This can improve the readability of your code and can be helpful for code completion in the IDE.
Here’s an example that demonstrates specifying parameter names in a function type:
In this code, the performRequest function takes two parameters: url of type String and callback of type (code: Int, content: String) -> Unit. The callback parameter is a function type that expects two parameters named code and content, both of type Int and String respectively. The function type represents a callback function that will be invoked when the request is completed.
When you call the performRequest function, you can provide a lambda expression as the argument for the callback parameter. The lambda can use any parameter names you prefer, regardless of the names specified in the function type declaration. For example:
Kotlin
val url = "https://blog.softaai.com"performRequest(url) { code, content ->// Code that uses the parameters 'code' and 'content'}performRequest(url) { code, page ->// Code that uses the parameters 'code' and 'page'}
In the above examples, we pass a lambda expression to the performRequest function. Inside the lambda, we can choose different names for the parameters (code and content in the first example, and code and page in the second example). These parameter names in the lambda expression do not need to match the names specified in the function type declaration.
Although the parameter names don’t affect type matching, using descriptive names can make your code more readable and understandable. Additionally, modern IDEs can utilize these parameter names for code completion, making it easier for you to write your code accurately.
Calling functions passed as arguments
In Kotlin, you can call functions that are passed as arguments to other functions. Let’s explore a couple of examples to understand how this works.
First, let’s consider the twoAndThree function, which takes another function as an argument and performs an arbitrary operation on the numbers 2 and 3:
Kotlin
funtwoAndThree(operation: (Int, Int) -> Int) {val result = operation(2, 3)println("The result is $result")}
In this example, the twoAndThree function accepts a function called operation, which has a function type (Int, Int) -> Int. This means the operation function takes two Int parameters and returns an Int. Inside the twoAndThree function, the operation function is called with arguments 2 and 3, and the result is printed.
To call the twoAndThree function and pass a function as an argument, you can use a lambda expression. For example:
Kotlin
twoAndThree { a, b -> a + b }
In this case, we pass a lambda expression that adds two numbers (a + b). The lambda matches the function type (Int, Int) -> Int because it takes two Int parameters and returns an Int. The result of the addition, 5, is printed by the twoAndThree function.
Similarly, you can pass a different lambda expression to achieve a different operation:
Kotlin
twoAndThree { a, b -> a * b }
Here, the lambda multiplies the two numbers (a * b), and the result, 6, is printed.
The syntax for calling a function passed as an argument is the same as calling a regular function. You use parentheses after the function name and provide the necessary arguments inside the parentheses.
Now, let’s consider another example: reimplementing the filter function from the standard library. The filter function takes a predicate as a parameter. The predicate is a function that takes a character and returns a Boolean result. The implementation checks whether each character satisfies the predicate and adds it to a StringBuilder if it does.
To reimplement the filter function for strings, let’s consider the following implementation:
In this implementation, the filter function is an extension function on the String class. It takes a predicate parameter, which is a function that takes a Char parameter and returns a Boolean.
Inside the function, a StringBuilder is created to store the filtered characters. The function iterates over each character of the string using the for loop and checks if the character satisfies the given predicate. If the predicate returns true for a character, it is appended to the StringBuilder.
Finally, the StringBuilder is converted to a string using the toString() function and returned as the result.
Here’s an example of how you can use the filter function:
Kotlin
println("softAai Apps".filter { it in'A'..'Z' })
In this case, the input string is "softAai Apps", and the predicate checks if each character is within the range from 'A' to 'Z'. The filtered result, which only contains the uppercase alphabetic characters, is printed as "AA".
The filter function implementation is straightforward. It enables you to filter characters from a string based on a given predicate, providing a more convenient and readable way to perform such operations.
By using higher-order functions and passing functions as arguments, you can create flexible and reusable code that can perform different operations based on the provided functions.
Default and null values for parameters with function types
When declaring a parameter of a function type, you can specify a default value for it. This can be useful when you want to provide a default behavior for the function if the caller doesn’t provide a specific implementation. Here’s an example:
Kotlin
fun <T> printCollection(collection: Collection<T>, transform: (T) -> String = { it.toString() }) {for (element in collection) {println(transform(element)) }}
In this example, the printCollection function takes a collection parameter of type Collection<T> and a transform parameter of function type (T) -> String. The transform parameter has a default value defined as a lambda expression { it.toString() }, which uses the toString() method to convert each element of the collection to a string.
You can call the printCollection function in different ways:
Kotlin
val numbers = listOf(1, 2, 3, 4, 5)// Omitting the transform parameter to use the default behaviorprintCollection(numbers)// Passing a lambda as the transform parameterprintCollection(numbers) { "Number: $it" }// Passing the transform parameter as a named argumentprintCollection(numbers, transform = { "Value: $it" })
In the first example, we omit the transform parameter, so the default behavior using toString() will be used to convert each element.
In the second example, we pass a lambda expression { "Number: $it" } as the transform parameter. This lambda defines a custom behavior to transform each element of the collection.
In the third example, we explicitly pass the transform parameter as a named argument, providing a different lambda expression { "Value: $it" } to customize the transformation.
Another option is to declare a parameter of a nullable function type. However, directly calling a function passed in such a parameter is not allowed because it could potentially lead to null pointer exceptions. To handle this, you can check for null explicitly or use the safe-call syntax callback?.invoke(). Here’s an example:
In this example, the performAction function takes a nullable function type parameter callback of type (() -> Unit)?. Inside the function, we can use the safe-call syntax callback?.invoke() to invoke the function only if it is not null.
Function Types with Generic Parameters
Kotlin allows you to define function types with generic parameters. This provides flexibility when working with functions that can operate on different types.
Kotlin
fun <T> processList(list: List<T>, operation: (T) -> Unit) {for (item in list) {operation(item) }}val numbers = listOf(1, 2, 3, 4, 5)processList(numbers) { println(it) } // Prints each number in the list
In the above example, the processList function takes a list of generic type T and a function type (T) -> Unit as parameters. The operation function is called for each item in the list and can perform any desired operation.
Now you know how to write functions that take functions as arguments, including how to provide default values for function parameters and handle nullable function types. This allows you to create more flexible and customizable functions in Kotlin.
Returning functions from functions
Returning functions from functions allows you to dynamically choose and provide different logic based on certain conditions or states. This can be useful in scenarios where the behavior of a program needs to adapt to different situations.
For example, let’s consider a shipping cost calculation scenario. The shipping cost may vary depending on the chosen delivery method. We can define a function called getShippingCostCalculator that takes the delivery parameter of type Delivery (an enum class representing different delivery options) and returns a function of type (Order) -> Double. The returned function calculates the shipping cost based on the selected delivery method.
Here’s an example implementation:
Kotlin
enumclassDelivery { STANDARD, EXPEDITED }classOrder(val itemCount: Int)fungetShippingCostCalculator(delivery: Delivery): (Order) -> Double { // Declares a function that returns a functionif (delivery == Delivery.EXPEDITED) {return { order ->6 + 2.1 * order.itemCount } // Returns lambdas from the function }return { order ->1.2 * order.itemCount } // Returns lambdas from the function}
In this example, when getShippingCostCalculator is called with Delivery.EXPEDITED, it returns a function that takes an Order parameter and calculates the shipping cost as 6 + 2.1 * order.itemCount. For any other delivery option, it returns a different function that calculates the shipping cost as 1.2 * order.itemCount.
You can use the returned function to calculate the shipping cost for a specific order. Here’s an example of usage:
Kotlin
val calculator = getShippingCostCalculator(Delivery.EXPEDITED) // Stores the returned function in a variableprintln("Shipping costs ${calculator(Order(3))}") // Invokes the returned function
In this code, we obtain the calculator function by calling getShippingCostCalculator with Delivery.EXPEDITED. Then, we pass an Order object with itemCount as 3 to the calculator function, which calculates and returns the shipping cost as 12.3. Finally, we print the shipping cost.
Let’s consider one more scenario where returning functions from functions is useful.
Suppose you’re working on a GUI contact-management application, and you need to determine which contacts should be displayed based on the state of the user interface (UI). You can define a function called getContactFilter that takes a UI state as a parameter and returns a function of type (Contact) -> Boolean. The returned function will determine whether a contact should be displayed or not based on the UI state.
Here’s an example implementation:
Kotlin
dataclassContact(val name: String, val isFavorite: Boolean)enumclassUIState { ALL, FAVORITES }fungetContactFilter(uiState: UIState): (Contact) -> Boolean { // Declares a function that returns a functionreturnwhen (uiState) { UIState.ALL -> { true } // Display all contacts UIState.FAVORITES -> { contact -> contact.isFavorite } // Display only favorite contacts }}
In this example, getContactFilter takes a UIState parameter and returns a function of type (Contact) -> Boolean. The returned function determines whether a contact should be displayed or not based on the UI state. If the UI state is UIState.ALL, the returned function always returns true, indicating that all contacts should be displayed. If the UI state is UIState.FAVORITES, the returned function checks the isFavorite property of the contact and returns its value, indicating whether the contact is a favorite or not.
You can use the returned function to filter the contacts based on the UI state. Here’s an example usage:
In this code, we obtain the filter function by calling getContactFilter with UIState.FAVORITES. Then, we have a list of contacts, and we use the filter function to filter the contacts based on the UI state. The filtered contacts, in this case, are the contacts that are marked as favorites. Finally, we print the names of the filtered contacts.
Returning functions from functions allows you to dynamically select and apply different logic based on conditions or states, providing flexibility and customization in your code.
Using function types from Java
Under the hood, function types are declared as regular interfaces: a variable of a function type is an implementation of a FunctionN interface. The Kotlin standard library defines a series of interfaces, corresponding to different numbers of function arguments: Function0 (this function takes no arguments), Function1 (this function takes one argument), and so on. Each interface defines a single invoke method, and calling it will execute the function. A variable of a function type is an instance of a class implementing the corresponding FunctionN interface, with the invoke method containing the body of the lambda.
Kotlin functions that use function types can be called easily from Java. Java 8 lambdas are automatically converted to values of function types:
// JavaprocessTheAnswer(number -> number + 1); // output : 43
In this case, the processTheAnswer function in Kotlin takes a function type (Int) -> Int as a parameter. In Java, you can pass a lambda expression number -> number + 1 as an argument, and it will be automatically converted to the corresponding function type.
What about older Java versions?
If you are using an older version of Java that doesn’t support lambdas, you can pass an instance of an anonymous class that implements the invoke method from the corresponding function interface. Here’s an example:
Kotlin
// JavaprocessTheAnswer(new Function1<Integer, Integer>() { // Uses the Kotlin function type from Java code (prior to Java 8)@Overridepublic Integer invoke(Integer number) { System.out.println(number);return number + 1; }});
In this Java example, we create an anonymous class that implements the Function1 interface. We override the invoke method, which corresponds to the function body in Kotlin, and provide the desired implementation.
What about Extention Functions?
In Java, you can easily use extension functions from the Kotlin standard library that expect lambdas as arguments. Note, however, that they don’t look as nice as in Kotlin — you have to pass a receiver object as a first argument explicitly. Here’s an example:
Kotlin
// JavaList<String> strings = new ArrayList<>();strings.add("42");CollectionsKt.forEach(strings, s -> { System.out.println(s);return Unit.INSTANCE;});
In this Java example, we use the CollectionsKt.forEach extension function from the Kotlin standard library. We pass a lambda expression s -> { ... } as an argument. Inside the lambda, we can perform the desired operations. Note that in Java, you need to explicitly return Unit.INSTANCE to match the Kotlin requirement of returning Unit.
It’s important to remember that in Java, functions or lambdas can return Unit. However, since Unit has a value in Kotlin, you need to explicitly return it. Additionally, you cannot directly pass a lambda that returns void as an argument of a function type that expects Unit as the return type.
Overall, while using function types and lambdas from Kotlin in Java may require some adjustments in syntax, it is still possible to utilize Kotlin’s higher-order functions and achieve the desired functionality.
Removing duplication through lambdas
In Kotlin, lambdas are anonymous functions that can be treated as values. They allow you to define blocks of code that can be passed around and executed later. This flexibility enables us to write more concise and reusable code.
Suppose you have a scenario where you need to perform similar operations on different elements of a collection. Without lambdas, you might end up writing repetitive code for each element, resulting in duplication. However, by utilizing lambdas, you can extract the common behavior and eliminate duplication.
To illustrate this concept, let’s consider an example involving website visit data. We have a class called SiteVisit that represents a visit to a website. Each visit has properties like path (the visited URL), duration (time spent on the page), and os (operating system used by the visitor). The os property is an enum called OS, representing different operating systems.
Our goal is to calculate the average duration of visits from Windows machines. We can achieve this by filtering the visits based on the operating system, mapping the durations, and then calculating the average using the average function.
In this example, we used the lambda expression { it.os == OS.WINDOWS } as a filter to select only the visits with the Windows operating system. Then, we used the map function to extract the durations of those visits. Finally, the average function calculated the average duration.
Now, let’s say we want to calculate the average duration for visits from Mac users as well. Without using lambdas, we would need to write similar code again, resulting in duplication. However, we can avoid this duplication by extracting the common behavior into a function and parameterizing it.
We define an extension function called averageDurationFor on the List<SiteVisit> class, which takes an os parameter representing the operating system. Inside the function, we filter the visits based on the given operating system, map the durations, and calculate the average.
Kotlin
funList<SiteVisit>.averageDurationFor(os: OS) =filter { it.os == os } .map(SiteVisit::duration) .average()
With this function in place, we can now calculate the average duration for both Windows and Mac visits without duplicating the code.
By parameterizing the behavior that varies (in this case, the operating system), we eliminated duplication and made the code more reusable and maintainable.
However, there are situations where a simple parameter is not sufficient to capture the complexity of the condition we want to apply. For example, if we want to calculate the average duration for visits from mobile platforms (iOS and Android), a single parameter representing the operating system won’t be enough. In such cases, lambdas provide a powerful solution.
We can modify our averageDurationFor function to take a lambda expression as a parameter. This lambda expression represents a condition that needs to be fulfilled for a visit to be included in the calculation.
Now, we can use this enhanced averageDurationFor function to calculate the average duration based on more complex conditions. For example, finding the average duration for visits from Android and iOS users:
In this case, we passed a lambda expression { it.os in setOf(OS.ANDROID, OS.IOS) } to the averageDurationFor function. This lambda expression represents the condition that checks if the operating system is either Android or iOS.
Similarly, we can use Lambdas to perform more intricate queries, such as finding the average duration of visits to the signup page from iOS users:
Here, we provided a lambda expression { it.os == OS.IOS && it.path == "/signup" } to specify the condition that includes only the visits from iOS users to the “/signup” page.
By using lambdas and function types, we can eliminate code duplication and extract both the repeated data and behavior into reusable functions. Lambdas allow us to write more expressive and concise code, making our programs easier to understand and maintain.
Function types and lambdas not only help eliminate code duplication but also provide a flexible and concise way to define different strategies or behaviors within your code. Instead of creating multiple classes or interfaces for each strategy, you can directly pass lambda expressions as different strategies, simplifying your code and making it more expressive.
Conclusion
Function types in Kotlin provide a powerful way to work with functions as first-class citizens. They enable you to pass functions as parameters, return them from functions, and even assign them to variables. This flexibility allows for concise and expressive code, making Kotlin a great language for functional programming paradigms. By understanding the various aspects of function types and exploring practical examples, you can leverage this feature to write more efficient and maintainable code. So go ahead, harness the power of function types in Kotlin, and take your programming skills to the next level!
In the world of modern programming languages, Kotlin has gained popularity for its flexibility and concise coding style, largely thanks to lambdas or anonymous functions. However, the use of lambdas can introduce overhead due to function calls and memory allocations. To address this concern, Kotlin offers inline functions as a means to optimize code execution. In this blog post, we will delve into inline functions in Kotlin, understanding how they work, their limitations, and the advantages they offer over lambdas. Additionally, we will explore advanced concepts such as noinline, crossinline, and reified types that further enhance the capabilities of inline functions.
Understanding Inline Functions in Kotlin
In Kotlin, inline functions can help remove the overhead associated with lambdas and improve performance. When you use a lambda expression, it is typically compiled into an anonymous class. This means that each time you use a lambda, an additional class is created. Moreover, if the lambda captures variables, a new object is created for each invocation. As a result, using Lambdas can introduce runtime overhead and make the implementation less efficient compared to directly executing the code.
To mitigate this performance impact, Kotlin provides the inline modifier for functions. When you mark a function with inline, the compiler replaces every call to that function with the actual code implementation, instead of generating a function call. This way, the overhead of creating additional classes and objects is avoided.
Let’s see a simple example to illustrate this:
Kotlin
inlinefunmultiply(a: Int, b: Int): Int {return a * b}funmain() {val result = multiply(2, 3)println(result)}
In this example, the multiply function is marked as inline. When you call multiply(2, 3), the compiler replaces the function call with the actual code of the multiply function:
Kotlin
funmain() {val result = 2 * 3// only for illustrating purposes, later we will see how it actually works println(result)}
This allows the code to execute the multiplication directly without the overhead of a function call.
Let’s see one more example to illustrate this:
Kotlin
inlinefunperformOperation(a: Int, b: Int, operation: (Int, Int) -> Int): Int {returnoperation(a, b)}funmain() {val result = performOperation(5, 3) { x, y -> x + y }println(result)}
In this example, the performOperation function is marked as inline. It takes two integers, a and b, and a lambda expression representing an operation to be performed on a and b. When performOperation is called, instead of generating a function call, the compiler directly replaces the code inside the function with the code from the lambda expression.
So, in the main function, the call to performOperation(5, 3) will be replaced with the actual code 5 + 3. This eliminates the overhead of creating an anonymous class and improves performance.
BTW, How inlining works actually?
When you declare a function as inline in Kotlin, its body is substituted directly into the places where the function is called, instead of being invoked as a separate function. This substitution process is known as inlining.
Let’s take a look at an example to understand it more:
In this example, the synchronized function is declared as inline. It takes a Lock object and a lambda action as parameters. The function locks the Lock object, executes the provided action lambda, and then releases the lock.
When you use the synchronized function, the code generated for every call to it is similar to a synchronized statement in Java.
In this case, the lambda expression passed to synchronized is substituted directly into the code of the calling function. The bytecode generated from the lambda becomes part of the definition of the calling function and is not wrapped in an anonymous class implementing a function interface.
Not inlined Case (passing lambda as a parameter)
It’s worth noting that if you call an inline function and pass a parameter of a function type from a variable, rather than a lambda directly, the body of the inline function is not inlined.
Here’s an example:
Kotlin
classLockOwner(val lock: Lock) {funrunUnderLock(body: () -> Unit) {synchronized(lock, body) // A variable of a function type is passed as an argument, not a lambda. }}
In this case, the lambda’s code is not available at the site where the inline function is called, so it cannot be inlined. The body of the runUnderLock function is not inlined because there’s no lambda at the invocation. Only the body of the synchronized function is inlined; the lambda is called as usual. The runUnderLock function will be compiled to bytecode similar to the following function:
Kotlin
classLockOwner(val lock: Lock) {fun__runUnderLock__(body: () -> Unit) { // This function is similar to the bytecode the real runUnderLock is compiled to lock.lock()try {body() // The body isn’t inlined, because there’s no lambda at the invocation. } finally { lock.unlock() } }}
Here, the body of the runUnderLock function cannot be inlined because the lambda is passed as a parameter from a variable (body) rather than directly providing a lambda expression.
Suppose when you pass a lambda as a parameter directly, like this:
Kotlin
lockOwner.runUnderLock {// code block A}
The body of the inline function runUnderLock can be inlined, as the compiler knows the exact code to replace at the call site.
However, when you pass a lambda from a variable, like this:
Kotlin
val myLambda = {// code block A}lockOwner.runUnderLock(myLambda)
The body of the inline function cannot be inlined because the compiler doesn’t have access to the code inside the lambda (myLambda) at the call site. It would require the compiler to know the contents of the lambda in order to inline it.
In such cases, the function call behaves like a regular function call, and the body of the function is not copied to the call site. Instead, the lambda is passed as an argument to the function and executed within the function’s context.
So, suppose even though the runUnderLock function is marked as inline, the body of the function won’t be inlined because the lambda is passed as a parameter from a variable.
What about multiple inlining?
If you have two uses of an inline function in different locations with different lambdas, each call site will be inlined independently. The code of the inline function will be copied to both locations where you use it, with different lambdas substituted into it.
If you have multiple calls to the inline function with different lambdas, like this:
Each call site will be inlined independently. The code of the inline function will be copied to both locations where you use it, with different lambdas substituted into it. This allows the compiler to inline the code at each call site separately.
Restrictions on inline functions
When a function is declared as inline in Kotlin, the body of the lambda expression passed as an argument is substituted directly into the resulting code. However, this substitution imposes certain restrictions on how the corresponding parameter can be used in the function body.
If the parameter is called directly within the function body, the code can be easily inlined. But if the parameter is stored for later use, the code of the lambda expression cannot be inlined because there must be an object that contains this code.
In general, the parameter can be inlined if it’s called directly or passed as an argument to another inline function. If it’s used in a way that prevents inlining, such as storing it for later use, the compiler will prohibit the inlining and show an error message stating “Illegal usage of inline-parameter.”
Let’s consider an example with the Sequence.map function:
Kotlin
fun <T, R> Sequence<T>.map(transform: (T) -> R): Sequence<R> {returnTransformingSequence(this, transform)}
The map function doesn’t call the transform function directly. Instead, it passes the transform function as a constructor parameter to a class (TransformingSequence) that stores it in a property. To support this, the lambda passed as the transform argument needs to be compiled into the standard non-inline representation, which is an anonymous class implementing a function interface.
“noinline” Modifier
In situations where a function expects multiple lambda arguments, you can choose to inline only some of them. This can be useful when one of the lambdas contains a lot of code or is used in a way that doesn’t allow inlining. To mark parameters that accept non-inlineable lambdas, you can use the noinline modifier:
By using noinline, you indicate that the notInlined parameter should not be inlined.
Note that the compiler almost fully supports inlining functions across modules, or functions defined in third-party libraries(we will discuss more at the end of this blog). You can also call most inline functions from Java; such calls will not be inlined but will be compiled as regular function calls.
Inlining collection operations
In Kotlin, the standard library provides a set of collection functions that accept lambda expressions as arguments. These functions, such as filter, map, and others, are declared as inline, which means that the bytecode of both the function and the lambda will be inlined at the call site.
Let’s compare the performance of filtering a list of people using the filter function with a lambda expression versus manually filtering the list using a loop:
Kotlin
///////////// manually //////////////////dataclassPerson(val name: String, val age: Int)val result = mutableListOf<Person>()for (person in people) {if (person.age < 30) result.add(person)}println(result) // [Person(name=Alice, age=29)]
Kotlin
////////////// with lambda /////////////////val people = listOf(Person("Alice", 29), Person("Bob", 31))println(people.filter { it.age < 30 }) // [Person(name=Alice, age=29)]
This code uses the filter function to filter the list based on the condition specified in the lambda expression { it.age < 30 }. The resulting code will be roughly the same as manually filtering the list using a loop.
The reason for this is that the filter function is declared as inline, and its bytecode, along with the bytecode of the lambda, will be substituted directly into the calling code. This eliminates the overhead of function calls and lambda object creation, resulting in efficient code execution.
Now, let’s consider a chain of operations where both filter and map are applied:
In this example, both filter and map functions are declared as inline. However, there is an intermediate collection created to store the result of filtering before applying the mapping operation. The code generated from the filter function adds elements to this intermediate collection, and the code generated from map reads from it.
If the number of elements to process is large and the overhead of the intermediate collection becomes a concern, you can use a Sequence instead by adding an asSequence call to the chain. However, it’s important to note that lambdas used to process a Sequence are not inlined. Each intermediate sequence is represented as an object storing a lambda in its field, and the terminal operation involves a chain of calls through each intermediate sequence. Therefore, adding asSequence calls to every chain of collection operations may not provide performance benefits for smaller collections and is more suitable for larger collections.
So, you can safely use idiomatic collection operations in Kotlin’s standard library functions, as they are declared as inline and their bytecode, along with the lambda expressions, will be inlined at the call site. If performance becomes a concern for larger collections, you can consider using Sequence and asSequence calls, but it’s not necessary for smaller collections, as regular collection operations perform well.
Deciding when to declare functions as inline
When deciding whether to declare a function as inline, it’s important to consider the specific circumstances and the type of function being used.
For regular function calls, it’s generally not necessary to use the inline keyword. The JVM already has powerful inlining support and automatically analyzes the code to inline calls when it provides the most benefit. The JVM performs this optimization while translating bytecode to machine code. Additionally, calling functions directly without inlining can provide clearer stack traces.
On the other hand, declaring functions as inline is beneficial when working with functions that take lambdas as arguments. In these cases, the overhead of inlining is more significant. By using the inline keyword, you can save on the function call overhead as well as the creation of additional classes and objects for lambda instances. The JVM currently may not always perform inlining effectively with calls and lambdas, so using the inline keyword can ensure efficient execution.
Furthermore, inlining allows you to use features that are not possible with regular lambdas, such as non-local returns(we will look later here). This can provide additional flexibility and functionality in your code.
However, it’s essential to consider the code size when deciding whether to use the inline modifier. If the function you want to inline is large, copying its bytecode into every call site can result in a significant increase in bytecode size. In such cases, it’s recommended to extract the code that is not related to the lambda arguments into a separate non-inline function. This approach helps manage code size and optimize performance.
It’s worth noting that the inline functions in the Kotlin standard library are typically small, as the developers have taken care to extract non-lambda-related code into separate functions.
So, you should carefully consider whether to use the inline keyword based on the specific circumstances and the type of function being used. Regular function calls can rely on the JVM’s inlining support, while functions with lambda arguments can benefit from the inline modifier to reduce overhead and enable additional features. Pay attention to code size and consider extracting unrelated code into separate non-inline functions if necessary.
Using inlined lambdas for resource management
Lambdas can be useful for simplifying code duplication when it comes to resource management. Resource management involves acquiring a resource before performing an operation and releasing it afterward. Resources can include files, locks, database transactions, and more.
Traditionally, the try/finally statement is used to implement this pattern. The resource is acquired before the try block and released in the finally block. However, in Kotlin, you can encapsulate the logic of the try/finally statement in a function and pass the code that uses the resource as a lambda to that function.
For example, the Kotlin standard library provides the withLock function, which offers a more idiomatic API for working with locks:
Kotlin
val l: Lock = ...l.withLock {// Access the resource protected by this lock}
The withLock function is an extension function defined in the Kotlin library. It takes a lambda as an argument and performs the necessary lock operations:
Kotlin
fun <T> Lock.withLock(action: () -> T): T {lock()try {returnaction() } finally {unlock() }}
Files are another type of resource commonly used with this pattern. In Java, the try-with-resources statement was introduced to simplify working with resources. In Kotlin, a similar effect can be achieved using the use function, which is an extension function in the Kotlin standard library.
Here’s an example of rewriting a Java method that reads the first line from a file using the use function in Kotlin:
The use function is called on a closable resource and receives a lambda as an argument. It ensures that the resource is closed properly, regardless of whether the lambda completes normally or throws an exception. The use function is inlined, meaning it doesn’t introduce any performance overhead.
Note that in the body of the lambda, a non-local return is used to return a value from the readFirstLineFromFile function.
Control flow in higher-order functions
When using higher-order functions like filter or forEach in Kotlin, the behavior of return statements changes. If you use a return statement inside a loop, it’s straightforward to understand that it will exit the loop. However, when you convert the loop into a higher-order function, such as filter, the return statement works differently.
Return statements in lambdas: return from an enclosing function
If you use a return statement inside a lambda passed to a higher-order function like forEach, it will not only exit the lambda but also return from the function that called the lambda. This type of return statement is called a non-local return because it affects a larger block of code than just the lambda itself.
To illustrate this, let’s consider an example. Suppose we have a list of Person objects and we want to find if there is a person named “Alice”:
Kotlin
dataclassPerson(val name: String, val age: Int)val people = listOf(Person("Alice", 29), Person("Bob", 31))funlookForAlice(people: List<Person>) { people.forEach {if (it.name == "Alice") {println("Found!")return } }println("Alice is not found")}
In this example, if the lambda inside forEach encounters a person with the name “Alice,” it will print “Found!” and immediately return from the lookForAlice function. However, if no person named “Alice” is found, it will execute the last line and print “Alice is not found.”
Non-local returns
If you use the return keyword in a lambda, it returns from the function in which you called the lambda, not just from the lambda itself. Such a return statement is called a non-local return because it returns from a larger block than the block containing the return statement. To understand the logic behind the rule, think about using a return keyword in a for loop or a synchronized block in a Java method. It’s obvious that it returns from the function and not from the loop or block
Note that the return from the outer function is possible only if the function that takes the lambda as an argument is inlined. In the example above, the forEach function is inlined, and the body of the forEach function is inlined together with the body of the lambda, so it’s easy to compile the return expression so that it returns from the enclosing function.
Using the return expression in lambdas passed to non-inline functions isn’t allowed. A non-inline function can save the lambda passed to it in a variable and execute it later, when the function has already returned, so it’s too late for the lambda to affect when the surrounding function returns.
Returning from lambdas: return with a label
You can indeed use a local return within a lambda expression in Kotlin. This type of return is similar to a break statement in a for loop. It allows you to terminate the execution of the lambda and continue executing the code from where the lambda was invoked. To differentiate a local return from a non-local return, you use labels.
Returns from a lambda use the “@” character to mark a label
To use a label with a lambda expression, you place the label name followed by the @ character before the opening curly brace of the lambda. Then, to perform a local return, you use the return@label syntax, where label represents the name of the label.
Here’s an example to demonstrate the use of labels and local returns:
In this code, the lambda expression inside the forEach function is labeled with label@. When the condition it.name == "Alice" is true, the return@label statement is executed, causing the lambda to exit. The program then proceeds with the line println("Alice might be somewhere").
It’s worth noting that you can also use the function name as the label for the lambda expression. Here’s an alternative version of the same code using the function name as the label:
In this case, return@forEach is used to explicitly specify the return label as the function name itself.
Note that if you specify the label of the lambda expression explicitly, labeling using the function name doesn’t work. A lambda expression can’t have more than one label.
Labeled “this” expression
The same rules and concepts of labels also apply to lambdas with receivers. In Kotlin, lambdas with receivers are lambdas that have an implicit context object accessible via the this reference within the lambda. When you specify a label for a lambda with a receiver, you can use the corresponding labeled this expression to access its implicit receiver.
Here’s an example that demonstrates this concept:
Kotlin
println(StringBuilder().apply sb@{ // This lambda’s implicit receiver is accessed by this@sb.listOf(1, 2, 3).apply { // “this” refers to the closest implicit receiver in the scopethis@sb.append(this.toString()) // All implicit receivers can be accessed,the outer ones via explicit labels. }})
Here’s a breakdown of the code:
StringBuilder().apply sb@{...}: This line creates a new StringBuilder instance using the constructor StringBuilder(). The apply function is then called on the StringBuilder instance. The sb@ label is used to explicitly label the lambda expression.
listOf(1, 2, 3).apply {...}: Inside the lambda expression, the apply function is called on a List created using listOf(1, 2, 3). This apply function is invoked on the List itself and not on the StringBuilder instance.
[email protected](this.toString()): The code within the lambda expression appends the string representation of the List (obtained through this.toString()) to the StringBuilder instance. The this@sb syntax refers to the implicit receiver of the outer lambda expression (StringBuilder instance).
It’s important to note that when using labels for lambdas with receivers, you can explicitly specify the label for the lambda expression, or you can use the function name as a label.
Anonymous functions: local returns by default
The non-local return syntax is fairly verbose and becomes cumbersome if a lambda contains multiple return expressions. Multiple return expressions mean lambda expressions that have more than one return statement within their body. This means that the lambda code can have multiple points where it can exit and return a value or terminate the execution.
Here’s an example of a lambda with multiple return statements:
To address this, you can use anonymous functions as an alternative syntax to pass around blocks of code. Anonymous functions provide a concise way to handle multiple returns within a block of code without the need for labels.
Here’s an example that demonstrates the use of anonymous functions to handle multiple returns:
val numbers = listOf(1, 2, 3, 4, 5): This line creates a list of integers containing the numbers 1, 2, 3, 4, and 5.
val result = numbers.map(fun(number): String {...}): The map function is called on the numbers list. It takes an anonymous function as an argument. The anonymous function accepts a single parameter number and returns a String. The map function applies this anonymous function to each element of the numbers list and creates a new list with the transformed values.
if (number % 2 == 0) { return "Even" } else { return "Odd" }: Within the anonymous function, this code block checks if the number is even or odd. If it’s even (when number % 2 == 0), the string “Even” is returned. Otherwise, if it’s odd, the string “Odd” is returned.
The returned string from each iteration of the anonymous function is collected by the map function, resulting in a new list result that contains the strings “Odd”, “Even”, “Odd”, “Even”, and “Odd” in this case.
Note that using anonymous functions can simplify the handling of multiple returns within a lambda expression.
Let’s see, how can this be possible?
An anonymous function is another way to write a block of code passed to a function. It is similar to a regular function, but its name and parameter types are omitted. Here’s an example:
Kotlin
funlookForAlice(people: List<Person>) { people.forEach(fun(person) {if (person.name == "Alice") returnprintln("${person.name} is not Alice") })}
In this example, the anonymous function is used inside the forEach function. It follows the same rules as regular functions for specifying the return type. Anonymous functions with a block body require the return type to be explicitly specified. However, if an expression body is used, the return type can be omitted.
Inside an anonymous function, a return expression without a label will return from the anonymous function itself, not from the enclosing function. The rule is that return returns from the closest function declared using the fun keyword. In contrast, lambda expressions do not use the fun keyword, so a return in a lambda expression returns from the outer function. The difference is illustrated here:
The return expression returns from the function declared using the fun keyword.
Here’s a comparison between an anonymous function return and a lambda expression return:
Kotlin
funlookForAlice(people: List<Person>) { people.forEach(fun(person) {if (person.name == "Alice") return// Returns from the anonymous function })}funlookForAlice(people: List<Person>) { people.forEach {if (it.name == "Alice") return// Returns from the enclosing function }}
Note that despite the similar appearance to regular function declarations, anonymous functions are another syntactic form of lambda expressions. The implementation details and inlining behavior for lambda expressions also apply to anonymous functions.
“crossinline” Modifier
the crossinline modifier in Kotlin is used to restrict non-local returns from lambdas. It helps ensure that lambdas passed to certain functions cannot use the return keyword to perform a non-local return.
But why do we need this “crossinline”?
Well, sometimes we pass lambdas to functions that are not inlined, such as higher-order functions, local objects, or nested functions. In these cases, the lambda can be executed in a different context from where it was defined. If the lambda could perform a non-local return, it might cause unexpected behavior or make the code harder to understand.
In Kotlin, when we pass a lambda to a higher-order function or a non-inlined function, the lambda can be executed in a different context from where it was defined. This means that the lambda may be called outside its original scope. By default, a lambda can have a non-local return, which means it can exit not only from itself but also from the surrounding function. However, in some cases, allowing non-local returns can lead to unexpected behavior or make the code harder to understand.
Here are a few simplified real-time use cases where crossinline can be useful:
Asynchronous Callbacks: Imagine a scenario where you have an asynchronous operation that takes a callback function. The callback function is executed when the operation completes. If the callback could perform a non-local return, it might prematurely exit the surrounding function, causing unexpected behavior. By marking the callback as crossinline, you ensure that it executes within the proper context and doesn’t exit the surrounding function prematurely.
Resource Management:Consider a function that manages the acquisition and release of resources, such as opening and closing a file. The function takes a lambda as a parameter to perform some operations on the resource. If the lambda could perform a non-local return, it might skip the resource release step, leading to resource leaks. By marking the lambda as crossinline, you ensure that the resource release step is always executed before the function returns.
Error Handling: In error handling scenarios, you might have a function that takes a lambda to handle the error case. If the lambda could perform a non-local return, it might bypass the necessary error handling steps defined within the surrounding function. By using crossinline, you ensure that the error-handling logic remains intact and is always executed within the proper context.
In these use cases, using crossinline helps maintain the expected flow and behavior of the code, preventing unexpected returns or skipping important steps within the surrounding function.
The purpose of crossinline is to enforce that such lambdas cannot perform non-local returns. By marking a lambda parameter as crossinline, we explicitly state that it should not use the return keyword to return from outside the lambda’s scope.
Let’s see an example to illustrate this concept:
Kotlin
inlinefunhigherOrderFunction(crossinline aLambda: () -> Unit) {normalFunction {aLambda() // Using aLambda inside normalFunction }}funnormalFunction(aLambda: () -> Unit) {return// Normal return from normalFunction}funmain() {higherOrderFunction {return// Error: Cannot perform non-local return from a crossinline lambda }}
In this example, we have a higher-order function called higherOrderFunction that takes a lambda parameter aLambda marked as crossinline. Inside higherOrderFunction, we invoke normalFunction and pass aLambda to it. Since aLambda is marked as crossinline, it cannot perform a non-local return.
Now, in the main function, we call higherOrderFunction and provide a lambda that tries to perform a non-local return using return. However, since the lambda is marked as crossinline, this will result in a compilation error.
The use of crossinline in this example ensures that the lambda passed to higherOrderFunction cannot perform non-local returns. This restriction makes the code easier to reason about and prevents potential issues that might arise from non-local returns in certain contexts.
“reified” Type
In Kotlin, the reified modifier is used in combination with the inline keyword to enable type information to be available at runtime for certain generic functions. It allows us to access and manipulate the type parameter of a generic function inside the function body.
By default, type parameters in Kotlin are erased at runtime due to type erasure.
Type erasure refers to the process by which type information is removed or erased at runtime in languages that employ generics. It is a mechanism used by the Java Virtual Machine (JVM) and other platforms to ensure compatibility with code compiled without generics.
In Kotlin, type parameters are erased at runtime due to type erasure, which means that the actual type arguments used when invoking a generic function or creating a generic class are not retained at runtime. Instead, the compiler replaces type parameters with their upper bounds or with the Any type if no upper bound is specified.
For example, consider the following generic function in Kotlin:
Kotlin
fun <T> printType(item: T) {println("Type of item: ${item::class.simpleName}")}
In this case, when the function is invoked with different type arguments, such as Int or String, the compiled bytecode does not retain information about the specific type argument. The type parameter T is erased, and the compiled code behaves as if it were using the Any type.
The consequence of type erasure is that, at runtime, generic code cannot differentiate between different type arguments. It means that within a generic function, you can’t directly access the specific type information of the type parameter T. For example, you can’t invoke functions or access properties specific to T without additional mechanisms.
However, with the reified modifier, we can retain type information within an inline function. This enables us to perform operations that require type-specific information, such as checking the type, accessing properties, or invoking functions specific to that type.
Here’s an example to illustrate the usage of reified:
Kotlin
inlinefun <reifiedT> printType(item: T) {println("Type of item: ${T::class.simpleName}")}funmain() {val number = 42val text = "softAai"printType(number) // Output: Type of item: IntprintType(text) // Output: Type of item: String}
In this example, we have an inline function called printType that takes a parameter named item of type T. The T type parameter is marked with the reified modifier. Inside the function, we use T::class to access the runtime class of T and retrieve its simple name.
In the main function, we call printType twice with different types: Int and String. When the function is inlined, the reified modifier allows us to access the type information of T at runtime. As a result, the type name of the item parameter is printed correctly.
The reified modifier simplifies certain operations that require runtime type information and eliminates the need for workarounds or reflection-based approaches. It improves type safety and enables more expressive and concise code.
It’s important to note that the reified modifier can only be used with inline functions, and it’s applicable only to type parameters of the function itself, not the class. Additionally, reified can be used in combination with other language features, such as is checks, when expressions, and function calls specific to the type T.
Overall, the reified modifier in Kotlin is a powerful tool that allows us to work with type information at runtime within inline functions.
Inline properties
Inline properties in Kotlin provide a way to mark property accessors as inline, allowing them to be inlined as regular functions at the call site. This can lead to improved performance and reduced overhead.
The inline modifier can be applied to the getter and setter accessors of properties that don’t have backing fields. You have the flexibility to annotate individual accessors or the entire property.
Here’s an example to illustrate the usage of inline properties:
In this example, we have an inline property foo with an inline getter. The getter returns an instance of Foo inline, meaning that the code inside the getter will be copied to the call site during compilation.
Similarly, we have an inline property bar with both an inline getter and setter. The getter and setter accessors can contain custom logic, and marking them as inline allows that logic to be inlined at the call site.
At the call site, accessing an inline property is no different from invoking a regular inline function. The property accessors are expanded and copied into the calling code, eliminating the overhead of function calls and providing potential performance benefits.
Let’s dive into a detailed example to explain how marking the setter or getter as inline can eliminate function call overhead.
Consider the following code:
Kotlin
dataclassPerson(val age: Int) {val currentAge: Intinlineget() = age}funmain() {val person = Person(25)println("Current age of the person: ${person.currentAge}")}
When you access the currentAge property using person.currentAge, the getter for the property is invoked. However, since the getter is marked as inline, its code is expanded and copied directly into the calling code (in this case, the println statement). This behavior is similar to invoking a regular inline function.
In this specific example, the getter’s code is quite simple: it returns the value of the age property. This code is directly inserted where the property is accessed,eliminating the overhead of a separate function call. This inlining results in potential performance benefits by avoiding the function call overhead and making the code more efficient.
The benefit of inlining the getter and setter is that the property accessors’ code is copied directly into the calling code during compilation. This eliminates the need for function calls and reduces the overhead associated with them. The result is improved performance, as the property access becomes as efficient as accessing a regular variable.
By using inline properties, we can achieve better performance when working with simple properties that don’t require complex logic in their accessors. However, it’s worth noting that inlining larger or more complex accessors can lead to increased code size, which might impact maintainability and readability.
So, marking the getter or setter as inline in Kotlin allows the code inside the accessors to be copied directly at the call site, eliminating the function call overhead. This results in improved performance when accessing or modifying inline properties.
Restrictions for public API inline functions
In Kotlin, when you have an inline function that is public or protected, it is considered part of a module’s public API. This means that other modules can call that function, and the function itself can be inlined at the call sites in those modules.
However, there are certain risks of binary incompatibility that can arise when changes are made to the module that declares the inline function, especially if the calling module is not re-compiled after the change.
To mitigate these risks, there are restrictions placed on public API inline functions. These functions are not allowed to usenon-public-API declarations, which include private and internal declarations and their parts, within their function bodies.
Using Private Declarations
Kotlin
privatefunprivateFunction() {// Implementation of private function}inlinefunpublicAPIInlineFunction() {privateFunction() // Error: Private declaration cannot be used in a public API inline function// Rest of the code}
In this scenario, we have a private function privateFunction(). When attempting to use this private function within the public API inline function publicAPIInlineFunction(), a compilation error will occur. The restriction prevents the usage of private declarations within public API inline functions.
Using Internal Declarations
Kotlin
internalfuninternalFunction() {// Implementation of internal function}inlinefunpublicAPIInlineFunction() {internalFunction() // Error: Internal declaration cannot be used in a public API inline function// Rest of the code}
In this scenario, we have an internal function internalFunction(). When trying to use this internal function within the public API inline function publicAPIInlineFunction(), a compilation error will arise. The restriction prohibits the usage of internal declarations within public API inline functions.
To eliminate this restriction and allow the use of internal declarations in public API inline functions, you can annotate the internal declaration with @PublishedApi. This annotation signifies that the internal declaration can be used in public API inline functions. When an internal inline function is marked with @PublishedApi, its body is checked as if it were a public function.
Using Internal Declarations with @PublishedApi
Kotlin
@PublishedApiinternalfuninternalFunction() {// Implementation of internal function}inlinefunpublicAPIInlineFunction() {internalFunction() // Allowed because internalFunction is annotated with @PublishedApi// Rest of the code}
In this scenario, we have an internal function internalFunction() that is annotated with @PublishedApi. This annotation indicates that the internal function can be used in public API inline functions. Therefore, using internalFunction() within the public API inline function publicAPIInlineFunction() is allowed.
By applying @PublishedApi to the internal declaration, we explicitly allow its usage in public API inline functions, ensuring that the function remains compatible and can be safely used in other modules.
So, the restrictions for public API inline functions in Kotlin prevent them from using non-public-API declarations. However, by annotating internal declarations with @PublishedApi, we can exempt them from this restriction and use them within public API inline functions, thereby maintaining compatibility and enabling safe usage across modules.
Conclusion
Inline functions in Kotlin offer an effective means of optimizing code efficiency by eliminating the overhead of lambdas and function calls. By replacing function calls with the actual function body, inline functions enhance performance and reduce memory usage. Additionally, advanced concepts like noinline, crossinline, and reified types provide further flexibility and control over inline function behavior. By understanding and leveraging these concepts effectively, developers can optimize their code and enhance application performance in Kotlin projects.
In the ever-evolving world of Android, each version brings its own set of enhancements and improvements. The past couple of Android versions brought some of the major upgrades Android has gotten since its inception. Android 12 introduced Material You, which brought much-needed UI changes, and Android 13 added quality-of-life improvements over Android 12, making it a more polished experience. Much like Android 13, Android 14 may seem like an incremental upgrade, but you would be surprised by just how many internal changes it brings to improve the overall Android experience.
Throughout this blog post, we will delve into the Android 14 new things you need to know as an Android developer in this fast-paced world. Here, we’ll explore how Android 14 empowers you to create exceptional experiences for your users effortlessly.
So, buckle up and get ready to embark on a thrilling adventure into the future of Android development, as we unravel the wonders of Google I/O 2023 and unveil the exciting world of Android 14!
Android 14 New Features
Android 14 is the latest version of Google’s mobile operating system, and it’s packed with new features for both users and developers. Here’s a look at some of the highlights:
Photo Picker
Say goodbye to privacy concerns when it comes to granting access to your photo library! In the past, apps would request access to your entire photo collection even if you just wanted to upload a single picture. This raised legitimate privacy worries since handing over access to all your photos wasn’t the safest option.
Luckily, Android 14 introduces a game-changing solution known as the Photo Picker feature. With this new interface, you have full control over which photos an app can access. Instead of granting unrestricted access, you can now select and share specific photos without compromising your privacy. This means that apps only get access to the photos you choose, ensuring that your entire photo library remains secure.
Thanks to Android 14’s Photo Picker, you can confidently enjoy the convenience of sharing photos while maintaining control over your privacy. It’s a small but significant step towards a safer and more personalized app experience.
Notification Flashes
Android 14 introduces a handy feature called “Notification Flashes” that proves invaluable in noisy environments or for individuals with hearing difficulties. If you often find yourself in situations where you can’t hear your phone’s notifications, this feature has got you covered.
To enable or disable Notification Flashes, follow these simple steps:
Open your phone’s Settings.
Look for the “Display” option and tap on it.
Scroll down and find “Flash notifications.”
You’ll see two toggle options: “Camera Flash” and “Screen Flash.” Toggle them on or off based on your preference.
If you choose to use Screen Flashes, you can even customize the color of the flash. Here’s how:
Within the “Flash notifications” menu, tap on “Screen Flash.”
You’ll be presented with a selection of colors to choose from.
Tap on a color to preview how it will look.
Once you’re satisfied with your choice, simply close the prompt.
With Notification Flashes, you can stay informed about incoming notifications, even in noisy environments or if you have difficulty hearing. It’s a simple yet powerful feature that enhances accessibility and ensures you never miss an important update.
Camera and Battery Life Improvements
Android 14 doesn’t just bring exciting new features but also focuses on enhancing the overall user experience. Google has made significant quality-of-life improvements to ensure a smoother and optimized performance.
One area of improvement is battery consumption. Android 14 is designed to be more efficient, helping to prolong your device’s battery life. This means you can enjoy using your phone for longer periods without worrying about running out of power.
Moreover, both the user interface (UI) and internal workings of Android 14 have been refined to provide a seamless experience. You can expect a smoother and more responsive interface, making navigation and app usage more enjoyable.
In addition to the general improvements, Android 14 introduces new camera extensions. These extensions optimize the post-processing time and enhance the quality of the images captured. If you have a Pixel device powered by the Tensor G2 chip, you’ll notice an even greater improvement in the camera department. The Tensor G2 chip brings significant advancements that further enhance the camera capabilities, resulting in stunning photos with reduced processing time.
With Android 14, you can look forward to a more efficient and polished experience, along with impressive camera enhancements, especially on Pixel devices powered by the Tensor G2 chip. Get ready to enjoy a smoother and more captivating Android journey!
Upcoming Features
As Android 14 is still in the development stage(currently in beta), the upcoming stable version may include or discard these proposed upcoming features.
LockScreen Customizations
One of the exciting features coming to Android 14 is the ability to customize your lock screen. This means you can personalize how your lock screen appears, including changing the clock style and customizing the app shortcuts located at the lower corners. This feature draws some inspiration from iOS 16.
These lock screen customizations are expected to be available in the stable Android 14 release, which is scheduled to launch next month if everything goes as planned for Google. However, it’s worth noting that the lock screen clock styles showcased at Google I/O 2023 weren’t particularly appealing, appearing somewhat flat. Hopefully, the final versions will have more vibrant and engaging styles to choose from.
Magic Compose
Google has an exciting feature called “Magic Compose” coming to the Messages app this summer. It works similarly to the AI generative features demonstrated at Google I/O 2023, which will be added to Google’s Workspace apps. Magic Compose helps you write text messages with different moods and styles. From the preview showcased at I/O, it looks really cool.
For example, if you type “Wanna grab dinner,” Magic Compose offers various rewrites that add excitement, lyrical flair, or even Shakespearean language. It’s a clever feature that adds fun and creativity to your messages. We hope it will eventually be available on Gboard as well. It seems like Google’s way of encouraging more people to use RCS and Google Messages in general. However, please note that Magic Compose is currently limited to Pixel devices.
Emoji, Generative AI, and Cinematic Wallpapers
Android has always been known for its customization options, and Android 14 takes it a step further with the addition of Emoji, Generative AI, and Cinematic wallpapers.
The Emoji wallpaper picker lets you create a unique and interactive wallpaper by selecting a few emoji and a dominant color. It combines them to create a fun and personalized wallpaper that reflects your favorite emoji.
The AI Generative Wallpaper feature is particularly exciting. It allows you to input a few words describing the type of wallpaper you want and then generates a selection of unique wallpapers exclusively for your device. These wallpapers are completely one-of-a-kind and tailored to your preferences.
Cinematic wallpapers bring depth and a parallax effect to your photos using AI. You can choose a photo and the feature will add a dynamic effect that responds to your device’s movements. It’s similar to the Cinematic feature in Google Photos, adding a captivating visual element to your device’s wallpaper.
With these customizable features, Android 14 offers even more ways to personalize your device and make it truly your own. Whether it’s through emoji mashups, generative wallpapers, or dynamic effects, Android 14 provides an enhanced level of customization for a unique and enjoyable user experience.
New Find My Device Experience
The Find My Device app on Android has received a fresh new look to match the latest design language. In addition, it will be receiving some exciting new features this fall. One of the notable additions is the expanded device support, allowing you to locate not only your phones but also accessories using other Android devices on the network.
This enhancement is a welcome addition to Android, as Apple has been a leader in the Find My iPhone experience. Furthermore, if you want to track larger objects like bicycles, manufacturers such as Tile and Chipolo will offer tracker tags that can be used with the Find My Device app.
With these updates, Android users can enjoy a more comprehensive and convenient way to locate their devices and belongings. It’s a great step forward in enhancing the Find My Device experience on Android.
Tracker Prevention and Alerts
Although Google’s efforts to convince Apple to adopt RCS have not been successful, both companies have collaborated on enhancing privacy measures, particularly with Tracker Prevention alerts.
BTW, RCS (Rich Communication Services) is an advanced messaging protocol replacing SMS, offering additional features and capabilities. Some of the features offered by RCS include read receipts, typing indicators, high-quality media sharing, group chats, and the ability to send messages over Wi-Fi or mobile data.
Regardless of the Android device you’re using, if an unidentified tracker is monitoring your activities, your Android device will provide a warning and assist you in locating the source. This collaboration between Google and Apple in the privacy department is a significant achievement, ensuring enhanced privacy and security for Android users.
Using your Android device as a Webcam
If you’re disappointed with the low-quality webcam on your laptop, hold off on buying an external webcam just yet. Android 14 might come with a fantastic feature that allows you to use your Android device as an external camera and stream in high-definition at 1080p.
To use this feature, simply connect your Android device to your PC and a menu will pop up. From there, select “webcam” to switch to using your phone’s camera. Currently, this feature is not available in the operating system, even as an experimental option, but it’s expected to be included in Android 14 if Google deems it ready for release.
With Android 14, you could potentially transform your Android device into a high-quality webcam, eliminating the need for an external camera. Keep an eye out for this exciting feature, which aims to provide a better video conferencing and streaming experience for Android users.
App Cloning
App Cloning is undoubtedly one of the most highly anticipated features in Android. In the past, users had to resort to downloading third-party app cloning utilities that often came bundled with spyware. However, with Android 14, Google plans to address this by introducing a native App Cloning utility.
App Cloning allows you to have two instances of the same app on your device. This feature is particularly useful for users with dual SIM phones who want to use multiple accounts of apps like WhatsApp simultaneously. By cloning the app and logging in with a secondary SIM card, you can have two separate accounts running concurrently.
Google initially hinted at the App Cloning feature during the Android 14 Developer Preview 1. However, there haven’t been any recent updates regarding its development. It is speculated that App Cloning may not be included in the initial stable release of Android 14. However, it is expected to be introduced in future Android 14 feature drop updates, specifically for Pixel users.
The addition of a native App Cloning utility will bring convenience and ease of use to Android users who require multiple instances of certain apps. While its exact timeline for availability remains uncertain, it is an exciting feature to look forward to in future updates of Android 14.
Predictive Back Gestures
Predictive back gestures were introduced in Android 14 Developer Preview 2 but were later removed in the following preview. These gestures allowed users to perform a slow back swipe to reveal the underlying app layer. This was particularly useful when you couldn’t remember the previous page or layer you were on.
By using predictive back gestures, you could check the layer below without losing the contents of the current page. It gave you the flexibility to verify if the previous layer was the one you intended to navigate to.
Initially, this feature was only supported in the Settings app and a few other system apps. However, it remains uncertain whether predictive back gestures will be included in the first stable release of Android 14. If not, there’s a possibility that it will be added in future feature updates.
While the fate of predictive back gestures in Android 14 is unclear, it presented an interesting way to navigate within apps and explore layers. We will have to wait and see if it becomes a part of the official release or is introduced in future updates.
App Pair
During Google I/O 2023, Google unveiled a feature called App Pair, which will be introduced in Android 14 later this year. This feature, showcased during the Pixel Fold announcement, allows users to pair and use apps together in split screens. You can also minimize or maximize them simultaneously.
At first glance, App Pair may not appear particularly useful for smartphones. However, with the increasing popularity of tablets, this feature could be a game-changer. It offers a compelling reason why Android tablets are no longer considered inferior to iPads.
With App Pair, users will have the ability to multitask more effectively on larger screens. By pairing apps in split screens, you can simultaneously use two apps side by side, enhancing productivity and convenience. Whether it’s taking notes while reading, watching a video while browsing the web, or messaging while referencing another app, App Pair makes multitasking on Android tablets a seamless experience.
The inclusion of App Pair in Android 14 demonstrates Google’s commitment to enhancing the tablet experience and bridging the gap between Android tablets and their competitors. It opens up new possibilities for users who rely on tablets for work, entertainment, or any other tasks that require multitasking.
With this upcoming feature, Android tablets are poised to offer a more compelling and competitive alternative to iPads, providing users with a powerful multitasking experience. Look forward to the release of Android 14 to enjoy the benefits of App Pair on compatible devices.
Partial Screen Recorder
In Android 14, a new screen recording feature called “Partial Screen Recording” may be introduced. Despite its name, it doesn’t mean recording only a selected area of the screen. Instead, it allows you to record a specific app without capturing any UI elements or notifications that might appear on the screen.
This feature works similarly to how Discord handles screen sharing. When you switch to view another app or the home screen during the recording, the recorded content will appear black. However, as soon as you switch back to the app you want to record, the content will be visible again. It’s a clever and convenient way to focus solely on recording the app without any distractions.
While the availability of the Partial Screen Recording feature in the official release of Android 14 is not confirmed, it is an exciting addition that can enhance the screen recording experience for users. So, keep an eye out for this neat feature in future Android updates.
Drag and Drop Text and Images to Different Apps
One exciting feature that Android 14 is expected to bring is the ability to drag and drop text and images between apps, similar to what iOS 15 offers. In the Android 14 Beta 3 build, you can already experience this feature with text, and it works seamlessly.
To use the text drag and drop feature, simply select the text you want to move, long press on it, and then drag it to another app where you want to paste the text. With your other hand, switch to the desired app and drop the text into the text area. It’s a convenient way to transfer text quickly and easily between different apps.
While the current beta version only supports text drag and drop, it is anticipated that the final Android 14 release will also include the ability to drag and drop images. This will allow you to effortlessly move images from one app to another, enhancing your productivity and ease of use.
Keep an eye out for the official Android 14 update to enjoy the full drag and drop functionality, making it simpler and more convenient to transfer both text and images between apps on your Android device.
Forced Themed Icons
One of the challenges with adaptive mono icons in Android 12 is that app developers need to add support for them. Without proper support, the overall experience may feel incomplete. However, in Android 13, Google introduced a feature that automatically converts icons to themed icons if they are not supported by developers. This helpful feature may also make its way to Android 14.
Currently, the Pixel launcher has a hidden flag that allows users to force themed icons, which has been present since Android 13 QPR Beta 3. This suggests that Google might enable this feature in the future. If enabled, it will contribute to a seamless and intuitive Android experience, ensuring that the icons match the overall theme of the device.
With automatic icon conversion, users won’t have to worry about inconsistent or mismatched icons on their devices. Android 14 aims to enhance the visual cohesiveness of the user interface, making it more polished and pleasing to the eye.
Keep an eye out for this feature in the upcoming Android 14 release, as it has the potential to improve the overall aesthetic and user experience on your Android device.
Conclusion
Android 14 introduces a range of features and improvements that enhance user experience. It offers customization options like LockScreen customizations and Emoji wallpaper pickers, along with privacy enhancements such as Tracker Prevention alerts. Quality of life improvements includes the Photo Picker feature and Notification Flashes. The update brings camera advancements, App Cloning utility, predictive back gestures, and the ability to use Android devices as external cameras. Android 14 promises a seamless and personalized experience, focusing on user customization and functionality.
Kotlin, being a modern and expressive programming language, provides a set of conventions that allow developers to use specific language constructs by defining functions with predefined names. These conventions provide a consistent and intuitive way to work with various language features. In this article, we’ll explore the different aspects of conventions in Kotlin and provide in-depth explanations along with examples.
Kotlin Conventions
As you know, Java has several language features tied to specific classes in the standard library. For example, objects that implement java.lang.Iterable can be used in for loops, and objects that implement java.lang.AutoCloseable can be used in try-with-resources statements. Kotlin has a number of features that work in a similar way, where specific language constructs are implemented by calling functions that you define in your own code. But instead of being tied to specific types, in Kotlin those features are tied to functions with specific names. For example, if your class defines a special method named plus, then, by convention, you can use the + operator on instances of this class. Because of that, in Kotlin we refer to this technique as conventions.
Kotlin uses the principle of conventions, instead of relying on types as Java does, because this allows developers to adapt existing Java classes to the requirements of Kotlin language features. The set of interfaces implemented by a class is fixed, and Kotlin can’t modify an existing class so that it would implement additional interfaces. On the other hand, defining new methods for a class is possible through the mechanism of extension functions. You can define any convention methods as extensions and thereby adapt any existing Java class without modifying its code
BTW, conventions in Kotlin are not limited to operator overloading or extension functions. There are various predefined function names that, when implemented in your class, enable specific language constructs and provide additional functionality. Here are a few examples:
iterator: By implementing the iterator function, you enable the usage of your class in for loops, similar to how objects implementing java.lang.Iterable work in Java.
invoke:Defining the invoke function allows you to treat instances of your class as function-like objects. This means you can call objects of your class as if they were functions directly, like myObject(parameter).
compareTo: If you implement the compareTo function, you can use comparison operators (<, <=, >, >=) on instances of your class, allowing for natural ordering.
Destructuring Declarations: Kotlin provides the ability to destructure objects into multiple variables using the component1(), component2(), etc. functions. By implementing these functions in your class, you can use destructuring declarations with instances of your class.
These are just a few examples of Kotlin conventions. By defining functions with specific names in your class, you can harness the power of Kotlin’s language features and make your code more expressive and concise. Kotlin conventions simplify the process of working with common language constructs and operators, promoting code readability and reducing boilerplate. Let’s go through each topic step by step.
Overloading arithmetic operators
As we see earlier, the most straightforward example of the use of conventions in Kotlin is arithmetic operators. In Java, the full set of arithmetic operations can be used only with primitive types, and additionally, the + operator can be used with String values. But these operations could be convenient in other cases as well. For example, if you’re working with numbers through the BigInteger class, it’s more elegant to sum them using + than to call the add method explicitly. To add an element to a collection, you may want to use the += operator. Kotlin allows you to do that, and in the below section, we’ll see how it works.
Overloading binary arithmetic operations
In Kotlin, you can overload binary arithmetic operations by defining functions with the corresponding operator modifier. That means Arithmetic operators, such as +, -, *, /, and %, can be overloaded in Kotlin. To overload an arithmetic operator, you need to define a function with a specific name and annotate it with the operator keyword. Let’s take the example of overloading the plus operator for the Point class.
Kotlin
dataclassPoint(val x: Int, val y: Int)
You can define the plus function within the Point class as follows:
Kotlin
dataclassPoint(val x: Int, val y: Int) {operatorfunplus(other: Point): Point {returnPoint(x + other.x, y + other.y) }}
Note that the operator keyword is used to declare the plus function. This explicitly indicates that you intend to use the function as an implementation of the + operator convention.
Once you have declared the plus function as an operator, you can use the + sign to sum up instances of the Point class:
Kotlin
val point1 = Point(1, 2)val point2 = Point(3, 4)val sum = point1 + point2println(sum) // Output: Point(x=4, y=6)
The point1 + point2 expression is equivalent to point1.plus(point2). The plus function is called automatically when the + operator is used between two Point instances.
Under the hood, when you use the + operator, the plus function is automatically invoked to perform the addition.
Alternatively, you can define the plus operator function as an extension function outside the Point class:
Kotlin
operatorfunPoint.plus(other: Point): Point {returnPoint(x + other.x, y + other.y)}
The implementation of the operator function remains the same, but it is now defined as an extension function.
Lists of all the binary operators you can define and the corresponding function names:
Overloadable binary arithmetic operators
You can overloadother arithmetic operators in a similar way by defining corresponding functions (minus, times, div, rem, etc.) within your class.
It’s important to note that the precedence of Kotlin operators follows the same rules as the standard numeric types. For example, in an expression like a + b * c, the multiplication (*) will always be executed before the addition (+), regardless of how you have defined those operators, even if you’ve defined those operators yourself. The operators *, /, and % have the same precedence, which is higher than the precedence of the + and — operators
Kotlin operators do not automatically support commutativity, which means you need to define separate operators if you want to swap the order of the operands. For example
Kotlin
operatorfunPoint.times(scale: Double): Point {returnPoint((x * scale).toInt(), (y * scale).toInt())}val p = Point(10, 20)println(p * 1.5) // Point(x=15, y=30)
Now suppose If you want users to be able to write 1.5 * pin addition to p * 1.5, you need to define a separate operator for that:
Kotlin
operatorfunDouble.times(p: Point): Point
Furthermore, The return type of an operator function can also be different from either of the operand types. For example, you can define an operator to create a string by repeating a character a number of times.
This operator takes a Char as the left operand and an Int as the right operand and has String as the result type. Such combinations of operand and result types are perfectly acceptable.
Note that you can overload operator functions like regular functions: you can define multiple methods with different parameter types for the same method name.
Java Interoperability (Operator Functions and Java)
When it comes to calling Kotlin operator functions from Java, it is straightforward. Since each overloaded operator in Kotlin is defined as a function, you can call them from Java just like regular functions by using the full function name.
For example, let’s say you have a Kotlin class Point with an overloaded plus operator function:
Kotlin
dataclassPoint(val x: Int, val y: Int) {operatorfunplus(other: Point): Point {returnPoint(x + other.x, y + other.y) }}
In Java, you would call this operator function as a regular function using the fully qualified name:
Kotlin
Point point1 = new Point(1, 2);Point point2 = new Point(3, 4);Point sum = point1.plus(point2);
Since Java doesn’t have a specific syntax for operator functions like Kotlin does, you need to call the operator function using the actual function name (plus in this case) instead of the operator symbol (+).
Now when calling Java from Kotlin, you can use the operator syntax for methods that match Kotlin conventions. Kotlin provides syntactic sugar to make code more concise and expressive. For example, if you have a Java class with a method named add and you call it from Kotlin, you can use the + operator instead, as long as the method follows the conventions of the plus operator in Kotlin.
On the other hand, if a Java class defines a method with the desired behavior but with a different name, you can define an extension function in Kotlin with the correct name that delegates to the existing Java method. This allows you to bridge the gap between Kotlin’s operator conventions and Java’s method names.
So, when interacting between Kotlin and Java, Kotlin operator functions can be called from Java as regular functions using their fully qualified names. In the reverse scenario, when calling Java from Kotlin, you can use the operator syntax for methods that adhere to Kotlin’s conventions, and you have the flexibility to define extension functions in Kotlin to map to existing Java methods with different names.
No special operators for bitwise operations
In Kotlin, there are no special operators for bitwise operations on standard number types. Instead, regular functions with infix call syntax are used to perform bitwise operations. You can also define similar functions that work with your own types.
Kotlin provides a set of functions for performing bitwise operations, including:
shl (Signed shift left):Performs a signed shift left operation.
shr (Signed shift right):Performs a signed shift right operation.
ushr (Unsigned shift right):Performs an unsigned (logical) shift right operation.
and (Bitwise and):Performs a bitwise and operation.
or (Bitwise or):Performs a bitwise or operation.
xor (Bitwise xor):Performs a bitwise exclusive or operation.
inv (Bitwise inversion):Performs a bitwise inversion operation.
Here’s an example demonstrating the use of some of these functions:
Kotlin
println(0x0F and 0xF0) // Bitwise and operation: 00001111 and 11110000 = 00000000 (result: 0)println(0x0F or 0xF0) // Bitwise or operation: 00001111 or 11110000 = 11111111 (result: 255)println(0x1 shl 4) // Signed shift left operation: 00000001 shifted left by 4 positions = 00010000 (result: 16)
In the above example, and performs a bitwise and operation between 0x0F (00001111) and 0xF0 (11110000), resulting in 0 (00000000). or performs a bitwise or operation between the same values, resulting in 255 (11111111). shl shifts 0x1 (00000001) left by 4 positions, resulting in 16 (00010000).
It’s important to note that while Kotlin doesn’t provide special operators for bitwise operations, you can use these functions to achieve the desired functionality. Additionally, you can define similar functions for your own types if needed.
Overloading compound assignment operators
In Kotlin, compound assignment operators such as +=, -=, and so on, are supported for certain operations. These operators allow you to modify a variable by performing an operation and assigning the result back to the variable in a single step.
When you define an operator, such as plus, Kotlin automatically supports the corresponding compound assignment operator, such as +=.
The += operator can be transformed into either the plus or the plusAssign function call.
However, there can be ambiguity when both the regular operator function and the compound assignment operator function are applicable.
To illustrate, let’s consider the example of the plusAssign function defined for a mutable collection in the Kotlin standard library:
When you use the += operator with a mutable collection, the plusAssign function is called. It adds an element to the collection.
In some cases, there may be ambiguity between using the regular operator or the compound assignment operator. When both the regular operator function (e.g., plus) and the compound assignment operator function (e.g., plusAssign) are applicable, Kotlin needs to decide which one to use.
To resolve this ambiguity, there are a few options:
Replace the use of the operator with a regular function call that explicitly states the intention. For example, instead of using +=, you can use the add function directly: collection.add(element).
Change a var to a val to make the compound assignment operation inapplicable. If the variable is read-only (val), the compound assignment operator cannot modify its value.
Design your classes consistently by providing either the regular operator function or the compound assignment operator function, but not both. If your class is immutable, like the Point class in an earlier example, it\’s best to provide operations that return a new value (e.g., plus). On the other hand, if your class is mutable, like a builder, it\’s best to provide operations like plusAssign that modify the instance in place.
It’s important to note that the Kotlin standard library supports both approaches for collections. The + and - operators always return a new collection. The += and -= operators work on mutable collections by modifying them in place and on read-only collections by returning a modified copy. However, it\’s worth mentioning that += and -= can only be used with a read-only collection if the variable referencing it is declared as a varand not as a val.
Kotlin
val readOnlyList = listOf(1, 2, 3)var mutableList = mutableListOf(4, 5, 6)mutableList += readOnlyListprintln(mutableList) // Output: [4, 5, 6, 1, 2, 3]readOnlyList += mutableList // Error: Val cannot be reassigned
In the above example, we have a read-only list readOnlyList and a mutable list mutableList. The += operator can be used to add the elements of readOnlyList to mutableList because mutableList is declared as a var. However, when we try to use += on readOnlyList, it throws an error because readOnlyList is declared as a val and cannot be reassigned.
So, to use += and -= operators on a read-only collection, make sure the variable referencing it is declared as a mutable variable (var). If it is declared as an immutable variable (val), you won\’t be able to modify the collection using these operators.
In Kotlin, when using the += and -= operators on collections, you can provide either individual elements or other collections as operands. The important thing is that the elements or collections you use should have a matching element type with the collection you are modifying.
It’s important to note that when using collections as operands, the element types should match. In the above example, both list1 and list2 contain integers, so the operations work seamlessly. If the element types don\’t match, such as combining a list of integers with a list of strings, you will encounter a compilation error.
Overloading unary operators
Kotlin allows you to overload unary operators, which are applied to a single value, as in -a. You can overload unary operators such as unary minus (-) and increment/decrement (++ and --) by declaring a function with a predefined name and marking it with the operator modifier.
Let’s start with the unary minus operator. To overload the unary minus operator for a class, you can define a function named unaryMinus without any parameters. This function should negate the coordinates of the object and return it.
Kotlin
dataclassPoint(val x: Int, val y: Int) {operatorfununaryMinus(): Point {returnPoint(-x, -y) }}
In the above example, the unaryMinus function negates the coordinates of the Point object and returns a new Point with negated values.
Overloadable unary arithmetic operators
Next, let’s consider the increment and decrement operators (++ and --). When you define the inc and dec functions to overload these operators, the compiler automatically supports the same semantics as for regular number types, including both pre-increment and post-increment operations.
Here’s an example that demonstrates the use of pre-increment and post-increment operators:
Kotlin
dataclassCounter(varvalue: Int) {operatorfuninc(): Counter {value++returnthis }operatorfundec(): Counter {value--returnthis }}funmain() {var counter = Counter(0)println(counter++) // Post-increment: prints the current value (0)println(++counter) // Pre-increment: increments and then prints the new value (2)}
In the above example, the inc function overloads the ++ operator for the Counter class. It increments the value property and returns the updated Counter object. Similarly, the dec function overloads the -- operator for decrementing.
When the increment or decrement operator is used, the semantics depend on whether it is used before or after the operand. The post-increment operator (counter++) evaluates the expression and then increments the value. The pre-increment operator (++counter) increments the value and then evaluates the expression.
Overloading comparison operators
In Kotlin, you can overload comparison operators (such as ==, !=, >, <, >=, <=) for any object, not just for primitive types. This allows for intuitive and concise comparisons.
Equality operators: “equals”
Using the == operator in Kotlin is translated into a call of the equals method, also using the != operator is also translated into a call of equals, with the obvious difference that the result is inverted.
An equality check == is transformed into an equals call and a null check
Equality operators (== and !=) can be used with nullable operands because they check for equality to null internally. When using the comparison a == b, it checks whether a is not null and, if it\’s not, calls a.equals(b). If both arguments are null references, the result is true.
To check if two objects refer to the same instance, you can use the identity equals operator (===). It behaves similarly to the == operator in Java and checks if both arguments reference the same object. However, note that the === operator cannot be overloaded.
The equals function is used to implement the equality comparison. It is marked as override because it is defined in the Any class, which is the base class for all objects in Kotlin. You don\’t need to mark it as an operator because the base method in Any is already marked as such, and the operator modifier applies to all methods that implement or override it. It is important to note that equals cannot be implemented as an extension function since the inherited implementation from Any would always take precedence over the extension.
Ordering operators: compareTo
For ordering operators (<, >, <=, >=), Kotlin supports the Comparable interface. The compareTo method defined in this interface can be called by convention, and the usage of comparison operators is translated into calls to compareTo. The return type of compareTo must be Int. For example, the expression p1 < p2 is equivalent to p1.compareTo(p2) < 0. Other comparison operators work in a similar way.
Comparison of two objects is transformed into comparing the result of the compareTo call with zero
Here’s an example of implementing the compareTo function for a Person class, using address book ordering:
In the above example, the compareTo function compares Person objects based on their last names first. If the last names are the same, it compares based on their first names.The Comparable interface is implemented to enable comparisons using the concise operator syntax.
It’s important to note that you don’t need to add any extensions to compare Java classes that implement the Comparable interface.
Kotlin
println("abc" < "bac") //true
Kotlin supports the operator syntax for these comparisons out of the box.
Conventions used for collections and ranges
Some of the most common operations for working with collections are getting and setting elements by index, as well as checking whether an element belongs to a collection. All of these operations are supported via operator syntax: To get or set an element by index, you use the syntax a[i] (called the index operator). The in operator can be used to check whether an element is in a collection or range and also to iterate over a collection. You can add those operations for your own classes that act as collections. Let’s now look at the conventions used to support those operations.
Accessing elements by index: “get” and “set”
In Kotlin, you can access and modify values in a map or a custom class using the indexing operator [] and the set function. Let\’s look at how to implement these conventions:
Implementing the get operator:
Access via square brackets is transformed into a get function call
To access a value from a map using the indexing operator, you can define the get function in your class.
The get function takes the key as a parameter and returns the corresponding value.
You can define the get function with a single parameter of the key type and mark it as operator.
Inside the get function, you can handle different cases based on the key value and return the appropriate result.
Assignment through square brackets is transformed into a set function call
To change the value for a key in a mutable map or a custom class, you can define the set function.
The set function takes the key and the new value as parameters and updates the corresponding value.
You can define the set function with two parameters (index and value) and mark it as operator.
Inside the set function, you can handle different cases based on the index and update the value accordingly.
Kotlin
operatorfunMutablePoint.set(index: Int, value: Int) {when (index) {0-> x = value1-> y = valueelse->throwIndexOutOfBoundsException("Invalid coordinate $index") }}
With these implementations, you can use the indexing operator [] on objects of your class to access or modify specific values. For example:
Kotlin
val p = Point(5, 10)val xCoordinate = p[0] // Accessing the x-coordinate of Point using the indexing operatorp[1] = 20// Modifying the y-coordinate of Point using the indexing operatorval mutableP = MutablePoint(3, 7)mutableP[0] = 15// Modifying the x-coordinate of MutablePoint using the indexing operator
By adhering to the conventions and implementing the get and set functions, you enable the use of square brackets for element access and modification, providing a familiar and intuitive syntax for working with maps or custom collection-like classes.
The “in” convention
In Kotlin, the in operator is used to check whether an object belongs to a collection or range. The corresponding function for the in operator is contains. Here’s how you can use the in operator and implement the contains function:
Using the in operator:
The in operator is transformed into a contains function call
The object on the right side of the in operator is the object on which the contains method will be called.
The object on the left side of the in operator becomes the argument passed to the contains method.
The in operator returns a Boolean value indicating whether the object is present in the collection or range.
Kotlin
val list = listOf(1, 2, 3, 4, 5)val number = 3val isInList = number in list // Checking if number is in the listval range = 1..10val point = 7val isInRange = point in range // Checking if point is in the range
Implementing the contains function:
If you want to use the in operator with custom classes, you need to implement the contains function in your class.
The contains function takes a single parameter that represents the object being checked for membership.
Inside the contains function, you can define the logic to determine whether the object is present in the collection or range.
The function should return a Boolean value indicating the result of the membership check.
Kotlin
dataclassRectangle(val left: Int, val top: Int, val right: Int, val bottom: Int) {operatorfunRectangle.contains(p: Point): Boolean {return p.x in upperLeft.x until lowerRight.x && p.y in upperLeft.y until lowerRight.y }}val rect = Rectangle(Point(10, 20), Point(50, 50))println(Point(20, 30) in rect) // trueprintln(Point(5, 5) in rect) // false
In the Rectangle class, the contains function checks if a given Point falls within the range of the rectangle. By using the open range (until), the condition point.x in left until right ensures that the x coordinate of the point is greater than or equal to left but less than right. Similarly, the condition point.y in top until bottom checks if the y coordinate of the point is greater than or equal to top but less than bottom.
In the implementation of Rectangle .contains, you used the until standard library function to build an open range and then you use the in operator on a range to check that a point belongs to it.
An open range is a range that doesn’t include its ending point. For example, if you build a regular (closed) range using 10..20, this range includes all numbers from 10 to 20, including 20. An open range 10 until 20 includes numbers from 10 to 19 but doesn’t include 20. A rectangle class is usually defined in such a way that its bottom and right coordinates aren’t part of the rectangle, so the use of open ranges is appropriate here.
The rangeTo convention
To create a range in Kotlin, you can use the .. syntax. For example, 1..10 represents a range that includes all the numbers from 1 to 10. This syntax is a concise way to call the rangeTo function, which returns a range.
The .. operator is transformed into a rangeTo function call
The rangeTo function is defined in the Kotlin standard library and can be called on any comparable element. It has the following signature:
This function returns a ClosedRange that allows you to check whether different elements belong to it.
Here’s an example of how you can use the rangeTo function to create a range:
Kotlin
import java.time.LocalDatefunmain() {val now = LocalDate.now()val futureRange = now.rangeTo(now.plusDays(10))for (date in futureRange) {println(date) }}
In this example, now.rangeTo(now.plusDays(10)) creates a range of LocalDate objects from the current date (now) to 10 days in the future. The range includes both the starting and ending dates. The rangeTo function isn’t a member of LocalDate but rather is an extension function on Comparable.
When using the rangeTo operator, it\’s recommended to use parentheses to clearly separate the range expression from other operations. For example:
Kotlin
val range = (start..end).step(2)
Additionally, if you want to call a method on a range, make sure to surround the range expression with parentheses to avoid compilation errors. For example:
Kotlin
(0..n).forEach { print(it) }
By using parentheses, you ensure that the forEach method is called on the range expression (0..n).
The “iterator” convention for the “for” loop
In Kotlin, the for loop uses the in operator for iteration. The meaning of the in operator in this context is different from its use with ranges. Here it is used to perform an iteration over a collection or a range.
When you write a for loop like for (x in list) { ... }, it is translated into a call to the iterator() method on the list object. This method returns an iterator on which the hasNext() and next() methods are called repeatedly to iterate over the elements of the collection.
The iterator() method can be defined as an extension function in Kotlin, which explains why you can iterate over a regular Java string. The Kotlin standard library provides an extension function iterator() on CharSequence, which is a superclass of String. It returns a CharIterator that allows iterating over the characters of the string.
Kotlin
operatorfunCharSequence.iterator(): CharIterator<br><br>// for (c in "resume sender app") {}
Here’s an example that demonstrates iterating over a custom range type, ClosedRange<LocalDate>:
Kotlin
operatorfunClosedRange<LocalDate>.iterator(): Iterator<LocalDate> = object : Iterator<LocalDate> {var current = startoverridefunhasNext() = current <= endInclusiveoverridefunnext() = current.apply { current = plusDays(1) } }}funmain() {val newYear = LocalDate.ofYearDay(2017, 1)val daysOff = newYear.minusDays(1)..newYearfor (dayOff in daysOff) {println(dayOff) }}// OUTPUT// 2016-12-31// 2017-01-01
In this example, the ClosedRange<LocalDate> type is extended with the iterator() method, which returns a custom iterator implementation. The iterator starts from the start date and increments by one day until it reaches the endInclusive date.
The daysOff range represents the day before and New Year day. The for loop then iterates over the dates in the range, printing each day off.
Note how you define the iterator method on a custom range type: you use LocalDate as a type argument. The rangeTo library function, shown in the previous section, returns an instance of ClosedRange, and the iterator extension on ClosedRange allows you to use an instance of the range in a for loop.
By defining the iterator() extension function on a custom range type, you can use an instance of the range in a for loop to perform iteration.
Destructuring declarations and component functions
Destructuring declarations in Kotlin allow you to unpack a single composite value and initialize multiple separate variables with its contents. Here’s how it works:
Destructuring declarations look like regular variable declarations, but with multiple variables grouped in parentheses.
Under the hood, the principle of conventions is used. To initialize each variable in a destructuring declaration, a function named componentN is called, where N is the position of the variable in the declaration.
Destructuring declarations are transformed into componentN function calls
For data classes, the compiler automatically generates componentN functions for every property declared in the primary constructor. However, for non-data classes, you need to manually define these functions. Here\’s an example:
In the example above, the Point class has two properties, x and y. The component1() function returns the value of x, and the component2() function returns the value of y. These functions enable destructuring declarations to work with instances of the Point class.
One of the main use cases for destructuring declarations is returning multiple values from a function. You can define a data class to hold the values you want to return and use it as the return type of the function. The destructuring declaration syntax allows you to easily unpack and use the returned values. Let’s take an example of splitting a filename into a name and an extension:
In the splitFilename function, the fullName is split into a list of two elements: the name and the extension. The destructuring declaration val (name, extension) = ... allows us to directly assign these values to the name and extension variables. The function then returns an instance of the NameComponents data class.
Of course, it’s not possible to define an infinite number of such componentN functions so the syntax would work with an arbitrary number of items, but that wouldn’t be useful, either. The standard library allows you to use this syntax to access the first five elements of a container
If you need to return more than two or three values, Kotlin provides the Pair and Triple classes from the standard library. These classes allow you to package multiple values together without defining your own custom class. However, using custom data classes is generally preferred as they provide better clarity about the contained values.
Destructuring declarations and loops
Destructuring declarations can be used not only as top-level statements in functions but also in other places where variable declarations are allowed, such as loops. One useful application is when iterating over entries in a map. Here’s an example that demonstrates this syntax by printing all entries in a given map:
In this example, the printEntries function takes a Map as input. The for loop uses a destructuring declaration (key, value) to unpack each entry in the map. The key variable will hold the key of the current entry, and the value variable will hold the corresponding value. The body of the loop then prints the key-value pair.
Under the hood, Kotlin provides two conventions for achieving this behavior. First, the Kotlin standard library includes an extension function called iterator on the Map interface, which returns an iterator over the map entries. This allows you to iterate directly over the map in a loop.
Second, the Map.Entry interface, representing a key-value pair in the map, has extension functions component1 and component2. These functions return the key and value, respectively, of the Map.Entry instance. The destructuring declaration in the loop internally calls these functions to assign the key and value to the declared variables.
In essence, the loop using destructuring declarations is equivalent to the following code:
Kotlin
for (entry in map.entries) {val key = entry.component1()valvalue = entry.component2()// ...}
This example highlights the significance of extension functions in Kotlin conventions. The extension functions iterator, component1, and component2 enable the concise and readable syntax for iterating over map entries and accessing their key-value pairs.
Conclusion
Conventions in Kotlin provide a powerful way to customize the behavior of language constructs and enhance code readability. By leveraging operator overloading and following function naming conventions, you can create expressive and intuitive APIs. Understanding and utilizing these conventions will help you write clean, concise, and idiomatic Kotlin code.
In this article, we covered various aspects of conventions in Kotlin, including operator overloading conventions for arithmetic operations, comparison operators, indexing, and containment. We also explored Conventions used for collections and ranges and Destructuring declarations.
Remember to practice using conventions in your Kotlin projects to improve code quality and maintainability. Happy coding!
Kotlin offers various powerful features to make code concise and efficient. One such feature is delegation, which allows you to delegate the implementation of properties or functions to another object. This concept of delegation plays a crucial role in achieving code reuse, separation of concerns, and enhancing the readability and maintainability of your code. In this blog, we will explore Kotlin delegation and delve into the details of delegated properties.
Delegated properties allow you to leverage the power of trusted helper objects called delegates. These delegates handle complex tasks, freeing up your properties to focus on their core responsibilities. From database tables to browser sessions and maps, the possibilities are endless.
Join me as we embark on this thrilling journey, exploring the art of delegation and unlocking the true potential of Kotlin delegation properties. Get ready to witness the magic as your properties become extraordinary with just a touch of delegation!
Understanding Kotlin Delegation
Delegation in programming is a design pattern where an object, known as the delegate, is given the responsibility to handle certain tasks or operations on behalf of another object, known as the delegator. The delegator object delegates the work to the delegate object, which performs the task and returns the result to the delegator.
Let’s use a real-life example to understand delegation in Kotlin. Consider a scenario where you have a restaurant with a customer, a waiter, and a chef. The customer wants to order a meal, and the waiter is responsible for taking the order and delivering it to the chef. The chef prepares the meal and hands it back to the waiter, who serves it to the customer.
In this example, the customer is the delegator, and the waiter is the delegate. The customer delegates the task of taking the order and delivering it to the waiter. The waiter performs these tasks on behalf of the customer and then delegates the task of preparing the meal to the chef. Finally, the waiter serves the meal back to the customer.
Let’s take one more example, consider a Car class that needs to perform some operations related to engine management. Instead of implementing those operations directly in the Car class, we can delegate them to an Engine object. This way, the Car class can focus on its core functionality, while the Engine object handles engine-related tasks.
Delegation provides benefits such as modularity, maintainability, and flexibility in designing software systems.
Overview of the Delegation Pattern
The delegation pattern is a design pattern where an object delegates some or all of its responsibilities to another object. Instead of inheriting behavior, an object maintains a reference to another object and forwards method calls to it. This promotes composition over inheritance and provides greater flexibility in reusing and combining behaviors from different objects.
In Kotlin, the delegation pattern is built into the language, making it easy and convenient to implement. With the by keyword, Kotlin allows a class to implement an interface by delegating all of its public members to a specified object. Let’s dive into the details and see how it works.
Basic Usage of Delegation in Kotlin
To understand the basic usage of delegation in Kotlin, let’s consider a simple example. Assume we have an interface called Base with a single function print()
Kotlin
interfaceBase {funprint()}
Next, we define a class BaseImpl that implements the Base interface. It has a constructor parameter x of type Int and provides an implementation for the print() function.
Kotlin
classBaseImpl(val x: Int) : Base {overridefunprint() {println(x) }}
Now, we want to create a class called Derived that also implements the Base interface. Instead of implementing the print() function directly, we can delegate it to an instance of the Base interface. We achieve this by using the by keyword followed by the object reference in the class declaration.
Kotlin
classDerived(b: Base) : Basebyb
In this example, the by clause in the class declaration indicates that b will be stored internally in objects of Derived, and the compiler will generate all the methods of Base that forward to b. This means that the print() function in Derived will be automatically delegated to the print() function of the b object.
To see the delegation in action, let’s create an instance of BaseImpl with a value of 10 and pass it to the Derived class. Then, we can call the print() function on the Derived object:
When we execute the print() function on the Derived object, it internally delegates the call to the BaseImpl object (b), and thus it prints the value 10.
Overriding Methods in Delegation
In Kotlin, when a class implements an interface by delegation, it can also override methods provided by the delegate object. This allows for customization and adding additional behavior specific to the implementing class.
Let’s extend our previous example to understand method overriding in the delegation. Assume we have an interface Base with two functions: printMessage() and printMessageLine():
In this example, the printMessage() function in the Derived class overrides the implementation provided by the delegate object b. When we call printMessage() on an instance of Derived, it will print “softAai” instead of the original implementation.
To test the overridden behavior, we can modify the main() function as follows:
When we call the printMessage() function on the Derived object, it invokes the overridden implementation in the Derived class, and it prints “softAai” instead of 10. However, the printMessageLine() function is not overridden in the Derived class, so it delegates the call to the BaseImpl object, which prints the original value 10 followed by a new line.
Property Delegation
In addition to method delegation, Kotlin also supports property delegation. This allows a class to delegate the implementation of properties to another object. Let’s understand how it works.
Assume we have an interface Base with a read-only property message:
Kotlin
interfaceBase {val message: String}
We modify the BaseImpl class to implement the Base interface with the message property:
Kotlin
classBaseImpl(val x: Int) : Base {overrideval message: String = "BaseImpl: x = $x"}
Now, let’s update the Derived class to delegate the Base interface and override the message property:
In this example, the Derived class delegates the implementation of the Base interface to the b object. However, it overrides the message property and provides its own implementation.
To see the property delegation in action, we can modify the main() function as follows:
Kotlin
funmain() {val b = BaseImpl(10)val derived = Derived(b)println(derived.message) // Output: Message of Derived}
When we access the message property of the Derived object, it returns the overridden value “Message of Derived” instead of the one in the delegate object b.
Delegated Properties in Kotlin
Delegated properties allow you to delegate the implementation of property accessors (getters and setters) to another object. This means that instead of writing the logic for accessing and setting the property directly in the class, you can delegate it to a separate class.
The general syntax for creating a delegated property is as follows:
Kotlin
classMyClass {var myProperty: TypebyDelegate()}
Here, myProperty delegates its getter and setter operations to the Delegate object.
Property Delegates
Property delegates are classes that implement the getValue and optionally setValue functions. These functions are invoked when the delegated property is accessed or modified.
The getValue function is responsible for returning the property value, and setValue is responsible for updating the property value.
Let’s say we have a Delegate class that will handle the logic for accessing and setting a property. The Delegate class should have two methods: getValue() and setValue(). The getValue() method retrieves the current value of the property, and the setValue() method sets a new value for the property. These methods can be defined as either members or extensions of the Delegate class.
Here’s an example of a Delegate class that simply stores the value internally:
Kotlin
classDelegate {privatevarvalue: Int = 0operatorfungetValue(thisRef: Any?, property: KProperty<*>): Int {// Get the current value of the propertyreturnvalue }operatorfunsetValue(thisRef: Any?, property: KProperty<*>, newValue: Int) {// Set a new value for the propertyvalue = newValue }}
In the above code snippet, the * is used in the parameter of the setValue() and getValue() methods of the Delegate class. This syntax is called a star-projection or star-spread operator.
In Kotlin, the KProperty interface represents a property, and it has a type parameter T that represents the type of property. When you use the star-projection (*) as the type argument (KProperty<*>), it means you are using a wildcard or an unknown type for the property.
In the context of delegated properties, the KProperty<*> parameter represents the property that is being accessed or set. The thisRef parameter represents the instance of the class that owns the property.
By using KProperty<*> with the star-projection, you’re saying that the property can be of any type. It allows you to create a generic Delegate class that can handle properties of different types without explicitly specifying the type.
Also, we defined the operator keyword, which is used to define and overload certain operators for custom types or classes.
Now, let’s create a class Foo with a property p that delegates its accessors to an instance of the Delegate class:
When you create an instance of Foo, you can access and modify the p property as if it were a regular property. However, behind the scenes, the access and modification operations are delegated to the Delegate class.
Let’s consider one more example for better understanding, just create a simple UpperCaseDelegate that converts a string property to uppercase when accessed:
Here, myProperty delegates its getter operation to UpperCaseDelegate, which converts the value to uppercase before returning it.
The “by" keyword
The delegate object is specified after the by keyword and can be any object that satisfies the rules of the convention for property delegates. It’s the delegate object that actually handles the logic for accessing and setting the property.
The by keyword is used in Kotlin to indicate that a property is delegated to another object or class. When you use the by keyword, you are specifying that the implementation of the property’s accessors (getters and setters) will be delegated to a separate delegate object.
By using the by keyword, you make it clear that the property’s implementation is delegated to another object, enhancing code readability and allowing for better separation of concerns.
Kotlin
classDelegate {privatevarvalue: Int = 0operatorfungetValue(thisRef: Any?, property: KProperty<*>): Int {// Get the current value of the propertyreturnvalue }operatorfunsetValue(thisRef: Any?, property: KProperty<*>, newValue: Int) {// Set a new value for the propertyvalue = newValue }}classFoo {var p: IntbyDelegate()}
In the above example, the p property of the Foo class is delegated to an instance of the Delegate class using the by keyword. The Delegate class provides the implementation for the property’s accessors.
So, when you access or modify the p property of an instance of the Foo class, the property accessors are automatically delegated to the Delegate object. Behind the scenes, the getValue() and setValue() methods of the Delegate class are called to handle the property operations.
Using the by keyword simplifies the syntax and makes it clear that the property behavior is delegated to another object. It promotes code reuse and separates the concerns of the owning class from the delegate class.
Standard Delegates
Kotlin provides several standard delegates in the standard library to address common scenarios. Some examples include:
lazy:Allows for lazy initialization of properties. The initialization is deferred until the property is accessed for the first time.
observable:Enables observing property changes by providing a callback function that is triggered whenever the property value is modified.
vetoable:Allows validation of property values by providing a callback function that can reject value changes based on specific conditions.
Lazy Initialization
Lazy initialization is a technique where you delay the initialization of an object until it is accessed for the first time. This can be useful when the initialization process is resource-intensive and the object might not always be needed during the lifetime of the program.
For example, consider a Person class that lets you access a list of the emails written by a person. The emails are stored in a database and take a long time to access. You want to load the emails on first access to the property and do so only once. Let’s say you have the following function loadEmails, which retrieves the emails from the database:
Kotlin
classEmail {/*...*/}funloadEmails(person: Person): List<Email> {println("Load emails for ${person.name}")returnlistOf(/*...*/ )}
Here’s how you can implement lazy loading using anadditional _emails property that stores null before anything is loaded and the list of emails afterward.
In this implementation, we have a nullable _emails property that acts as a backing property to store the loaded emails. The emails property is the one we access to retrieve the list of emails. In the getter of the emails property, we check if _emails is null. If it is, we initialize it by calling the loadEmails function. We then return the value of _emails, forcibly unwrapping it with !! operator since we know it won’t be null at this point.
While this approach works, it can become cumbersome and error-prone when dealing with multiple lazy properties. Additionally, the implementation is not thread-safe.
To simplify and improve the code, Kotlin provides a built-in solution using the lazy delegate. The lazy function returns an object that has a getValue method, which can be used together with the by keyword to create a delegated property. Here’s how we can use it in our example:
With this implementation, the emails property is delegated to the lazy delegate. The lambda expression passed to the lazy function is used to initialize the value of the property when it is accessed for the first time. The lazy delegate ensures that the initialization happens only once, and subsequent accesses to the property will return the cached value.
The lazy function is thread-safe by default, meaning that the initialization is synchronized and can be safely accessed from multiple threads. If you need more control over the thread-safety or want to optimize for a single-threaded environment, you can specify additional options to the lazy function.
Lazy delegate: “by lazy()”
In Kotlin, you can achieve lazy initialization using the lazy delegate provided by the standard library. The lazy function returns an object that has a getValue method, which can be used together with the by keyword to create a delegated property.
The lazy delegate is a built-in feature in Kotlin that allows you to create properties whose values are computed lazily. It provides a concise way to implement lazy initialization without manually managing the initialization state. Here’s how you can use it:
Kotlin
val property: Typebylazy {// Initialization code here// This block will be executed only once, when the property is accessed for the first time// The value of the block will be cached and returned for subsequent accesses// Return the computed value}
Here’s an example to illustrate the usage of lazy initialization with the lazy delegate:
Kotlin
classExample {val expensiveProperty: Intbylazy {// Expensive computation or initializationprintln("Initializing expensiveProperty...")// Return the computed value42 }}funmain() {val example = Example()println("Before accessing expensiveProperty")// The initialization code of expensiveProperty is not executed yetprintln("Value of expensiveProperty: ${example.expensiveProperty}")// The initialization code of expensiveProperty is executed hereprintln("After accessing expensiveProperty")println("Value of expensiveProperty: ${example.expensiveProperty}")// The cached value is returned without re-initialization}
Kotlin
OUTPUTBefore accessing expensivePropertyInitializing expensiveProperty...Value of expensiveProperty: 42After accessing expensivePropertyValue of expensiveProperty: 42
In this example, the expensiveProperty in the Example class is lazily initialized using the lazy delegate. The initialization code block is not executed until the property is accessed for the first time. The computed value (42 in this case) is then cached and returned for subsequent accesses.
When you run the above code, you’ll see that the initialization code block is executed only once, when the property is first accessed. On subsequent accesses, the cached value is returned without re-executing the initialization code.
Lazy initialization with by lazy() simplifies the code by abstracting away the details of managing the initialization state and caching the computed value. It ensures that the property is initialized lazily and provides a convenient way to implement lazy initialization in Kotlin.
Once again here’s an example to illustrate lazy initialization using the lazy delegate:
In this example, the Person class has a property called emails, which is lazily initialized using the lazy delegate. The lazy function takes a lambda as an argument, which it will call to initialize the value of the property when it is accessed for the first time.
The benefit of using the lazy delegate is that the initialization logic is encapsulated within it. The value assigned to the emails property will only be computed once, on the first access, and subsequent accesses will return the cached value. This can help improve performance by avoiding unnecessary computations or resource allocations until they are actually needed.
You can think of the emails property as having a backing property that holds the computed value, and the lazy delegate takes care of initializing and caching the value behind the scenes. The delegate ensures that the value is computed lazily, i.e., only when it is first accessed.
Here’s how you would use the Person class:
Kotlin
val person = Person("amol")println(person.emails) // Initialization happens here, loadEmails() is calledprintln(person.emails) // Cached value is returned without re-initialization
In this example, the loadEmails() function will only be called on the first access of the emails property. Subsequent accesses will return the cached value without re-initializing it.
The lazy delegate is thread-safe by default, meaning that the initialization is synchronized and can be safely accessed from multiple threads. However, if you know that the class will only be used in a single-threaded environment, you can provide additional options to bypass synchronization and improve performance.
The lazy delegate allows you to achieve lazy initialization of properties. It simplifies the code by encapsulating the initialization logic and ensures that the value is computed only when it is first accessed, providing better performance and resource utilization.
"observable” Delegate
The observable delegate allows you to observe property changes by providing a callback function that is triggered whenever the property value is modified.
Here’s the general syntax for using the observable delegate:
Kotlin
var propertyName: TypebyDelegates.observable(initialValue) { property, oldValue, newValue ->// Callback function logic}
Let’s see an example that uses the observable delegate to observe changes in a property:
Kotlin
import kotlin.properties.DelegatesclassPerson {var age: IntbyDelegates.observable(25) { property, oldValue, newValue ->println("Age changed from $oldValue to $newValue") }}funmain() {val person = Person() person.age = 30// Output: Age changed from 25 to 30 person.age = 35// Output: Age changed from 30 to 35}
In this example, the age property is observed using the observable delegate. Whenever the age property is modified, the callback function is triggered, printing the old value and the new value.
"vetoable” Delegate
The vetoable delegate allows you to validate property values by providing a callback function that can reject value changes based on specific conditions.
Here’s the general syntax for using the vetoable delegate:
Kotlin
var propertyName: TypebyDelegates.vetoable(initialValue) { property, oldValue, newValue ->// Validation logic// Return true to accept the new value, or false to reject it}
Let’s see an example that uses the vetoable delegate to validate a property value:
Kotlin
import kotlin.properties.DelegatesclassCircle {var radius: DoublebyDelegates.vetoable(0.0) { property, oldValue, newValue -> newValue >= 0.0// Only accept positive or zero radius values }}funmain() {val circle = Circle() circle.radius = 5.0println(circle.radius) // Output: 5.0 circle.radius = -2.0// Value rejected due to validationprintln(circle.radius) // Output: 5.0 (unchanged)}
In this example, the radius property is validated using the vetoable delegate. The callback function checks if the new value is greater than or equal to zero. If the validation condition is not met (e.g., negative radius), the value change is rejected, and the property retains its previous value.
Delegating to another property
Delegating a property to another property means that the getter and setter of one property are implemented by accessing or modifying another property’s value. This delegation can be done for top-level properties, member properties (including extension properties) within the same class, or even member properties of another class.
To delegate a property to another property, you use the :: qualifier followed by the delegate property’s name. Here are a few examples to illustrate how property delegation works:
Kotlin
var topLevelInt: Int = 0classClassWithDelegate(val anotherClassInt: Int)classMyClass(var memberInt: Int, val anotherClassInstance: ClassWithDelegate) {var delegatedToMember: Intbythis::memberIntvar delegatedToTopLevel: Intby ::topLevelIntval delegatedToAnotherClass: IntbyanotherClassInstance::anotherClassInt}var MyClass.extDelegated: Intby ::topLevelInt
In the code above, we have different scenarios for property delegation:
delegatedToMember is a property within the MyClass class that delegates its getter and setter to the memberInt property of the same class. This means that accessing or modifying delegatedToMember will actually read from or write to memberInt.
delegatedToTopLevel is a property within the MyClass class that delegates its getter and setter to the top-level property topLevelInt. So, accessing or modifying delegatedToTopLevel will actually read from or write to topLevelInt.
delegatedToAnotherClass is a property within the MyClass class that delegates its getter to the anotherClassInt property of an instance of ClassWithDelegate. This means that accessing delegatedToAnotherClass will read the value of anotherClassInstance.anotherClassInt.
extDelegated is an extension property of MyClass that delegates its getter and setter to the top-level property topLevelInt. This allows instances of MyClass to have an additional property extDelegated that shares its value with topLevelInt.
Property delegation can be useful in various scenarios. One common use case is when you want to introduce a new property while maintaining backward compatibility with an existing one. In such cases, you can introduce a new property, annotate the old property with the @Deprecated annotation, and delegate its implementation to the new property. Here’s an example:
Kotlin
classMyClass {var newName: Int = 0@Deprecated("Use 'newName' instead", ReplaceWith("newName"))var oldName: Intbythis::newName}funmain() {val myClass = MyClass()// Notification: 'oldName: Int' is deprecated.// Use 'newName' instead myClass.oldName = 42println(myClass.newName) // Output: 42}
In this example, we have a class MyClass with oldName and newName properties. The oldName property is deprecated and annotated with @Deprecated, indicating that it should not be used anymore. The implementation of oldName is delegated to the newName property using by this::newName. So, accessing or modifying oldName will actually access or modify the newName property.
In the main function, we demonstrate the usage of the deprecated oldName property. When assigning a value to oldName, a deprecation warning is displayed. However, the value is stored in the newName property, which can be accessed correctly.
Overall, delegating properties to other properties provides a powerful mechanism to reuse existing property implementations, introduce backward compatibility, and simplify property access and modification.
Property delegate requirements
Property delegate requirements will be demonstrated for both read-only (val) and mutable (var) properties. Let’s break down the concept of delegated properties for read-only and mutable properties (var) and understand how to provide the necessary operator functions for delegation.
For a read-only property (val), the delegate must provide the getValue() operator function with the following parameters:
thisRef: This parameter should be the same type as, or a supertype of, the property owner (for extension properties, it should be the type being extended).
property: This parameter should be of type KProperty<*> or its supertype.
getValue(): This function must return the same type as the property (or its subtype).
In this code, we have the Owner class with a read-only property valResource. The delegation is done by using the by keyword and providing an instance of the ResourceDelegate class. The ResourceDelegate class defines the getValue() function, which returns an instance of Resource. The function receives the property owner (thisRef) and the KProperty<*> instance representing the property being delegated.
For a mutable property (var), in addition to the getValue() function, the delegate must provide the setValue() operator function with the following parameters:
thisRef: This parameter should be the same type as, or a supertype of, the property owner (for extension properties, it should be the type being extended).
property: This parameter should be of type KProperty<*> or its supertype.
value: This parameter should be of the same type as the property (or its supertype).
In this code, we have the Owner class with a mutable property varResource. The ResourceDelegate class now includes the setValue() function, which allows modifying the value of the delegated property. The function receives the property owner (thisRef), the KProperty<*> instance representing the property being delegated, and the new value to be assigned.
You can define the getValue() and setValue() functions as member functions of the delegate class itself or as extension functions. Both functions need to be marked with the operator keyword to enable operator overloading.
Alternatively, you can create delegates as anonymous objects using the interfaces ReadOnlyProperty and ReadWriteProperty from the Kotlin standard library. These interfaces provide the required getValue() and setValue() methods. By using anonymous objects, you can avoid creating separate classes for the delegates. Here’s an example:
Kotlin
funresourceDelegate(resource: Resource = Resource()): ReadWriteProperty<Any?, Resource> = object : ReadWriteProperty<Any?, Resource> {privatevar curValue = resourceoverridefungetValue(thisRef: Any?, property: KProperty<*>): Resource = curValueoverridefunsetValue(thisRef: Any?, property: KProperty<*>, value: Resource) { curValue = value } }val readOnlyResource: ResourcebyresourceDelegate() // ReadWriteProperty used as a read-only propertyvar readWriteResource: ResourcebyresourceDelegate() // ReadWriteProperty used as a mutable property
In this code, the resourceDelegate() function returns an anonymous object implementing the ReadWriteProperty interface. The ReadWriteProperty interface extends ReadOnlyProperty, so it can be used as a delegate for both read-only and mutable properties. The anonymous object defines the necessary getValue() and setValue() functions.
By using delegated properties and providing the appropriate operator functions, you can create flexible and reusable property delegation patterns in Kotlin.
Storing property values in a map
Another common pattern where delegated properties come into play is objects that have a dynamically defined set of attributes associated with them. Such objects are sometimes called expando objects. in a contact-management system, each person may have some required properties (like name) that are handled in a special way, as well as additional attributes that can vary for each person(youngest child’s birthday, for example).
One way to implement such a system is by using a map to store all the attributes of a person and providing properties that allow access to the information with special handling. Let’s go through the code examples to understand this approach.
First, we have the Person class with a private _attributes map. This map will store the attributes of a person, where the keys are attribute names and the values are attribute values.
In this code, we have a set attributefunction that allows adding or updating attributes in the _attributes map. The name property is an example of a required property that is handled in a special way. It retrieves the value of the “name” attribute from the _attributes map.
To create an instance of the Person class and load data into it, we can use a generic API, such as deserialization from JSON, as shown in the example below:
Kotlin
val p = Person()valdata = mapOf("name" to "amol", "company" to "softAai")for ((attrName, value) indata) { p.setAttribute(attrName, value)}println(p.name) // Output: amol
Here, we create a new Person instance and provide the data as a map. We iterate over each key-value pair in the data map and call setAttribute to store the attributes in the _attributes map. Finally, we can access the name property, which internally retrieves the value of the “name” attribute from the _attributes map.
Now, instead of manually implementing the property and the _attributes map, we can simplify the code using delegated properties. We can directly delegate the name property to the _attributes map using the by keyword, as shown below:
In this code, we no longer have the explicit getter for the name property. Instead, we use the by keyword to delegate the property to the _attributes map. The standard library provides getValue and setValue extension functions for maps, allowing the property to automatically get and set the values in the map based on the property name.
With the delegated property in place, we can use it just like before:
Kotlin
val p = Person()valdata = mapOf("name" to "amol", "company" to "softAai")for ((attrName, value) indata) { p.setAttribute(attrName, value)}println(p.name) // Output: amol
The output remains the same, but now the name property is implemented as a delegated property, simplifying the code and removing the need for an explicit getter.
Delegated properties provide a concise and reusable way to handle dynamically defined attributes in expando objects. By leveraging the by keyword and the standard library extension functions, we can delegate the property access to a map or any other custom logic, making the code more maintainable and flexible.
Translation rules for delegated properties
When using delegated properties in Kotlin, the Kotlin compiler generates auxiliary properties to handle the delegation. These auxiliary properties are used to store the delegate object and manage the getter and setter operations.
Let’s take an example to understand how this works. Consider the following code:
Kotlin
classC {var prop: TypebyMyDelegate()}
When the compiler encounters this code, it generates a hidden property called prop$delegate. This hidden property is of the same type as the delegate class (MyDelegate in this case). It is responsible for handling the delegation of the prop property.
In the generated code, the prop property has a getter and a setter. The getter delegates the getValue() operation to the prop$delegate property, passing the instance of the outer class (this) and the reflection object (this::prop) that represents the property itself. The delegate’s getValue() function is responsible for providing the value of the property.
Similarly, the setter delegates the setValue() operation to the prop$delegate property, passing the instance of the outer class (this), the reflection object (this::prop), and the new value of the property. The delegate’s setValue() function handles the assignment of the new value.
By generating the prop$delegate property and delegating to it, the compiler ensures that the getter and setter operations are correctly handled by the delegate object (MyDelegate).
Optimized cases for delegated properties
When it comes to optimization, the Kotlin compiler can omit the $delegate field in certain cases. Here are the optimized cases for delegated properties:
A referenced property:
Kotlin
classC<Type> {privatevar impl: Type = ...var prop: Typeby ::impl}
In this case, the property prop is delegated to another property impl using the by keyword. Since the delegate is a referenced property within the same class, the compiler can optimize the generated code and omit the $delegate field. Instead, the accessors directly delegate to the referenced property impl.
When using a named object as a delegate, the Kotlin compiler can optimize the code and omit the $delegate field. The accessors directly call the delegate’s getValue function without the need for an intermediate property.
A final val property with a backing field and a default getter in the same module:
Kotlin
val impl: ReadOnlyProperty<Any?, String> = ...classA {val s: Stringbyimpl}
In this case, the delegate impl is a final val property with a backing field and a default getter defined in the same module as the property s. The compiler can optimize the code and omit the $delegate field. The accessors directly delegate to the getValue function of the delegate without the need for an intermediate property.
A constant expression, enum entry, this, or null:
Kotlin
classA {operatorfungetValue(thisRef: Any?, property: KProperty<*>) ...val s bythis}
If the delegate is a constant expression, an enum entry, this, or null, the Kotlin compiler can optimize the code and omit the $delegate field. The accessors directly call the getValue function of the delegate without the need for an intermediate property.
In these optimized cases, the compiler eliminates the need for the $delegate field, which can save memory and provide more efficient property access. The accessors directly invoke the corresponding functions of the delegate, leading to more streamlined code execution.
Note that these optimizations are applied by the Kotlin compiler to improve performance and reduce unnecessary overhead when using delegated properties.
Translation rules when delegating to another property
When delegating to another property, the Kotlin compiler optimizes the code by generating immediate access to the referenced property. This means that the compiler doesn’t generate the $delegate field. This optimization helps save memory and improves performance.
Let’s take a look at the example code:
Kotlin
classC<Type> {privatevar impl: Type = ...var prop: Typeby ::impl}
In this case, the property prop is delegated to the property impl using the by keyword. The compiler optimizes the code by directly accessing the impl property within the property accessors of prop. This means that the delegated property’s getValue and setValue operators are skipped, and there is no need for the KProperty reference object.
The compiler generates the following code:
Kotlin
classC<Type> {privatevar impl: Type = ...var prop: Typeget() = implset(value) { impl = value }fungetProp$delegate(): Type = impl // This method is needed only for reflection
As you can see, the accessors for the prop property directly delegate to the impl property. The getValue accessor returns the value of impl, and the setValue accessor assigns the value to impl.
The method getProp$delegate() is also generated, but it is only needed for reflection purposes. It allows reflective access to the delegate object, but it is not used in regular property access.
This optimization avoids the creation of an additional field and reduces the overhead associated with delegated property access. By directly accessing the referenced property, the code becomes more efficient and memory-friendly.
The same optimization principle applies when delegating to another property using this keyword. The compiler generates immediate access to the referenced property without the need for an intermediate field.
Overall, these translation rules improve the performance of delegated properties and eliminate unnecessary memory usage.
What will be the right-hand side of “by"?
In Kotlin, when using delegated properties, the expression to the right of the by keyword can be more than just a new instance creation. It can also be a function call, another property, or any other expression, as long as the value of the expression is an object that provides the getValue and setValue methods with the correct parameter types.
The getValue and setValue methods can be declared directly on the object itself or defined as extension functions. This gives you the flexibility to use existing functions or properties to handle the behavior of your delegated properties.
Here’s an example to demonstrate this concept:
Kotlin
classExample {varvalue: String = "initial"// Delegated property using a function callvar customValue: StringbygetValueFromFunction()// Delegated property using another propertyvar anotherValue: Stringby ::value// Delegated property using an extension functionvar computedValue: IntbycalculateValue()// Extension function providing delegated property behaviorprivatefuncalculateValue(): ReadWriteProperty<Any?, Int> {var storedValue: Int = 0return object : ReadWriteProperty<Any?, Int> {overridefungetValue(thisRef: Any?, property: KProperty<*>): Int {// Perform custom logic to compute the valuereturn storedValue * 2 }overridefunsetValue(thisRef: Any?, property: KProperty<*>, value: Int) {// Perform custom logic to store the value storedValue = value / 2 } } }}fungetValueFromFunction(): ReadWriteProperty<Any?, String> {var storedValue: String = ""return object : ReadWriteProperty<Any?, String> {overridefungetValue(thisRef: Any?, property: KProperty<*>): String {return storedValue }overridefunsetValue(thisRef: Any?, property: KProperty<*>, value: String) { storedValue = value.toUpperCase() } }}funmain() {val example = Example()// Using the delegated properties example.customValue = "amol"println(example.customValue) // Output: AMOL example.anotherValue = "softAai"println(example.anotherValue) // Output: softAai example.computedValue = 5println(example.computedValue) // Output: 10}
In the example above, the customValue property is delegated to the result of a function call getValueFromFunction(), which returns a custom delegate object implementing the ReadWriteProperty interface. The delegate modifies the stored value by converting it to uppercase when setting the value.
The anotherValue property is delegated to the value property using the ::value syntax. Any changes made to anotherValue will be reflected in the value property.
The computedValue property is delegated to the result of the calculateValue() extension function. The extension function provides the delegated property behavior by implementing the ReadWriteProperty interface. In this case, the delegate computes the value by multiplying it by 2 and stores the value by dividing it by 2.
By allowing various expressions on the right-hand side of by and supporting both object-defined and extension-defined getValue and setValue methods, Kotlin enables flexible and customizable behavior for delegated properties.
Providing a delegate
The provideDelegate operator allows you to extend the logic for creating the object to which the property implementation is delegated. If the object used on the right-hand side of the by keyword defines provideDelegate as a member or extension function, that function will be called to create the property delegate instance.
One use case of provideDelegate is to perform additional checks or actions during the initialization of the property delegate. For example, you can check the consistency of the property before binding it.
Here’s an example that demonstrates how to use provideDelegate:
In this example, the ResourceLoader class defines the provideDelegate function. This function is called for each property during the creation of an instance of MyUI. It performs the necessary validation or checks before creating the property delegate.
Without the provideDelegate functionality, you would need to pass the property name explicitly to achieve the same functionality, which could be less convenient.
The provideDelegate method has the same parameters as the getValue function:
thisRef must be the same type as, or a supertype of, the property owner (for extension properties, it should be the type being extended).
property must be of type KProperty<*> or its supertype.
The provideDelegate method is responsible for creating and returning the property delegate instance that will handle the property access.
In the generated code, when provideDelegate is present, it is called to initialize the auxiliary prop$delegate property. Compare the generated code for the property declaration val prop: Type by MyDelegate() with the generated code when provideDelegate is available:
It’s important to note that the provideDelegate method only affects the creation of the auxiliary property (prop$delegate) and does not impact the generated code for the getter or the setter of the delegated property.
You can also use the PropertyDelegateProvider interface from the standard library to create delegate providers without creating new classes. Here’s an example:
In this case, the PropertyDelegateProvider creates a delegate provider using a lambda expression. The lambda receives the thisRef (property owner) and property information and returns a property delegate instance.
Delegation in Android Applications
Kotlin delegation and delegated properties can be useful in Android applications to simplify code, separation of concerns, and provide flexibility in handling certain tasks. Here are a few examples of how delegation can be used in Android applications:
1. Shared Preferences Delegation
Android applications often need to store and retrieve key-value pairs using SharedPreferences. Delegation can be used to simplify the code for accessing SharedPreferences. For example:
In this example, the SettingsManager class uses delegation to handle the isNotificationsEnabled property, which is backed by a shared preference value. The BooleanPreferenceDelegate class implements the delegated property behavior.
2. Dependency Injection with Delegation:
Dependency injection frameworks like Dagger can benefit from delegation to simplify the injection process. By using a delegated property, you can abstract away the complexity of dependency resolution. Here’s a simplified example:
In this example, the MyActivity class uses delegation to lazily inject the MyDependency instance. The inject function provides the delegation logic for dependency injection, making it easy to reuse across different activities.
These are just a few examples of how delegation and delegated properties can be used in Android applications. They demonstrate how delegation can simplify code, improve code organization, and provide flexibility in various scenarios.
Conclusion
Kotlin delegation and delegated properties provide an elegant and efficient way to handle code reuse and separation of concerns. By understanding the concept of delegation and utilizing delegated properties, you can write cleaner, more maintainable code in your Kotlin projects. Whether you’re developing Android applications or working on other Kotlin projects, delegation is a powerful tool to enhance your codebase. Start exploring the possibilities of delegation in Kotlin and unlock the benefits it brings to your software development journey.
Kotlin provides several special types that serve specific purposes, including types such as Any, Unit, and Nothing. Understanding these types and their characteristics is crucial for writing clean and concise Kotlin code. In this article, we will explore the features and use cases of each type, along with relevant examples.
Any: The Root Type
In Kotlin, the Any type serves as the root of the type hierarchy, similar to how the Object class is the root of the class hierarchy in Java. However, there are some differences between the two.
In Java, the Object class is a supertype of all reference types, butit doesnot include primitive types. This means that if you want to use a primitive type where an Object is required, you need to use wrapper types like Integer to represent the primitive value.
On the other hand, in Kotlin, the Any type is a supertype of all types, including primitive types such as Int. This means that you can assign a value of a primitive type directly to a variable of type Any, and automatic boxing will be performed.
For example:
Kotlin
val answer: Any = 42
Note that the Any type is non-nullable, so a variable of type Any cannot hold a null value. If you need a variable that can hold any value, including null, you need to use the Any? type.
Internally, the Any type in Kotlin corresponds to java.lang.Object. When you use Object in Java method parameters or return types, it is seen as Any in Kotlin. However, it is considered a platform type because its nullability is unknown. When a Kotlin function uses Any, it is compiled to Object in the Java bytecode.
The Any type inherits three methods from Object: toString(), equals(), and hashCode(). These methods are available in all Kotlin classes. However, other methods are defined on java.lang.Object, such as wait() and notify(), are not directly available on Any. If you need to call these methods, you can manually cast the value to java.lang.Object.
Key Characteristics of Any Type
Type Safety: Although Any type allows flexibility in holding values of different types, Kotlin’s type system ensures type safety. The compiler performs type checks and prevents incompatible operations on Any-typed variables. This ensures that operations specific to a particular type are only performed when the type is guaranteed at compile-time.
Common Functions and Properties: Any supports common functions and properties provided by Kotlin’s standard library, such as toString(), equals(), and hashCode(). These functions are available on all classes in Kotlin since they implicitly inherit from Any.
Smart Casting: Kotlin’s smart casting mechanism enables the automatic casting of Any-typed variables to more specific types. If the compiler can guarantee the correctness at compile-time, the variable is automatically cast to the more specific type. This allows developers to access type-specific functions and properties without explicit casting.
Practical Use Cases and Examples
Let’s explore some practical use cases of the Any type:
1. Writing Generic Functions: The Any type is often used in the context of writing generic functions that can operate on values of any type. This allows developers to create reusable code that can handle a wide range of input types.
Kotlin
fun <T> printValue(value: T) {println("The value is: $value")}val stringValue: String = "softAai Apps!"val intValue: Int = 42printValue(stringValue) // The value is: softAai Apps!printValue(intValue) // The value is: 42
2. Working with Heterogeneous Collections:Any type is useful when dealing with collections that can contain elements of different types. This allows developers to create collections that can hold values of various types, providing flexibility in scenarios where the types are not known in advance.
Kotlin
val heterogeneousList: List<Any> = listOf("softAai", 42, true)for (element in heterogeneousList) {when (element) {is String ->println("String: $element")is Int ->println("Int: $element")is Boolean ->println("Boolean: $element")else->println("Unknown type") }}
In the above example, the heterogeneousList contains elements of different types (String, Int, Boolean). By using Any type in the list’s declaration, we can store and process elements of different types in a single collection.
3. Working with Unknown Types: In some scenarios, the specific type of a value may not be known at compile time. In such cases, the Any type allows us to handle values dynamically and perform type-specific operations using smart casting.
Kotlin
funprintLength(any: Any) {if (any is String) {println("Length: ${any.length}") // Smart cast: any is automatically cast to String } else {println("Unknown type") }}val stringValue: String = "softAai Apps!"val intValue: Int = 42printLength(stringValue) // Length: 13printLength(intValue) // Unknown type
In the above example, the printLength() function takes an Any-typed parameter. If the parameter is a String, its length is printed using smart casting. This allows us to perform String-specific operations on the variable without the need for explicit casting.
Kotlin’s Any type provides flexibility in holding values of any type while ensuring type safety. It allows developers to write generic functions, work with heterogeneous collections, and handle unknown types dynamically. By leveraging the features of Any type, developers can create more flexible and reusable code.
Unit: Kotlin’s ‘’void’’
In Kotlin, the Unit type serves a similar purpose as the void type in Java. It is used as the return type of a function that does not have any meaningful value to return. Syntactically, you can explicitly declare the return type as Unit or simply omit the type declaration, and the function will be treated as returning Unit.
Kotlin
funf(): Unit { ... }// orfunf() { ... }
In most cases, you won’t notice much of a difference between void and Unit. When a Kotlin function has the Unit return type and does not override a generic function, it is compiled to a regular void function under the hood. If you override it from Java, the Java function simply needs to return void.
However, what distinguishes Kotlin’s Unit from Java’s void is that Unit is a full-fledged type and can be used as a type argument. In Kotlin, Unit is a type with a single value, also called Unit. This is useful when you override a function that returns a generic parameter and want it to return a value of the Unit type.
For example, suppose you have an interface Processor<T> that requires a process() function to return a value of type T. You can implement it with Unit as the type argument:
Kotlin
interfaceProcessor<T> {funprocess(): T}classNoResultProcessor : Processor<Unit> {overridefunprocess() {// do stuff }}
In this case, the process() function in NoResultProcessor returns Unit, which is a valid value for the Unit type. You don’t need to write an explicit return statement because the compiler adds return Unit implicitly.
In Java, the options for achieving a similar behavior are not as elegant. One option is to use separate interfaces, such as Callable and Runnable, to represent functions that do or do not return a value. Another option is to use the special java.lang.Void type as the type parameter. However, in the latter case, you still need to include an explicit return null; statement to return the only possible value that matches the Void type.
The reason Kotlin chose the name Unit instead of Void is because the name Unit is traditionally used in functional languages to mean “only one instance.” This aligns with the nature of Kotlin’s Unit type, which represents a single value. Using the name Void could be confusing, especially since Kotlin already has a distinct type called Nothing that serves a different purpose. Having two types named Void and Nothing would be confusing due to the similarities in meaning and function.
Key Characteristics of Unit Type
Return Type: Unit is commonly used as a return type for functions that do not produce a result or return a meaningful value. When a function’s return type is Unit, the return keyword becomes optional.
Functionality: The Unit type itself does not have any special functions or properties associated with it. It is simply a marker type to indicate the absence of a value. However, functions returning Unit can still have side effects, such as printing to the console or modifying state.
Single Value: Unit has only one value, also named Unit. It represents the absence of any specific value. Since it signifies nothing meaningful, there is no need to create instances of Unit.
Practical Use Cases and Examples
Let’s explore some practical use cases of the Unit type:
Functions with Side Effects: Functions that perform side effects, such as printing or modifying state, often have a return type of Unit. This conveys that the function doesn’t produce a meaningful result but performs actions that affect the program’s state.
Kotlin
fungreet(name: String): Unit {println("Hello, $name!")}greet("softAai") // Hello, softAai!
In the above example, the greet() function takes a name parameter and prints a greeting message. The return type is Unit, indicating that the function doesn’t return a specific value.
2. Discarding Function Results: Sometimes, we may invoke a function solely for its side effects and not require its return value. In such cases, the Unit type can be used to explicitly indicate the disregard for the result.
Kotlin
funlogMessage(message: String): Unit {// Perform logging}val result: Unit = logMessage("An important log message")
In the above example, the logMessage() function logs a message but doesn’t return any meaningful result. The result variable is assigned the value of Unit, indicating that the return value is disregarded.
3. Interoperability with Java: When working with Java libraries or frameworks that have void-returning methods, Kotlin’s Unit type can be used as a compatible return type. This allows seamless integration between Kotlin and Java codebases.
Kotlin
// Kotlin function calling a Java method with void return typefuncallJavaMethod(): Unit { JavaClass.someVoidMethod()}
In the above example, the callJavaMethod() function invokes a Java method, which returns void. By specifying the return type as Unit, Kotlin can seamlessly interact with the Java codebase.
Kotlin’s Unit type represents the absence of a meaningful value and is commonly used as a return type for functions without specific results. It allows developers to indicate side effects, discard function results and enable seamless interoperability with Java code. By utilizing the Unit type effectively, developers can write expressive and concise code that conveys the absence of a meaningful value where necessary.
Nothing: “This function never returns”
In Kotlin, the Nothing type is used to represent functions that never return normally. It is a special return type that indicates that the function does not complete successfully and does not produce any value.
One common use case for the Nothing type is in functions that intentionally fail or throw an exception to indicate an error or an unexpected condition. For example, a testing library may have a function called fail that throws an exception to fail a test with a specified message:
Kotlin
funfail(message: String): Nothing {throwIllegalStateException(message)}fail("Error occurred") // Throws an IllegalStateException with the specified message
In this example, the fail function has a return type of Nothing. Since the function always throws an exception, it never completes normally, and the return type Nothing reflects that.
The Nothing type itself does not have any values. It is used solely as a function return type or as a type argument for a type parameter that is used as a generic function return type. In other contexts, such as declaring a variable, using Nothing doesn’t make sense because there are no values of that type.
One useful feature of functions returning Nothing is that they can be used on the right side of the Elvis operator (?:) for precondition checking. The compiler knows that a function with the Nothing return type never completes normally, so it can infer the non-null type of the variable being assigned.
Kotlin
val address = company.address ?: fail("No address")println(address.city)
In this example, if company.address is null, the fail function is called, which throws an exception. Since the fail function has a return type of Nothing, the compiler infers that the type of address is non-null. This allows you to safely access address.city without null checks.
Key Characteristics of Nothing Type
Absence of Instances: Similar to the Unit type, there are no instances of the Nothing type. It is used purely as a type to indicate situations where a value cannot exist.
Subtype Relationship: Nothing is a subtype of all other Kotlin types, which means it can be used in place of any type when necessary. This allows developers to express that a particular branch of code is unreachable or throws an exception.
Type Inference: The Nothing type also plays a role in Kotlin’s type inference system. If a function has a return type of Nothing, the compiler can infer the type of any variables or expressions within that function to be Nothing as well.
Practical Use Cases and Examples
Let’s explore some practical use cases of the Nothing type:
1. Throwing Exceptions: The Nothing type is often used when a function is intended to throw an exception and never complete normally. By specifying the return type as Nothing, we explicitly indicate that the function will always throw an exception and never return a value.
In the above example, the throwError() function explicitly specifies a return type of Nothing. It throws an exception, ensuring that the function never returns normally.
2. Unreachable Code: The Nothing type is useful in situations where a specific branch of code should be unreachable due to a condition or assertion. By assigning a value of type Nothing to a variable or using it as the return type, the compiler can ensure that the unreachable code is detected.
In the above example, the error function takes a message parameter of type String and has a return type of Nothing.
The error function throws a RuntimeException with the specified error message. This means that when the error function is called, it will always throw an exception and never return normally. By specifying a return type of Nothing, it signals to the compiler that this function does not have a normal return path.
In the processStatus function, the error function is used within the else branch of the when expression. If the else branch is reached, the error function is called with the appropriate error message, indicating an invalid status code.
The main function remains the same, where a sample statusCode of 200 is passed to the processStatus function, and the resulting response is printed to the console.
When you run this code, if the else branch is reached (which should not happen under normal circumstances), the error function will throw a RuntimeException with the error message “Invalid status code: <status>”.
3. Type Inference and Control Flow Analysis: The Nothing type aids Kotlin’s type inference system and control flow analysis. If the compiler determines that a certain branch of code results in a non-terminating function call or throws an exception, it can infer the type of variables within that branch to be Nothing.
Kotlin
funinfiniteLoop(): Nothing {while (true) {// Perform some operations }}val result = if (condition) 42elseinfiniteLoop() // The type of 'result' is Nothing
In the above example, the infiniteLoop() function has a return type of Nothing because it never terminates. The compiler infers that the type of the variable ‘result’ is also Nothing because one branch of the conditional expression results in an infinite loop.
Kotlin’s Nothing type represents a value that never exists and is used in scenarios where a function cannot return normally or a value cannot be assigned. It is commonly used to handle exceptional scenarios and indicate unreachable code. By leveraging the Nothing type effectively, developers can enhance the reliability and expressiveness of their Kotlin programs.
Summary
To summarize, Kotlin’s Any, Unit, and Nothing types provide distinct functionalities and play crucial roles in different scenarios. The Any type allows variables to hold values of any type, providing flexibility and enabling generic programming. The Unit type represents the absence of a meaningful value and is commonly used as a return type for functions without specific results. Lastly, the Nothing type is used to handle exceptional situations, indicate unreachable code, or represent values that never exist.
By understanding the characteristics and use cases of these types, developers can write more expressive, type-safe, and concise code in Kotlin. Leveraging the flexibility of Any, expressing the absence of a value with Unit, and handling exceptional scenarios using Nothing, Kotlin developers can build robust and reliable applications.
Kotlin, a modern programming language for the JVM, comes with a robust and expressive set of collection classes and functions. Kotlin collections provide a seamless way to work with data, enabling efficient data manipulation, transformation, and filtering. Whether you’re a beginner or an experienced Kotlin developer, understanding the various collection types, operations, and best practices is essential. In this article, we will explore Kotlin collections in depth, covering all aspects and providing practical examples to solidify your understanding.
What are Kotlin Collections?
In Kotlin, collections refer to data structures that can hold multiple elements. They provide a way to store, retrieve, and manipulate groups of related objects. Kotlin provides a rich set of collection classes and interfaces in its standard library, making it convenient to work with collections in various scenarios.
Here are some commonly used collection interfaces in Kotlin:
Collection: The root interface for read-only collections. It provides methods for accessing elements, such as iteration, size checking, and element presence checks.
MutableCollection: Extends the Collection interface and adds methods for modifying the collection, such as adding and removing elements.
List: Represents an ordered collection of elements. Elements can be accessed by their indices. Kotlin provides ArrayList and LinkedList as implementations of the List interface.
MutableList: Extends the List interface and adds methods for modifying the list, such as adding, removing, and modifying elements.
Set: Represents a collection of unique elements, with no defined order. Kotlin provides HashSet and LinkedHashSet as implementations of the Set interface.
MutableSet: Extends the Set interface and adds methods for modifying the set.
Map: Represents a collection of key-value pairs. Each key in the map is unique, and you can retrieve the corresponding value using the key. Kotlin provides HashMap and LinkedHashMap as implementations of the Map interface.
MutableMap: Extends the Map interface and adds methods for modifying the map.
These are just a few examples of collection interfaces in Kotlin. The standard library also includes other collection interfaces and their corresponding implementations, such as SortedSet, SortedMap, and Queue, along with various utility functions and extension functions to work with collections more efficiently.
Collections in Kotlin provide a convenient way to handle groups of data and perform common operations like filtering, mapping, sorting, and more. They play a vital role in many Kotlin applications and can greatly simplify data manipulation tasks.
Read-Only and Mutable Collections
Kotlin collection design separates interfaces for accessing and modifying data in collections. This design distinguishes between read-only and mutable interfaces, providing clarity and control over how collections are used and modified.
Thekotlin.collections.Collectioninterfaceis used for accessing data in a collection. It allows you toiterate over the elements, obtain the size, check for the presence of specific elements, and perform other read operations. However, it does not provide methods for adding or removing elements.
Kotlin
funprintCollection(collection: Collection<Int>) {for (element in collection) {println(element) }}val myList = listOf(1, 2, 3)printCollection(myList) // This works fine
To modify the data in a collection, you should use the kotlin.collections.MutableCollection interface. It extends the Collection interface and adds methods for adding and removing elements, clearing the collection, and other modification operations.
Kotlin
funaddToCollection(collection: MutableCollection<Int>, element: Int) { collection.add(element)}val myMutableList = mutableListOf(1, 2, 3)addToCollection(myMutableList, 4) // This modifies the collection
Creating a defensive copy
By using read-only interfaces (Collection) throughout your code, you convey that the collection won’t be modified. If a function accepts a Collection parameter, you can be confident that it only reads data from the collection. On the other hand, when a function expects a MutableCollection, it indicates that the collection will be modified. If you have a collection that is part of your component’s internal state and needs to be passed to a function requiring a MutableCollection, you may need to create a defensive copy of that collection to ensure its integrity.
Kotlin
funmodifyCollection(collection: MutableCollection<Int>) {val defensiveCopy = collection.toList()// Perform modifications on the defensiveCopy// ...}val originalList = mutableListOf(1, 2, 3)modifyCollection(originalList) // The original list remains unchanged
In this example, we have a function modifyCollection that takes a mutable collection as a parameter. However, if the collection is part of your component’s internal state and you want to ensure its integrity, you can create a defensive copy of the collection before passing it to the function.
By calling toList() on the original collection, we create a new read-only list defensiveCopy that contains the same elements. The modifyCollection function can then perform any modifications on the defensive copy without affecting the original collection.
This approach allows you to protect the original collection from unintended modifications, especially when it is part of the component’s internal state or when you want to ensure its immutability in certain scenarios.
Immutable Collections
Kotlin offers a variety of immutable collection types, such as lists, sets, and maps, that cannot be modified once created. These collections guarantee thread safety and immutability, ensuring data integrity in multi-threaded scenarios. Let’s see some examples:
Kotlin
val numbers = listOf(1, 2, 3, 4, 5) // Immutable listval setOfColors = setOf("red", "green", "blue") // Immutable setval mapOfUsers = mapOf(1 to "Alice", 2 to "Bob", 3 to "Charlie") // Immutable map
However, it’s important to note that read-only collections are not necessarily immutable. A read-only collection interface can be one of many references to the same collection. Other references to the collection may have mutable interfaces, allowing modifications.
This means that if you have concurrent code or multiple references to the same collection, modifications from other codes can occur while you’re working with it. This can lead to issues such as ConcurrentModificationException errors. To handle such situations, you need to ensure proper synchronization of access to the data or use data structures that support concurrent access when working in a multi-threaded environment.
Consider the following code snippet:
Kotlin
val mutableList = mutableListOf(1, 2, 3)val readOnlyList: List<Int> = mutableList// Concurrent modification by another referencemutableList.add(4)// Accessing the read-only listreadOnlyList.forEach { println(it) }
In this example, we have a mutable list called mutableList and a read-only list called readOnlyList, which is a reference to the same underlying list. Initially, both lists contain elements [1, 2, 3].
However, the mutableList is mutable, so we can add an element (4) to it. After adding the element, the mutableList becomes [1, 2, 3, 4].
Now, let’s try to iterate over the elements in the readOnlyList using the forEach function. We might expect it to print [1, 2, 3], but what actually happens?
Since the readOnlyList is just a read-only view of the same underlying list, any modifications made to the mutableList will affect the readOnlyList as well. In this case, we added an element to the mutableList, causing the readOnlyList to contain [1, 2, 3, 4]. As a result, when we iterate over the elements in readOnlyList, it will print [1, 2, 3, 4] instead of [1, 2, 3].
This behavior can lead to unexpected results and even errors like ConcurrentModificationException. If you have concurrent code or multiple references to the same collection, modifications made by one reference can affect the others, potentially causing data inconsistencies or errors.
To handle such situations, you need to ensure proper synchronization of access to the data or use data structures that support concurrent access. For example, you can use synchronized blocks or locks to control access to the collection in a multi-threaded environment. Alternatively, you can use concurrent data structures provided by the Kotlin standard library, such as ConcurrentHashMap, which are designed to handle concurrent modifications safely.
It’s crucial to be aware of these considerations when working with read-only collections that are shared among multiple references or used in concurrent scenarios.
Kotlin collections and Java
In Kotlin, every collection type is an instance of the corresponding Java collection interface. This means that Kotlin collections seamlessly integrate with Java collections without requiring any conversion, wrappers, or data copying.
However, in Kotlin, each Java collection interface has two representations: a read-only version and a mutable version. The read-only interfaces mirror the structure of the Java collection interfaces but lack mutating methods, while the mutable interfaces extend their corresponding read-only interfaces and provide mutating methods.
For example, the Java class java.util.ArrayList is treated as if it inherited from the MutableList interface. This means that you can use an ArrayList instance in Kotlin as if it were a MutableList, and you can call the methods defined in the MutableList interface on an ArrayList object. Similarly, the Java class java.util.HashSetis treated as if it inherited from the MutableSet interface, allowing you to use a HashSet instance as a MutableSet.
Other Java collection implementations, such as LinkedList and SortedSet, have similar supertypes in Kotlin. This means that LinkedList is treated as if it inherited from a related interface, and SortedSet is also treated as if it inherited from a corresponding Kotlin interface. These interfaces provide a common set of methods that can be used across different implementations.
The purpose of treating Java classes as if they inherited from their corresponding Kotlin interfaces is to provide compatibility and allow seamless interoperability between Kotlin and Java collections. Kotlin provides both mutable and read-only interfaces, allowing for clear separation and appropriate usage of collections depending on whether you need to mutate them or not.
What about Map?
Similarly, the Map class (which doesn’t extend Collection or Iterable) in Java has two versions in Kotlin: Map (read-only) and MutableMap (mutable). These versions provide different sets of functions for working with maps.
When calling a Java method that expects a collection as a parameter, you can pass a Kotlin collection directly without any extra steps. Kotlin handles the interoperability between Kotlin collections and Java collections seamlessly.
However, there is an important caveat to consider. Since Java does not distinguish between read-only and mutable collections, Java code can modify a collection even if it’s declared as read-only on the Kotlin side. The Kotlin compiler cannot fully analyze the modifications made by Java code, so Kotlin cannot reject a call passing a read-only collection to Java code that modifies it.
As a result, when writing a Kotlin function that passes a collection to Java code, it’s your responsibility to use the correct type for the parameter based on whether the Java code will modify the collection or not.
Kotlin collection interfaces
Now we will delve deep into the collection interfaces and explore their implementations, enabling you to leverage the full power of Kotlin collections in your projects.
Below is a diagram of the Kotlin collection interfaces:
Collection
TheCollection<T> interface serves as the foundation of the collection hierarchy in Kotlin. It represents the common behavior of read-only collections and provides essential operations such as retrieving the size of the collection and checking if an item is present.
In addition, the Collection inherits from the Iterable<T> interface, which defines operations for iterating over elements in a collection. This allows you to use Collection as a parameter in functions that work with different collection types, providing a versatile way to handle collections in your code.
However, for more specific scenarios, it’s recommended to use the inheritors of Collection: List and Set. These inheritors offer additional functionality tailored to their respective purposes. Let’s see some examples:
Kotlin
// Using Collection as a parameterfunprintCollectionSize(collection: Collection<Int>) {println("Collection size: ${collection.size}")}val list: List<Int> = listOf(1, 2, 3, 4, 5)valset: Set<Int> = setOf(1, 2, 3, 4, 5)printCollectionSize(list) // Output: Collection size: 5printCollectionSize(set) // Output: Collection size: 5// Using List and Set directlyval listItems: List<String> = listOf("apple", "banana", "orange")val setItems: Set<String> = setOf("apple", "banana", "orange")println(listItems.size) // Output: 3println(setItems.contains("banana")) // Output: true
In the example above, we demonstrate the usage of Collection as a parameter in the printCollectionSize function, which can accept both List and Set. Additionally, we directly use the List and Set interfaces to access their specific methods, such as retrieving the size or checking for item membership.
List
The List<T> interface in Kotlin stores elements in a specific order and provides indexed access to them. The indices start from zero, representing the first element, and go up to lastIndex, which is equal to (list.size — 1).
A List allows duplicate elements (including nulls), meaning it can contain any number of equal objects or occurrences of a single object. When comparing lists for equality, they are considered equal if they have the same sizes and structurally equal elements at the same positions.
The MutableList<T> interface extends List and provides additional write operations specifically designed for lists. These operations allow you to add or remove an element at a specific position within the list.
While lists share similarities with arrays, there is one crucial difference: an array’s size is fixed upon initialization and cannot be changed, whereas a list does not have a predefined size. Instead, a list’s size can be modified through write operations like adding, updating, or removing elements.
In Kotlin, the default implementation of MutableList is ArrayList, which can be visualized as a resizable array that dynamically adjusts its size based on the number of elements it contains. This provides flexibility and allows you to manipulate the list as needed.
Let’s illustrate the concepts with a simple example:
Kotlin
// Creating a list and accessing elementsval fruits: List<String> = listOf("apple", "banana", "orange")println(fruits[1]) // Output: banana// Creating a mutable list and modifying elementsval mutableFruits: MutableList<String> = mutableListOf("apple", "banana", "orange")mutableFruits.add("grape")mutableFruits[1] = "kiwi"mutableFruits.removeAt(0)println(mutableFruits) // Output: [kiwi, orange, grape]
In the example above, we first create an immutable list of fruits. We can access individual elements using the indexing syntax (fruits[1]) and retrieve the element at the specified position.
Next, we create a mutable list of fruits using MutableList. This allows us to perform write operations on the list. We add a new element with add, update an element at index 1 using indexing assignment (mutableFruits[1] = "kiwi"), and remove an element at a specific position using removeAt. Finally, we print the modified list.
Set
The Set<T> interface in Kotlin stores unique elements, and their order is generally undefined. In a Set, duplicate elements are not allowed, except for a single occurrence of null. Comparing two sets for equality depends on their sizes and whether each element in one set has an equal element in the other set.
The MutableSet interface extends MutableCollection and provides write operations specific to sets. This allows you to add or remove elements from the set.
Let’s illustrate the concepts with an example:
Kotlin
// Creating a set and adding elementsval numbers: Set<Int> = setOf(1, 2, 3, 4, 5)println(numbers) // Output: [1, 2, 3, 4, 5]// Creating a mutable set and modifying elementsval mutableNumbers: MutableSet<Int> = mutableSetOf(1, 2, 3, 4, 5)mutableNumbers.add(6)mutableNumbers.remove(3)println(mutableNumbers) // Output: [1, 2, 4, 5, 6]
In the example above, we first create an immutable set of numbers. Since sets store unique elements, any duplicate values are automatically eliminated.
Next, we create a mutable set of numbers using MutableSet. This allows us to perform write operations on the set. We add a new element with add and remove an element with remove. Finally, we print the modified set.
Set<T> interface provides a way to store unique elements without a specific order. The default implementation for MutableSet<T> is LinkedHashSet, which preserves the order of element insertion. This means that the elements in a LinkedHashSet are ordered based on the order in which they were added, ensuring predictable results when using functions like first() or last().
In the above example, we create a MutableSet using linkedSetOf, which creates a LinkedHashSet. The order of the elements in the set is preserved based on their insertion order. When we call first(), it returns the first element, which is “apple”. Similarly, last() returns the last element, which is “kiwi”. Since LinkedHashSet maintains the insertion order, these functions give predictable results.
On the other hand, the HashSet implementation does not guarantee any specific order of elements. Therefore, calling functions like first() or last() on a HashSet can yield unpredictable results. However, HashSet requires less memory compared to LinkedHashSet, making it more memory-efficient for storing the same number of elements.
In the above example, we create a MutableSet using hashSetOf, which creates a HashSet. The order of the elements in the set is not guaranteed. Therefore, calling first() or last() on a HashSet can give unpredictable results. The output can vary each time you run the code.
Map
The Map<K, V> interface in Kotlin is a collection type that stores key-value pairs, also known as entries. Unlike other collection interfaces, Map does not inherit from the Collection interface. However, it provides specific functions for accessing values by their corresponding keys, searching for keys and values, and more.
In a Map, keys are unique, meaning that each key can be associated with only one value. However, different keys can be paired with equal values. Comparing two maps for equality depends on the key-value pairs they contain, regardless of the order in which the pairs are stored.
Kotlin
funmain() {val numbersMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key4" to 1) val anotherMap = mapOf("key2" to 2, "key1" to 1, "key4" to 1, "key3" to 3)println("The maps are equal: ${numbersMap == anotherMap}")}
The MutableMap interface extends Map and provides additional write operations specific to maps. These operations allow you to add new key-value pairs or update the value associated with a given key.
The default implementation of MutableMap is LinkedHashMap, which preserves the order of element insertion when iterating over the map. This means that when you iterate over a LinkedHashMap, the elements will be returned in the same order in which they were added. On the other hand, HashMap does not guarantee any specific order of elements and is more focused on performance and memory efficiency.
Let’s see an example to understand the concepts:
Kotlin
// Creating a map and accessing values by keyval ages: Map<String, Int> = mapOf("John" to 25, "Jane" to 30, "Alice" to 35)println(ages["John"]) // Output: 25// Creating a mutable map and modifying valuesval mutableAges: MutableMap<String, Int> = mutableMapOf("John" to 25, "Jane" to 30, "Alice" to 35)mutableAges["John"] = 26mutableAges["Bob"] = 40mutableAges.remove("Jane")println(mutableAges) // Output: {John=26, Alice=35, Bob=40}
In the above example, we first create an immutable map of ages, where each person’s name is paired with their age. We can access the values by providing the corresponding key (ages["John"]).
Next, we create a mutable map of ages using MutableMap. This allows us to perform write operations on the map. We update the value associated with the key “John” using indexing assignment (mutableAges["John"] = 26), add a new key-value pair with mutableAges["Bob"] = 40, and remove a key-value pair using remove. Finally, we print the modified map.
Commonly Used Collection Implementations
Kotlin provides several commonly used collection implementations that offer different characteristics and performance trade-offs. Let’s explore some of these implementations:
ArrayList
ArrayList is an implementation of the MutableList interface and provides dynamic arrays that can grow or shrink in size. It offers fast element retrieval by index and efficient random access operations.
Kotlin
val arrayList: ArrayList<String> = ArrayList()arrayList.add("Apple")arrayList.add("Banana")arrayList.add("Orange")println(arrayList) // Output: [Apple, Banana, Orange]
LinkedList
LinkedList is an implementation of the MutableList interface that represents a doubly-linked list. It allows efficient element insertion and removal at both ends of the list but has slower random access compared to ArrayList.
Kotlin
val linkedList: LinkedList<String> = LinkedList()linkedList.add("Apple")linkedList.add("Banana")linkedList.add("Orange")println(linkedList) // Output: [Apple, Banana, Orange]
HashSet
HashSet is an implementation of the MutableSet interface that stores elements in an unordered manner. It ensures the uniqueness of elements by using hash codes and provides fast membership checking.
Kotlin
val hashSet: HashSet<String> = HashSet()hashSet.add("Apple")hashSet.add("Banana")hashSet.add("Orange")println(hashSet) // Output: [Apple, Banana, Orange]
TreeSet
TreeSet is an implementation of the MutableSet interface that stores elements in sorted order based on their natural order or a custom comparator. It provides efficient operations for retrieving elements in a sorted manner.
Kotlin
val treeSet: TreeSet<String> = TreeSet()treeSet.add("Apple")treeSet.add("Banana")treeSet.add("Orange")println(treeSet) // Output: [Apple, Banana, Orange]
HashMap
HashMap is an implementation of the MutableMap interface that stores key-value pairs. It provides fast lookup and insertion operations based on the hash codes of keys.
TreeMap is an implementation of the MutableMap interface that stores key-value pairs in a sorted order based on the natural order of keys or a custom comparator. It provides efficient operations for retrieving entries in a sorted manner.
These are some of the commonly used collection implementations in Kotlin. Each implementation has its own characteristics and usage scenarios, so choose the one that best fits your requirements in terms of performance, order, uniqueness, or sorting.
Iterable
When working with collections in Kotlin, traversing through the elements is a common requirement. The Kotlin standard library provides mechanisms such as iterators and for loops to facilitate this traversal.
Iterators
Iterators are objects that allow sequential access to the elements of a collection without exposing the underlying structure of the collection. You can obtain an iterator for inheritors of the Iterable<T> interface, including Set and List, by calling the iterator() function on the collection.
Here’s an example of using an iterator to traverse a collection:
Kotlin
val numbers = listOf(1, 2, 3, 4, 5)val iterator = numbers.iterator()while (iterator.hasNext()) {val element = iterator.next()println(element)}
In the above example, we create a List of numbers and obtain an iterator by calling iterator() on the list. We then use a while loop to iterate through the elements. The hasNext() function checks if there is another element, and next() retrieves the current element and moves the iterator to the next position. We can perform operations on each element, such as printing its value.
Alternatively, Kotlin provides a more concise way to iterate through a collection using the for loop:
Kotlin
val numbers = listOf(1, 2, 3, 4, 5)for (element in numbers) {println(element)}
In this case, the for loop implicitly obtains the iterator and iterates over the elements of the collection.
Additionally, the standard library provides the forEach() function, which simplifies iterating over a collection and executing code for each element:
Kotlin
val numbers = listOf(1, 2, 3, 4, 5)numbers.forEach { element ->println(element)}
The forEach() function takes a lambda expression as an argument, and the code within the lambda is executed for each element in the collection.
ListIterator
For lists, there is a special iterator implementation called ListIterator. It supports iterating through lists in both forward and backward directions. The ListIterator provides functions such as hasPrevious(), previous(), nextIndex(), and previousIndex() to facilitate backward iteration and retrieve information about element indices.
Kotlin
val colors = listOf("red", "green", "blue")val listIterator = colors.listIterator()while (listIterator.hasNext()) {val element = listIterator.next()println(element)}while (listIterator.hasPrevious()) {val element = listIterator.previous()println(element)}
In the above code, we create a list of colors and obtain a ListIterator by calling listIterator() on the list. We then use a while loop to iterate through the list in the forward direction using next().
After reaching the end of the list, we use another while loop to iterate in the backward direction using previous(). This allows us to traverse the list from the last element back to the first element.
MutableIterator
For mutable collections, there is MutableIterator, which extends Iterator and provides the remove() function. This allows you to remove elements from a collection while iterating over it. In addition, MutableListIterator allows the insertion and replacement of elements while iterating through a list.
In the above code, we create a mutable list of numbers and obtain a MutableIterator by calling iterator() on the list. We iterate through the list using a while loop and remove the even numbers using remove() when encountered.
After iterating, we print the modified list, which now contains only the odd numbers.
By using ListIterator, you can traverse lists in both forward and backward directions, while MutableIterator allows you to remove elements from mutable collections during iteration. These iterators provide flexibility and control when working with lists and mutable collections in Kotlin.
Collection Creation Function In Kotlin
To create a collection in Kotlin, you can use the various collection classes provided by the Kotlin standard library, such as List, MutableList, Set, MutableSet, Map, and MutableMap. These classes have constructors and factory functions to create collections with initial elements.
Here’s an example of how we can create different types of collections in Kotlin:
Kotlin
val list = listOf("apple", "banana", "orange") // Creating a Listval mutableList = mutableListOf("apple", "banana", "orange") // Creating a MutableListvalset = setOf("apple", "banana", "orange") // Creating a Setval mutableSet = mutableSetOf("apple", "banana", "orange") // Creating a MutableSetval map = mapOf(1 to "apple", 2 to "banana", 3 to "orange") // Creating a Mapval mutableMap = mutableMapOf(1 to "apple", 2 to "banana", 3 to "orange") // Creating a MutableMap
You can replace the initial elements with your own data or leave the collections empty if you want to populate them later.
Note: The examples above use immutable (val) collections, which means you cannot modify their contents once created. If you need to modify the collection, you can use their mutable counterparts (MutableList, MutableSet, MutableMap) and add or remove elements as needed.
Empty collections
In Kotlin, there are convenient functions for creating empty collections: emptyList(), emptySet(), and emptyMap(). These functions allow you to create collections without any elements.
When using these functions, it’s important to specify the type of elements that the collection will hold. This helps the compiler infer the appropriate type for the collection and enables type safety during compile-time checks.
Here’s an example of using the emptyList() function:
Kotlin
val emptyStringList: List<String> = emptyList()
In the above example, we create an empty List of Strings using emptyList(). By specifying the type parameter <String>, we ensure that the list can only hold String elements. This helps avoid type errors and provides type safety when working with the list.
Similarly, we can create an empty Set or an empty Map:
Kotlin
val emptyIntSet: Set<Int> = emptySet()<br>val emptyStringToIntMap: Map<String, Int> = emptyMap()
In these examples, we create an empty Set of Integers using emptySet() and an empty Map from Strings to Integers using emptyMap(). By explicitly specifying the types <Int> and <String, Int>, respectively, we ensure that the sets and maps are appropriately typed and can only hold elements of the specified types.
Using these functions to create empty collections is especially useful in scenarios where you need to initialize a collection variable but don’t have any initial elements to add. It allows you to start with an empty collection of the desired type and later add or populate it as needed.
Kotlin Collection Operations
Kotlin collections provide a rich set of operations to manipulate, transform, and filter data efficiently. Let’s explore some commonly used operations:
Mapping: Transform each element in a collection using a mapping function.
Kotlin
val numbers = listOf(1, 2, 3, 4, 5)val squaredNumbers = numbers.map { it * it }
Filtering: Select elements from a collection based on a given condition.
Kotlin
val numbers = listOf(1, 2, 3, 4, 5)val evenNumbers = numbers.filter { it % 2 == 0 }
Reducing:Perform a reduction operation on a collection to obtain a single result.
Kotlin
val numbers = listOf(1, 2, 3, 4, 5)val sum = numbers.reduce { acc, value-> acc + value }
Grouping:Group elements of a collection based on a given key.
Kotlin
val words = listOf("apple", "banana", "avocado", "blueberry")val groupedWords = words.groupBy { it.first() }
Collection Operations with Predicates
Kotlin collections provide powerful operations that utilize predicates, enabling advanced data manipulation. Let’s explore some of these operations:
Checking if all elements satisfy a condition
Kotlin
val numbers = listOf(1, 2, 3, 4, 5)val allPositive = numbers.all { it > 0 }
Checking if any element satisfies a condition
Kotlin
val numbers = listOf(1, 2, 3, 4, 5)val hasNegative = numbers.any { it < 0 }
Finding the first element that satisfies a condition
Kotlin
val numbers = listOf(1, 2, 3, 4, 5)val firstEven = numbers.firstOrNull { it % 2 == 0 }
Counting the number of elements that satisfy a condition
Kotlin
val numbers = listOf(1, 2, 3, 4, 5)val countEven = numbers.count { it % 2 == 0 }
Extension Functions on Collections
One of the highlights of Kotlin collections is the ability to use extension functions, which allow you to add new functionality to existing collection classes. These functions enhance the readability and conciseness of your code. Let’s take a look at some examples:
fun <T> List<List<T>>.flatten(): List<T> {returnthis.flatMap { it }}val listOfLists = listOf(listOf(1, 2), listOf(3, 4), listOf(5, 6))val flattenedList = listOfLists.flatten()
In the above code, we define an extension function called flatten for the List<List<T>> type. The function uses flatMap to concatenate all the inner lists into a single list, resulting in a flattened structure.
Commonly Used Extension Functions
sortBy():Sorts the collection in ascending order based on a specified key selector.
groupBy():Groups the elements of a collection by a specified key selector and returns a map where the keys are the selected values and the values are lists of corresponding elements.
By combining Kotlin collections with extension functions, you can perform a wide range of operations efficiently and with expressive code. These features make Kotlin a powerful language for working with data and collections.
Null Safety in Collections
Null safety is a crucial aspect of Kotlin that helps prevent null pointer exceptions and ensures more reliable code. Kotlin’s type system includes built-in null safety features for collections, which offer better control and safety when dealing with nullable elements.
In Kotlin collections, you can specify whether the collection itself or its elements can be nullable. Let’s explore how null safety works in collections:
Nullable Collections
By default, Kotlin collections are non-nullable, meaning they cannot hold null values. For example, List<Int> represents a list that can only contain non-null integers. If you try to add a null value to a non-nullable collection, it will result in a compilation error.
Kotlin
val list: List<Int> = listOf(1, 2, null) // Error: Null cannot be a value of a non-null type Int
To allow null values in a collection, you can specify a nullable type. For example, List<Int?> represents a list that can contain both non-null and nullable integers.
Kotlin
val list: List<Int?> = listOf(1, 2, null) // Okay
Safe Access to Elements
When working with collections that may contain null values, it’s essential to use safe access operators to prevent null pointer exceptions. Kotlin provides the safe access operator (?.) and the safe call operator (?.let) for this purpose.
Kotlin
val list: List<String?> = listOf("Alice", null, "Bob")val firstElement: String? = list.firstOrNull()val length: Int? = list.firstOrNull()?.length// Safe access using the safe call operatorval uppercaseNames: List<String>? = list.map { it?.toUpperCase() }
In the above code, firstOrNull() is used to safely retrieve the first element of the list, which may be null. The safe access operator (?.) is used to access the length property of the first element, ensuring that a null value won’t result in a null pointer exception.
The safe call operator is also useful when performing transformations or operations on elements within the collection. In the example, the map function is called on the list, and the safe call operator is used to convert each element to uppercase. The result is a nullable list (List<String>?), which accounts for the possibility of null elements.
Filtering Nullable Elements
When working with collections that may contain null values, you may need to filter out the null elements. Kotlin provides the filterNotNull() function for this purpose.
In the above code, filterNotNull() is used to create a new list that excludes the null elements. The resulting filteredList is of type List<String>, guaranteeing non-null values.
Null safety in collections is an essential aspect of Kotlin that helps eliminate null pointer exceptions and provides more reliable code. By leveraging nullable types and safe access operators, you can handle nullable elements in collections and ensure safer and more robust code.
Collection Conversion
Converting between different collection types and arrays is a common requirement when working with data in Kotlin. Kotlin provides convenient functions for converting collections to different types and converting collections to arrays. Let’s explore these conversion mechanisms:
Converting Between Collection Types
Kotlin provides extension functions to convert between different collection types. Here are some commonly used conversion functions:
These conversion functions allow you to transform a collection into a different type based on your requirements. It’s important to note that the resulting collection is a new instance with the transformed elements.
Converting to Arrays
Kotlin also provides functions to convert collections to arrays. Here are the commonly used conversion functions:
toTypedArray(): Converts a collection to an array of the specified type.
These conversion functions allow you to obtain arrays from collections, which can be useful when interacting with APIs that require array inputs or when specific array types are needed.
It’s important to note that arrays are fixed in size and cannot be dynamically resized like mutable collections. Therefore, the resulting arrays will have the same number of elements as the original collections.
By using these conversion functions, you can easily convert collections to different types or arrays based on your specific requirements in Kotlin.
Kotlin Standard Library Functions for Collections
The Kotlin Standard Library provides several useful functions that can be applied to collections to simplify and enhance their usage. Let’s explore two categories of these functions:
let, apply, also, and run
These functions allow you to perform operations on collections and access their elements in a concise and expressive manner.
let:Executes a block of code on a collection and returns the result.
Kotlin
val list: List<Int> = listOf(1, 2, 3)val result: List<String> = list.let { collection ->// Perform operations on the collection collection.map { it.toString() }}println(result) // Output: [1, 2, 3]
apply:Applies a block of code to a collection and returns the collection itself.
Kotlin
val list: MutableList<Int> = mutableListOf(1, 2, 3)list.apply {// Perform operations on the collectionadd(4)removeAt(0)}println(list) // Output: [2, 3, 4]
also:Performs additional operations on a collection and returns the collection itself.
Kotlin
val list: List<Int> = listOf(1, 2, 3)val result: List<Int> = list.also { collection ->// Perform additional operations on the collectionprintln("Size of the collection: ${collection.size}")}println(result) // Output: [1, 2, 3]
run:Executes a block of code on a collection and returns the result.
Kotlin
val list: List<Int> = listOf(1, 2, 3)val result: List<String> = run {// Perform operations on the collection list.map { it.toString() }}println(result) // Output: [1, 2, 3]
These functions provide different ways to interact with collections, allowing you to perform operations, transform elements, or execute code on the collections themselves.
withIndex and zip
These functions enable you to work with the indices and combine multiple collections
withIndex:Provides access to the index and element of each item in a collection.
Kotlin
val list: List<String> = listOf("Apple", "Banana", "Orange")for ((index, element) in list.withIndex()) {println("[$index] $element")}// Output:// [0] Apple// [1] Banana// [2] Orange
zip:Combines elements from two collections into pairs.
These functions provide convenient ways to work with indices and combine collections, making it easier to iterate through collections or create pairs of elements from different collections.
By utilizing these standard library functions, you can simplify your code, make it more expressive, and enhance the functionality of collections in Kotlin.
Collection Performance Considerations
When working with collections, it’s important to consider their performance characteristics to ensure efficient usage. Here are some considerations and best practices to keep in mind:
Choosing the Right Collection Type
Selecting the appropriate collection type for your specific use case can significantly impact performance. Consider the following factors:
List vs. Set:Use a List when the order and duplicate elements are important. Choose a Set when uniqueness and fast membership checks are required.
ArrayList vs. LinkedList: Use an ArrayList when you need efficient random access and iteration. Opt for a LinkedList when frequent insertion and removal at both ends of the list are required.
HashSet vs. TreeSet: Choose a HashSet when order doesn’t matter, and uniqueness and fast membership checks are important. Use a TreeSet when elements need to be stored in sorted order.
HashMap vs. TreeMap: Use a HashMap for fast key-value lookups and insertions without requiring sorted order. Choose a TreeMap when entries need to be stored in sorted order based on keys.
Consider the specific requirements and performance trade-offs of each collection type to make an informed decision.
Performance Tips and Best Practices
To optimize collection performance, consider the following tips:
Minimize unnecessary operations:Avoid unnecessary operations like copying collections or converting them back and forth. Optimize your code to perform only the required operations.
Use proper initial capacity: When creating collections, provide an appropriate initial capacity to avoid frequent resizing, especially for ArrayLists and HashMaps. Estimate the number of elements to be stored to improve performance.
Prefer specific collection interfaces:Use more specific collection interfaces like List, Set, or Map instead of the general Collection interface to leverage their specialized operations and improve code readability.
Be cautious with nested iterations:Avoid nested iterations over large collections as they can lead to performance issues. Consider alternative approaches like using index-based iterations or transforming data into more efficient data structures if possible.
Utilize lazy operations:Take advantage of lazy operations like filter, map, and takeWhile to avoid unnecessary computations on large collections until they are actually needed.
Use appropriate data structures: Choose the right data structure for your specific requirements. For example, if you frequently need to check for containment, consider using a HashSet instead of a List.
Measure and profile performance:If performance is critical, measure and profile your code to identify bottlenecks and areas for optimization. Utilize tools like profilers to identify performance hotspots.
By considering these performance considerations and following best practices, you can ensure efficient usage of collections in your Kotlin code. Optimize your code based on specific requirements and evaluate performance trade-offs to achieve better performance.
Conclusion
Kotlin collections provide a powerful and intuitive way to handle data manipulation in your Kotlin applications. By understanding the different collection types, operations, extension functions, and performance considerations, you can write efficient and expressive code. In this article, we covered the various aspects of Kotlin collections, providing detailed explanations and examples for each topic. With this knowledge, you’re equipped to harness the full potential of Kotlin collections and optimize your data manipulation workflows. Start exploring Kotlin collections and elevate your Kotlin programming skills to new heights.
Null pointer exceptions (NullPointerExceptions) are a common source of errors in programming languages, causing applications to crash unexpectedly. Kotlin, a modern programming language developed by JetBrains, addresses this issue by incorporating null safety as a core feature of its type system. By distinguishing between nullable and non-nullable types, Kotlin enables developers to catch potential null pointer exceptions at compile time, resulting in more robust and reliable code. In this article, we will take an eagle-eye view of Kotlin’s nullability system, along with the tools and techniques provided to handle them effectively. We will also delve into the nuances of working with nullable types when mixing Kotlin and Java code.
Understanding Nullabiliy
Nullability in Kotlin is a crucial feature that prevents NullPointerException errors. These errors often provide vague error messages like “An error has occurred: java.lang.NullPointerException” or “Unfortunately, the application X has stopped,” causing inconvenience for both users and developers.
Modern languages, including Kotlin, aim to transform these runtime errors into compile-time errors. By incorporating nullability into the type system, Kotlin’s compiler candetect potential errors during compilation, significantly reducing the occurrence of runtime exceptions.
In the following sections, we will explore nullable types in Kotlin. We’ll examine how Kotlin identifies values that can be null and delve into the tools provided by Kotlin to handle nullable values. Additionally, we will discuss the specifics of working with nullable types when mixing Kotlin and Java code.
Nullable types
A type in programming determines the possible values and operations that can be performed on those values. For example, in Java, the double type represents a 64-bit floating-point number, allowing standard mathematical operations. On the other hand, the String type in Java can hold instances of the String class or null, with different operations available for each.
However, Java’s type system falls short when it comes to nullability. Variables declared with a specific type, such as String, can still hold null values, leading to potential NullPointerException errors. While annotations like @Nullable and @NotNull can help detect and mitigate these errors, they are not consistently applied, and their use doesn’t entirely solve the problem.
To address this issue, Kotlin provides nullable types and introduces thesafe-call operator (We will discuss those in much greater detail later on), ?. The safe-call operator allows combining null checks and method calls into a single operation. For example, the expression s?.toUpperCase() is equivalent to if (s != null) s.toUpperCase() else null. The result type of such an invocation is nullable, denoted by appending a question mark to the type (e.g., String?).
So, Nullable types in Kotlin are used to indicate variables or properties that are allowed to have null values. When a variable is nullable, calling a method on it can be unsafe, as it may result in a NullPointerException.
In Kotlin, you can indicate that variables of a certain type can store null references by adding a question mark after the type declaration. For example, String?, Int?, and MyCustomType? are all nullable types.
Here above Figure show, a variable of a nullable type can store a null reference.
By default, regular types in Kotlin are non-null, which means they cannot store null references unless explicitly marked as nullable. However, when dealing with nullable types, the set of operations that can be performed on them becomes restricted. Let’s explore a few examples to understand the problems that can arise and their solutions.
Assigning null to a non-null variable:
Kotlin
val x: String? = nullvar y: String = x // Error: Type mismatch - inferred type is String? but String was expected
In the above example, we try to assign a nullable type (x) to a variable of a non-null type (y), resulting in a type mismatch error during compilation.
Passing nullable type as a non-null parameter:
Kotlin
funstrLen(str: String) {// Function logic}val x: String? = nullstrLen(x) // Error: Type mismatch - inferred type is String? but String was expected
In the above example, we attempt to pass a nullable type (x) as an argument to a function (strLen()) that expects a non-null parameter. This results in a type mismatch error during compilation.
To handle nullable types, you can perform null checks to compare them with null. The Kotlin compiler remembers these null checks and treats the value as non-null within the scope of the check.
Performing a null check:
Kotlin
val x: String? = nullif (x != null) {// Code block within the null check// Compiler treats 'x' as non-null here}
In the above example, by adding a null check, the compiler recognizes that the value of x is handled for nullability within the code block. The compiler will treat x as non-null within that scope, allowing further operations without type mismatch errors.
By distinguishing nullable and non-null types, Kotlin’s type system offers clearer insights into allowed operations and potential exceptions at runtime. Notably, nullable types in Kotlin do not introduce runtime overhead as all checks are performed during compilation.
By utilizing null checks, you can effectively work with nullable types and ensure your code compiles correctly.
Null Safety Tools in Kotlin
Now let’s see how to work with nullable types in Kotlin and why dealing with them is by no means annoying. We’ll start with the special operator for safely accessing a nullable value.
Safe call operator: “?.”
The safe-call operator, ?., is a powerful tool in Kotlin that combines null checks and method calls into a single operation. It allows you to call methods on non-null values while gracefully handling null values.
For example, consider the expression s?.toUpperCase(). This is equivalent to the more verbose code if (s != null) s.toUpperCase() else null. The safe-call operator ensures that if the value s is not null, the toUpperCase() method will be executed normally. However, if s is null, the method call is skipped, and the result will be null.
Here’s an example usage of the safe-call operator:
In the printAllCaps() function, the s?.toUpperCase() expression safely calls the toUpperCase() method on s if it\’s not null. The result, allCaps, will be of type String?, indicating that it can hold either a non-null uppercase string or a null value.
The safe-call operator is not limited to method calls; it can also be used for accessing properties. Additionally, you can chain multiple safe-call operators together for more complex scenarios. Let’s see the below example.
Kotlin
classAddress(val streetAddress: String, val zipCode: Int, val city: String, val country: String)classCompany(val name: String, val address: Address?)classPerson(val name: String, val company: Company?)funPerson.countryName(): String {val country = this.company?.address?.countryreturnif (country != null) country else"Unknown"}
We have three classes: Address, Company, and Person. The Address class represents a physical address with properties such as streetAddress, zipCode, city, and country. The Company class represents a company with properties name and address, where address is an instance of the Address class. The Person class represents a person with properties name and company, where company is an instance of the Company class.
The Person class also has an extension function called countryName(), which returns the country name associated with the person’s company. The function uses the safe-call operator ?. to access the country property of the address property of the company. If any of these properties are null, the result will be null.
Kotlin
val person = Person("amol", null)println(person.countryName())
Here, we created an instance of the Person class named person with the name of “amol” and a null company. When we call the countryName() function on person, it tries to access the company property, which is null. Consequently, the address and country properties will also be null.
Therefore, the output of println(person.countryName()) will be “Unknown” because the safe-call operator ensures that if any part of the chain (company, address, or country) is null, the overall result will be null. In this case, since the company is null, the country is considered unknown.
The countryName() function demonstrates how to safely navigate through a chain of nullable properties using the safe-call operator, providing a default value (“Unknown” in this case) when any of the properties in the chain are null. This approach helps avoid null pointer exceptions and handle null values gracefully.
Elvis operator: “?:”
In Kotlin, you can eliminate unnecessary repetition when dealing with null checks using the Elvis operator ?:. This operator provides a default value instead of null.
Here’s how it works:
Kotlin
funfoo(s: String?) {val t: String = s ?: ""}
In the above example, If “s” is null, the result is an empty string.
The Elvis operator takes two values, and its result is the first value if it isn’t null, or the second value if the first one is null.
When used in conjunction with the safe-call operator (?.), the Elvis operator (?:) in Kotlin serves the purpose of providing an alternative value instead of null when the object on which a method is called is null.
Kotlin
val name: String? = nullval length = name?.length ?: 0
In the above code, the variable name is nullable, and we want to obtain its length. However, if name is null, calling length directly would result in a NullPointerException. To handle this situation, we use the safe-call operator (?.) to safely access the length property of name.
Additionally, we employ the Elvis operator (?:) to provide a fallback value of 0 in case name is null. So, if name is not null, its length will be assigned to the variable length. Otherwise, length will be assigned the value 0.
This way, we avoid potential NullPointerException errors and ensure that length always has a valid value, even if name is null.
Let’s see one more example, the countryName() function from the previous code listing can be simplified to a single line using the Elvis operator:
In this case, if any part of the chain (company, address, or country) is null, the default value “Unknown” will be used.
The Elvis operator is particularly handy in Kotlin because operations like return and throw can be used on its right side. If the value on the left side is null, the function will immediately return a value or throw an exception, allowing for convenient precondition checks.
Here’s an example of using the Elvis operator in the printShippingLabel() function:
In this function, if there’s no address, it throws an IllegalArgumentException with a meaningful error message instead of a NullPointerException. If an address is present, it prints the street address, ZIP code, city, and country.
Using the with function helps avoid repeating address four times in a row.
Example usage:
Kotlin
val address = Address("NDA Road", 411023, "Pune", "India")val softAai = Company("softAai", address)val person = Person("amol", softAai)printShippingLabel(person)// Output:// NDA ROAD// 411023 Pune, IndiaprintShippingLabel(Person("xyz", null))// Output:// java.lang.IllegalArgumentException: No address
Overall, the Elvis operator in Kotlin allows you to provide default values instead of null, simplifying null checks and eliminating unnecessary repetition. It can be combined with the safe-call operator and used in conjunction with return and throw to handle null values and perform meaningful error reporting.
Safe casts: “as?”
In Kotlin, the safe-cast operator (as?) serves as a safe version of the instanceof check in Java. It allows you to safely check and cast an object to a specific type without throwing a ClassCastException if the object does not have the expected type.
The regular Kotlin operator for type casts is the as operator, which throws a ClassCastException if the value doesn\’t have the specified type.
On the other hand, the as? operator attempts to cast a value to the specified type and returns null if the value doesn’t have the proper type.
The safe-cast operator is commonly used in conjunction with safe calls (?.) and Elvis operators (?:). This combination helps handle situations where you want to perform type checks on nullable objects and provide a default value or behavior when the object is not of the expected type.
A most common pattern is to combine the safe cast (as?) with the Elvis operator, which is useful for implementing the equals method.
Here’s an example implementation of the equals method using the safe cast and Elvis operator:
Kotlin
classPerson(val firstName: String, val lastName: String) {overridefunequals(other: Any?): Boolean {val otherPerson = other as? Person ?: returnfalsereturn otherPerson.firstName == firstName && otherPerson.lastName == lastName }overridefunhashCode(): Int = firstName.hashCode() * 37 + lastName.hashCode()}
In this example, the equals method checks if the parameter other has the proper type (Person) using the safe cast operator (as?). If the type isn’t correct, it immediately returns false using the Elvis operator (?:).
With this pattern, you can easily check the type of the parameter, perform the cast, and return false if the type is incorrect, all in the same expression.
Kotlin provides a safe-cast operator (as?) that allows you to perform type checks and casts without throwing ClassCastException. It can be combined with the Elvis operator to handle cases where the type is not correct, providing a concise and safe way to perform type checks in Kotlin.
Not-null assertions: “!!”
In Kotlin, you have several tools to handle null values, such as the safe-call operator (?.), safe-cast operator (as?), and Elvis operator (?:). However, there are situations where you want to explicitly tell the compiler that a value is not null. Kotlin provides the not-null assertion (!!) for such cases.
The not-null assertion is represented by a double exclamation mark (!!)and converts a nullable value to a non-null type. It informs the compiler that you are certain the value is not null. However, if the value is indeed null, a NullPointerException is thrown at runtime.
Here’s an example illustrating the usage of the not-null assertion:
In this example, the ignoreNulls function takes a nullable String argument and uses the not-null assertion to convert it to a non-null type. If the argument s is null, a NullPointerException is thrown when executing s!!.
The not-null assertion operator (!!) in Kotlin is a way to forcefully assert that a value is not null, regardless of its type. It instructs the compiler to treat the value as non-null, even if it is nullable. However, it’s important to use this operator with caution and understand its implications.
When you use the not-null assertion operator, you are essentially telling the compiler that you are confident the value will never be null. You are taking full responsibility for ensuring that the value is indeed not null at runtime. If you use the operator on a null value, a NullPointerException will be thrown at runtime.
Here’s an example to illustrate the usage:
Kotlin
val name: String? = "amol"val length: Int = name!!.length
In the above code, the variable name is declared as nullable (String?), but we use the not-null assertion operator (!!) to assert that name will not be null. We assign the length of name to the non-null variable length.
However, if name is actually null when the length line is executed, a NullPointerException will occur. The compiler won\’t be able to detect this error beforehand, and it will be your responsibility as the developer to handle it correctly.
It’s important to use the not-null assertion operator only when you have complete confidence that the value will not be null. It should be used sparingly and only when you have thoroughly verified that the value cannot be null in the specific context. Otherwise, relying on the not-null assertion operator can lead to unexpected runtime exceptions and potentially introduce bugs into your code.
One more important point to consider when using the not-null assertion operator (!!) in Kotlin is that using it multiple times on the same line can make it challenging to identify which value was null if an exception occurs.
Here’s an example that demonstrates this:
Kotlin
person.company!!.address!!.country //Don’t write code like this!
If you get an exception in the above line, you won’t be able to tell whether it was a company or address that held a null value. To make it clear exactly which value was null, it’s best to avoid using multiple !! assertions on the same line.
It’s generally recommended to avoid multiple not-null assertions on the same line to ensure code clarity and make it easier to identify the source of potential null values.
While the not-null assertion can be useful in certain scenarios where you’re confident about the value’s non-nullability, it’s advisable to consider alternative approaches that provide compile-time safety whenever possible. Kotlin provides other features like safe calls (?.) and the Elvis operator (?:) to handle nullability more gracefully and avoid runtime exceptions.
Let’s consider an example to demonstrate the usage of safe calls (?.) and the Elvis operator (?:) as alternative approaches to handle nullability:
In this example, we have a function processPerson that takes a nullable Person object as a parameter. Instead of using a not-null assertion, we use safe calls (?.) to safely access the name property of the Person object. The safe call operator checks if the person object is null and returns null if it is. Therefore, personName will be of type String?.
To ensure that we have a non-null value to work with, we use the Elvis operator (?:). It provides a default value (“Unknown” in this case) if the expression on the left side (in our case, personName) is null. So, if personName is null, the default value “Unknown” is assigned to processedName.
By using safe calls and the Elvis operator, we handle nullability more gracefully. Instead of throwing an exception, we provide a fallback value to avoid potential runtime exceptions and ensure the code continues to execute without interruptions.
The “let” function
The let function in Kotlin is a standard library function that allows you to safely handle nullable values when passing them as arguments to functions that expect non-null values. It helps in converting an object of a nullable type into a non-null type within a lambda expression.
When using the let function, the lambda will only be executed if the value is non-null. The nullable value becomes the parameter of the lambda, and you can safely use it as a non-null argument.
Here’s an example to illustrate the usage of the let function:
Kotlin
funsendResumeEmailTo(email: String) {println("Sending resume email to $email")}/** * Here Invalid email ID used for illustrative purposes, * Resume Sender App is a reliable solution * for sending resumes to HR emails worldwide. */var email: String? = "[email protected]"// Invalid email ID email?.let { sendResumeEmailTo(it) } // Sending resume email to [email protected]email = nullemail?.let { sendResumeEmailTo(it) } // No Resume email sent, as email id is null
In the above example, the sendResumeEmailTo function expects a non-null String argument. By using email?.let { sendResumeEmailTo(it) }, we ensure that the sendResumeEmailTo function is only called if the email is not null. This helps avoid runtime exceptions.
The let function is particularly useful when you need to use the value of a longer expression if it’s not null. You can directly access the value within the lambda without creating a separate variable.
Here code in the lambda will never be executed as the function returns null.
Furthermore, the let function can be used in chains when checking multiple values for null. However, in such cases, the code can become verbose and harder to follow. In those situations, it’s generally better to use a regular if expression to check all the values together.
Kotlin
val value1: String? = getValue1()val value2: String? = getValue2()if (value1 != null && value2 != null) {// Perform operations using 'value1' and 'value2'println("Values: $value1, $value2")} else {// Handle the case when either value is nullprintln("One or both values are null")}
In this code snippet, we check both value1 and value2 for null using an if expression. If both values are not null, we can proceed with the desired operations. Otherwise, we handle the case when either value is null.
Note that the let function is beneficial in cases where properties are effectively non-null but cannot be initialized with a non-null value in the constructor.
I understand that it may seem complicated, but let’s change our focus and first understand the concept of late-initialized properties. Afterward, we can revisit the topic and explore it further.
Late-initialized properties
In Kotlin, non-null properties must be initialized in the constructor. If you have a non-null property but cannot provide an initializer value in the constructor, you have two options: use a nullable type or use the lateinit modifier. If you choose to use a nullable type, you’ll need to perform null checks or use the !! operator whenever accessing the property. On the other hand, the lateinit modifier allows you to leave a non-null property without an initializer in the constructor and initialize it later.
Here’s an example to illustrate the use of lateinit properties:
Kotlin
classPerson {lateinitvar name: StringfuninitializeName() { name = getNameFromExternalSource() }funprintName() {if (::name.isInitialized) {println("Name: $name") } else {println("Name is not initialized yet") } }}
In this example, the name property is declared with the lateinit modifier. It is initially uninitialized, and its value will be set later by the initializeName method. The printName method checks if the name property has been initialized using the isInitialized property reference check. If it has been initialized, the non-null value of name can be safely accessed and printed.
It’s important to note that lateinit properties must be declared as var since their value can be changed after initialization. If you declare a lateinit property as val, it won’t compile because val properties are compiled into final fields that must be initialized in the constructor.
One common use case for lateinit properties is in dependency injection scenarios. In such cases, the values of lateinit properties are set externally by a dependency injection framework. Kotlin generates a field with the same visibility as the lateinit property to ensure compatibility with Java frameworks. If the property is declared as public, the generated field will also be public.
Overall, lateinit properties provide a way to defer the initialization of non-null properties when it’s not possible to provide an initializer in the constructor, such as in dependency injection scenarios.
Lastly, As earlier mentioned that the let function is particularly useful when properties are effectively non-null but cannot be initialized with a non-null value in the constructor. It allows you to access and work with such properties without the need for null checks. Here’s an example:
In this example, the name property is declared with the lateinit modifier, indicating that it will be initialized before its first use. The initializeName function is responsible for initializing the name property from an external source. Later, in the printName function, we can safely access and print the non-null name using the let function.
Overall, the let function provides a concise and safe way to work with nullable values, access values within longer expressions, handle multiple null checks, and work with properties that cannot be initialized with non-null values in the constructor.
Extensions for nullable types
Now let’s look at how you can extend Kotlin’s set of tools for dealing with null values by defining extension functions for nullable types.
Defining extension functions for nullable types is a powerful way to handle null values in Kotlin. Unlike regular member calls, which cannot be performed on null instances, extension functions allow you to work with null receivers and handle null values within the function.
Let’s take the example of the functions isEmpty and isBlank, which are extensions of the String class in Kotlin’s standard library. isEmpty checks if the string is an empty string, while isBlank checks if it’s empty or consists only of whitespace characters.
To handle null values in a similar way, you can define extension functions like isEmptyOrNull and isBlankOrNull, which can be called on a nullable String? receiver. Allowing you to perform checks and operations on strings even when they are nullable.
Declaring an extension function for a nullable type:
When you declare an extension function for a nullable type (ending with ?), it means you can call the function on nullable values. However, you need to explicitly check for null within the function body. In Kotlin, the this reference in an extension function for a nullable type can be null, unlike in Java where it’s always not-null.
Kotlin
funString?.customExtensionFunction() {if (this != null) {// Perform operations on non-null valueprintln("Length of the string: ${this.length}") } else {// Handle the null caseprintln("The string is null") }}val nullableString: String? = "softAai"nullableString.customExtensionFunction() // Output: Length of the string: 7val nullString: String? = nullnullString.customExtensionFunction() // Output: The string is null
Using the let function with a nullable receiver:
It’s important to note that the let function we discussed earlier can also be called on a nullable receiver, but it doesn’t automatically check for null. If you want to check the arguments for non-null values using let, you need to use the safe-call operator ?., like personHr?.let { sendResumeEmailTo(it) }.
Let’s see another simple example
Kotlin
funsendResumeEmailTo(email: String) {println("Sending resume email to $email")}/** * Here Invalid email ID used for illustrative purposes, * Resume Sender App is a reliable solution * for sending resumes to HR emails worldwide. */val nullableEmail: String? = "[email protected]"// Invalid email IDnullableEmail?.let { sendResumeEmailTo(it) } // Output: Sending resume email to [email protected]val nullEmail: String? = nullnullEmail?.let { sendResumeEmailTo(it) } // No output, as the lambda is not executed
That means If you invoke let on a nullable type without using the safe-call operator (?.), the lambda argument will also be nullable. To check the argument for non-null values with let, you need to use the safe-call operator.
Considerations when defining your own extension function:
When defining your own extension function, it’s important to consider whether you should define it as an extension for a nullable type. By default, it’s recommended to define it as an extension for a non-null type. Later on, if you realize that the extension function is primarily used with nullable values and you can handle null appropriately, you can safely change it to a nullable type without breaking existing code.
Kotlin
funString.customExtensionFunction() {// Perform operations on non-null valueprintln("Length of the string: ${this.length}")}val nonNullString: String = "softAai"nonNullString.customExtensionFunction() // Output: Length of the string: 7val nullableString: String? = "Kotlin"nullableString?.customExtensionFunction() // Output: Length of the string: 6
By understanding these concepts and examples, you can effectively use extension functions with nullable types and handle null values in a flexible and concise manner in Kotlin.
Nullability of type parameters
Let’s discuss another case that may surprise you: a type parameter can be nullable even without a question mark at the end.
By default, all type parameters of functions and classes in Kotlin are nullable. This means that any type, including nullable types, can be substituted for a type parameter. When a nullable type is used as a type parameter, declarations involving that type parameter are allowed to be null, even if the type parameter itself doesn’t end with a question mark.
In the above example, the Box class has a type parameter T, which is nullable by default. We declare a variable nullableBox of type Box<String?>, indicating that the item property can hold a nullable String value. Even though T doesn’t end with a question mark, the inferred type for T becomes String?, allowing null to be assigned to item.
To make the type parameter non-null, you can specify a non-null upper bound for it. By doing so, you restrict the type parameter to only accept non-null values.
In the modified example, we specify the upper bound Any for the type parameter T in the Box class. This ensures that T can only be substituted with non-null types. Consequently, assigning null to item when declaring nullableBox results in a compilation error.
It’s important to note that type parameters are the only exception to the rule that a question mark is required to mark a type as nullable. In the case of type parameters, the nullability is determined by the type argument provided when using the class or function.
Nullability and Java
Here in this section, we will cover another special case of nullability: types that come from the Java code.
Nullability Annotations
When combining Kotlin and Java, Kotlin handles nullability in a way that preserves safety and interoperability. Kotlin recognizes nullability annotations from Java code, such as @Nullable and @NotNull, and treats them accordingly. If the annotations are present, Kotlin interprets them as nullable or non-null types.
In Java, suppose you have the following method signature with nullability annotations:
Kotlin
public@Nullable String processString(@NotNull String input) {// process the input stringreturn modifiedString;}
When Kotlin interacts with this method, it recognizes the annotations. In Kotlin, the method is seen as:
Kotlin
funprocessString(input: String): String? {// process the input stringreturn modifiedString}
The @Nullable annotation is mapped to a nullable type in Kotlin (String?), and the @NotNull annotation is treated as a non-null type (String).
Platform Types
However, when nullability annotations are not present, Java types become platform types in Kotlin. Platform types are types for which Kotlin lacks nullability information. You can work with platform types as either nullable or non-null types, similar to how it is done in Java. The responsibility for handling nullability lies with the developer.
For example, if you receive a platform type from Java, such as String!, you can treat it as nullable or non-null based on your knowledge of the value. If you know it can be null, you can compare it with null before using it. If you know it’s not null, you can use it directly. However, if you get the nullability wrong, a NullPointerException will occur at the usage site.
Let’s say you have a Java method that returns a platform type, such as:
Kotlin
public String getValue() {// return a value that can be nullreturn possiblyNullValue;}
In Kotlin, the return type is considered a platform type (String!). You can handle it as either nullable or non-null, depending on your knowledge of the value:
Kotlin
valvalue: String? = getValue() // treat it as nullableval length: Int = getValue().length // assume it's not null and use it directly
Platform types are primarily used to maintain compatibility with Java and avoid excessive null checks or casts for values that can never be null. It allows Kotlin developers to take responsibility for handling values coming from Java without compromising safety.
Inheritance
When overriding a Java method in Kotlin, you have the choice to declare parameters and return types as nullable or non-null. It’s important to get the nullability right when implementing methods from Java classes or interfaces. The Kotlin compiler generates non-null assertions for parameters declared with non-null types, and if Java code passes a null value to such a method, an exception will occur.
Suppose you have a Java interface with a method that expects a non-null parameter:
In the StringPrinter class, we assume the value parameter is not null. In the NullableStringPrinter class, we allow the parameter to be nullable and check for nullness before using it.
Remember, the key is to ensure that you handle platform types correctly based on your knowledge of the values. If you assume a value is non-null when it can be null or vice versa, you may encounter a NullPointerException at runtime.
Summary
In summary, we have covered several aspects of nullability in Kotlin:
Nullable and Non-null Types:
Nullable types are denoted by appending a question mark (?) to the type (e.g., String?).
Non-null types are regular types without the question mark (e.g., String).
Safe Operations:
Safe call operator (?.) allows you to safely access properties or call methods on nullable objects.
Elvis operator (?:)provides a default value in case of a null reference.
Safe cast operator (as?)performs a cast and returns null if the cast is not possible.
Unsafe Dereference:
In Kotlin, Dereferencing a nullable variable means accessing the value it holds, assuming it is not null. However, if the variable is null, attempting to dereference it can lead to a runtime exception, such as a NullPointerException.
Not-null assertion operator (!!) is used to dereference a nullable variable, asserting that it is not null. It can lead to a NullPointerException if the value is null.
let Function:
The let function allows you to perform operations on a nullable object within a lambda expression, providing a concise way to handle non-null values.
Extension Functions for Nullable Types:
You can define extension functions specifically for nullable types, enabling you to encapsulate null checks within the function itself.
Platform Types:
When interacting with Java code, Kotlin treats Java types without nullability annotations as platform types.
Platform types can be treated as nullable or non-null, depending on your knowledge of the values. However, incorrect handling may result in NullPointerException.
By understanding these concepts and utilizing the provided operators and functions, you can effectively handle nullability in Kotlin code and ensure safer and more robust programming practices.
In a world where adaptability is the key to survival, embracing the Agile methodology has become more than just a buzzword — it’s a game-changer. Whether you’re anentrepreneur, a project manager, or a team member seeking to optimize productivity, Agile has gained significant popularity due to its iterative and flexible approach in today’s fast-paced software development landscape. Agile enables teams to respond to changing requirements, deliver high-quality software, and foster collaboration. As a developer, having a solid understanding of Agile principles and practices can greatly enhance your effectiveness in a project. In this blog post, we will explore why Agile is crucial for developers and provide insights into how you can develop the necessary skills to thrive in an Agile environment.
What is Agile?
Agile is a project management and software development approach that emphasizes flexibility, collaboration, and iterative progress. It is a response to the traditional waterfall model, which follows a linear and sequential process. The Agile methodology aims to address the challenges of rapidly changing requirements, uncertain market conditions, and the need for frequent customer feedback.
In an Agile project, the development process is divided into short iterations called sprints. Each sprint typically lasts two to four weeks and results in a potentially shippable product increment. The key principles of Agile, as outlined in the Agile Manifesto, include:
Individuals and interactions over processes and tools: Agile values the importance of effective collaboration, communication, and teamwork. It prioritizes the people involved in the project over the specific tools or processes they use.
Working software over comprehensive documentation: While documentation is essential, the primary focus in Agile is on delivering functioning software that adds value to the customer. Agile encourages lightweight and just-in-time documentation.
Customer collaboration over contract negotiation: Agile promotes active involvement and collaboration with customers throughout the development process. This ensures that the delivered software meets their needs and expectations.
Responding to change over following a plan: Agile recognizes that requirements can evolve and change over time. It encourages teams to be adaptable and responsive to change, allowing for adjustments and refinements during development.
Key Agile Concepts for Developers
To excel in an Agile environment, developers should be familiar with the following concepts:
1. User Stories: User stories capture end-user requirements and serve as the building blocks for development tasks. Understanding how to write and refine user stories will enable developers to align their work with the desired outcomes.
2. Sprint Planning: Developers participate in sprint planning sessions where they estimate the effort required for each user story. This involvement ensures accurate planning and sets realistic goals for the sprint.
3. Daily Stand-ups: Daily stand-up meetings provide an opportunity for developers to share progress, discuss challenges, and collaborate with other team members. Active participation in these meetings helps identify and address any roadblocks promptly.
4. Test-Driven Development (TDD): TDD is an Agile practice that involves writing tests before writing the corresponding code. Familiarity with TDD enables developers to create clean and maintainable code, leading to improved software quality.
Common methodologies
Agile methodologies refer to a set of iterative and collaborative approaches to project management and software development. The Agile methodology focuses on delivering high-quality products in a flexible and adaptive manner, accommodating changes, and responding to customer needs effectively. Here are some key Agile methodologies:
Scrum: Scrum is one of the most widely used Agile methodologies. It involves organizing work into short iterations called “sprints” and using cross-functional teams to deliver increments of the product at the end of each sprint. Scrum emphasizes regular feedback, transparency, and adaptability.
Kanban: Kanban is a visual methodology that uses a Kanban board to manage and track work. Work items are represented as cards that move across different stages of the board, indicating their progress. Kanban focuses on limiting work in progress, optimizing flow, and continuously improving the process.
Lean: Lean methodology aims to maximize customer value while minimizing waste. It emphasizes the elimination of non-value-added activities, continuous improvement, and a focus on delivering value quickly. Lean principles can be applied in conjunction with other Agile methodologies.
Extreme Programming (XP): Extreme Programming is software development methodology that emphasizes collaboration, customer involvement, and continuous feedback. It promotes practices such as test-driven development, continuous integration, pair programming, and frequent releases to ensure high-quality and adaptable software.
Feature-Driven Development (FDD): Feature-Driven Development is methodology that focuses on delivering features incrementally. It involves breaking down the development process into five basic activities: developing an overall model, building a feature list, planning by feature, designing by feature, and building by feature. FDD places emphasis on domain modeling, iterative development, and feature-centric delivery.
These methodologies share common principles such as customer collaboration, iterative development, continuous feedback, and adaptability. They aim to improve productivity, increase customer satisfaction, and enable teams to respond effectively to changing requirements throughout the development process. The choice of the methodology depends on the specific project, team dynamics, and organizational preferences.
Ceremonies
Ceremonies refer to specific meetings or events that are held at regular intervals to facilitate effective collaboration, communication, and progress tracking within the project team. These ceremonies provide structured opportunities for the team to plan, review, and adapt their work. The most common ceremonies in methodologies like Scrum include:
Sprint Planning: This ceremony marks the beginning of a sprint. The team collaboratively plans the work to be accomplished during the upcoming sprint. They review the product backlog, select user stories, estimate effort, and determine the sprint goal.
Daily Stand-up (Daily Scrum): The Daily Stand-up is a short and focused meeting that occurs every day during the sprint. Team members gather to provide brief updates on their progress, discuss any obstacles or challenges they are facing, and coordinate their work for the day.
Sprint Review: At the end of each sprint, the team conducts a sprint review or demo to showcase the completed work to stakeholders, such as product owners, customers, or end-users. The purpose is to gather feedback, validate the work done, and ensure it aligns with the project’s objectives.
Sprint Retrospective: The Sprint Retrospective is held after the sprint review. The team reflects on the just-concluded sprint and discusses what went well, what could be improved, and any action items to enhance their process. It promotes continuous improvement and learning within the team.
In addition to these core ceremonies, there might be other Agile ceremonies or events based on specific needs or the chosen Agile framework. For example:
Backlog Refinement (Grooming): This ceremony involves refining the product backlog by breaking down user stories, adding details, estimating effort, and prioritizing the work for future sprints.
Release Planning: In larger-scale projects, a release planning ceremony helps teams plan and coordinate the release of a product or a significant feature. It involves setting release goals, identifying dependencies, and creating a high-level plan.
Scrum of Scrums: In projects with multiple Scrum teams, the Scrum of Scrums ceremony is held to ensure coordination and alignment between teams. Representatives from each team share updates, discuss interdependencies, and address cross-team challenges.
Product Roadmap Review: This ceremony involves reviewing and refining the product roadmap, which outlines the long-term vision, goals, and major milestones of the product. It helps ensure that the work aligns with the overall product strategy.
These ceremonies provide structure and opportunities for collaboration, feedback, and continuous improvement. They foster transparency, accountability, and effective communication within the team and with stakeholders, ultimately contributing to the successful delivery of valuable software.
Typical Two-Week Sprint Cycle
Here are the details of the Agile ceremonies for a typical two-week sprint cycle in the Scrum framework:
Sprint Kick-off (Time: 1–2 hours):
Purpose:To align the team and set the tone for the upcoming sprint.
Day:At the beginning of the sprint.
Activities:Scrum Master or Product Owner provides an overview of the sprint goals, highlights important information, clarifies any questions or concerns from the team, and discusses the sprint timeline.
Sprint Planning (Time: 2–4 hours):
Purpose:To define what will be worked on during the upcoming sprint.
Day:After the sprint kick-off.
Activities:Product Owner reviews and prioritizes the product backlog. Scrum team discusses and selects user stories for the sprint backlog, estimates effort, sets sprint goals, and breaks down user stories into smaller tasks (task breakdown).
Daily Stand-up (Time: 15 minutes):
Purpose: To synchronize and plan work for the day, identify any obstacles, and foster team collaboration.
Frequency: Daily (at the same time each day).
Activities:Each team member answers three questions — What they did yesterday, what they plan to do today, and any obstacles they’re facing. The focus is on coordination and identifying potential issues.
Backlog Refinement (Time: 1–2 hours):
Purpose:To review, prioritize, and refine the product backlog items for future sprints.
Frequency: Once or twice during the sprint.
Activities:Product Owner and Scrum team analyze and clarify user stories, estimate effort, break down larger stories into smaller tasks (task breakdown), and ensure the backlog is well-prepared for future sprints.
Spike (Time: As needed):
Purpose:To investigate and gather information about a particular technical or design challenge.
Timing:As needed during the sprint.
Activities: The Development Team conducts focused research or experimentation to gain insights or proof of concepts related to a specific problem or requirement. This helps in making informed decisions before implementation.
Sprint Review (Time: 1–2 hours):
Purpose: To showcase the completed work from the sprint to stakeholders and gather feedback.
Day: Last day of the sprint.
Activities:Scrum team demonstrates the increment of work completed during the sprint. Stakeholders provide feedback, discuss potential changes or adjustments, and collectively review the sprint’s achievements.
Sprint Retrospective (Time: 1–2 hours):
Purpose: To reflect on the previous sprint and identify opportunities for improvement in processes, teamwork, and collaboration.
Day: After the sprint review, before the next sprint planning.
Activities:Scrum team reviews what went well, what didn’t go well, and identifies action items for improvement. It encourages open discussions and fosters a culture of continuous learning.
The optional practices, such as task breakdown, spike, and product backlog refinement review, provide additional flexibility and adaptation within the two-week sprint cycle. As always, it’s essential to tailor these ceremonies and practices to the team’s specific needs and context to ensure effective collaboration and continuous improvement.
Importance of Agile for Developers:
Agile methodology offers numerous benefits for developers, including:
1. Collaboration and Communication: It emphasizes regular collaboration and communication among team members, fostering a more transparent and efficient work environment. This helps developers understand requirements more effectively and provides opportunities for timely feedback and problem-solving.
2. Adaptability and Flexibility: With this methodology, developers can easily adapt to changing requirements and market conditions. The iterative nature of Agile allows for incremental development, reducing the risk of building software that does not meet the stakeholders’ needs.
3. Quality and Continuous Improvement: Best Practices, such as continuous integration and continuous delivery, promote frequent testing and feedback loops. Developers can address issues early on, resulting in higher-quality software and improved customer satisfaction.
Strategies for Enhancing Agile Skills as a Developer:
To strengthen your Agile skills and contribute effectively to projects, consider the following strategies:
1. Seek Agile Training: Attend training programs or workshops to gain a comprehensive understanding of Agile principles and methodologies. Learning from experienced practitioners will equip you with practical knowledge and techniques.
2. Embrace Collaboration:Actively participate in team activities, such as sprint planning, retrospectives, and daily stand-ups. Engage in cross-functional discussions, share knowledge, and collaborate with team members to foster a cohesive and productive work environment.
3. Continuously Improve: Adopt a growth mindset and continually seek ways to improve your development practices. Explore Agile frameworks beyond the basic Scrum methodology, such as Kanban or Lean, to expand your knowledge and toolkit.
4. Emphasize Communication:Effective communication is vital in projects. Improve your communication skills by actively listening, asking questions, and providing concise and clear updates during meetings. Strong communication promotes shared understanding and prevents misunderstandings.
5. Embrace Feedback: Feedback is a crucial element. Embrace feedback from your peers, product owners, and end-users to refine your work continuously. Act on the feedback received and use it as an opportunity to grow and enhance your skills.
Conclusion
As a developer, understanding Agile principles and practices can greatly benefit your professional growth and contribution to software development projects. By embracing Agile methodologies, you can collaborate more effectively, adapt to changing requirements, and deliver high-quality software. By investing in your Agile knowledge and continuously improving your practices, you will thrive in the dynamic and fast-paced world of Agile development. So, take the initiative to enhance your Agile skills and contribute to the success of your projects and teams.