Contents

Miscellaneous

Kotlin Annotations Overview

In Kotlin, annotations provide a way to attach metadata to code. This metadata can then be used by development tools, libraries, or frameworks to process the code without altering its behavior. Annotations are applied to code elements such as classes, functions, properties, or parameters and are typically evaluated at compile-time.

Annotations frequently contain the following parameters, which must be compile-time constants:

1. Primitive types (e.g., Int, Long, etc.)
2. Strings
3. Enumerations
4. Classes
5. Other annotations
6. Arrays of the types mentioned above

Applying Annotations

To apply an annotation, simply use the annotation name prefixed with the @ symbol before the code element you wish to annotate. For example:

				
					@Positive val number: Int
				
			

If an annotation accepts parameters, these can be passed inside parentheses, much like a function call:

				
					@AllowedLanguage("Kotlin")
				
			

When passing another annotation as a parameter to an annotation, omit the @ symbol. For instance:

				
					@Deprecated("Use === instead", ReplaceWith("this === other"))
				
			

When using class objects as parameters, use ::class:

				
					@Throws(IOException::class)
				
			

An annotation that requires parameters looks similar to a class with a primary constructor:

				
					annotation class Prefix(val prefix: String)
				
			
Annotating Specific Elements

1. Annotating a Constructor : You can annotate class constructors by using the constructor keyword:

				
					class MyClass @Inject constructor(dependency: MyDependency) {
    // ...
}
				
			

2. Annotating a Property : Annotations can be applied to properties within a class. For example:

				
					class Language(
    @AllowedLanguages(["Java", "Kotlin"]) val name: String
)
				
			
Built-in Annotations in Kotlin

Kotlin provides several built-in annotations that offer additional functionality. These annotations are often used to annotate other annotations.

1. @Target: The @Target annotation specifies where an annotation can be applied, such as classes, functions, or parameters. For example:

				
					@Target(AnnotationTarget.CONSTRUCTOR, AnnotationTarget.LOCAL_VARIABLE)
annotation class CustomAnnotation

class Example @CustomAnnotation constructor(val number: Int) {
    fun show() {
        println("Constructor annotated with @CustomAnnotation")
        println("Number: $number")
    }
}

fun main() {
    val example = Example(5)
    example.show()

    @CustomAnnotation val message: String
    message = "Hello Kotlin"
    println("Local variable annotated")
    println(message)
}

				
			

Output:

				
					Constructor annotated with @CustomAnnotation
Number: 5
Local variable annotated
Hello Kotlin

				
			

2. @Retention: The @Retention annotation controls how long the annotation is retained. It can be retained in the source code, in the compiled class files, or even at runtime. The parameter for this annotation is an instance of the AnnotationRetention enum:

  • SOURCE
  • BINARY
  • RUNTIME

Example:

				
					@Retention(AnnotationRetention.RUNTIME)
annotation class RuntimeAnnotation

@RuntimeAnnotation
fun main() {
    println("Function annotated with @RuntimeAnnotation")
}

				
			

Output:

				
					Function annotated with @RuntimeAnnotation
				
			

3. @Repeatable: The @Repeatable annotation allows multiple annotations of the same type to be applied to an element. This is currently limited to source retention annotations in Kotlin.

Example:

				
					@Repeatable
@Retention(AnnotationRetention.SOURCE)
annotation class RepeatableAnnotation(val value: Int)

@RepeatableAnnotation(1)
@RepeatableAnnotation(2)
fun main() {
    println("Multiple @RepeatableAnnotation applied")
}
				
			

Output:

				
					Multiple @RepeatableAnnotation applied
				
			

Kotlin Reflection

Reflection is a powerful feature that allows a program to inspect and modify its structure and behavior at runtime. Kotlin provides reflection through its kotlin.reflect package, allowing developers to work with class metadata, access members, and use features like functions and property references. Kotlin reflection is built on top of the Java reflection API but extends it with additional features, making it more functional and flexible.

Key Features of Kotlin Reflection
  • Access to Properties and Nullable Types: Kotlin reflection enables access to both properties and nullable types.
  • Enhanced Features: Kotlin reflection offers more features than Java reflection.
  • Interoperability with JVM: Kotlin reflection can seamlessly access and interact with JVM code written in other languages.
Class References in Kotlin Reflection

To obtain a class reference in Kotlin, you can use the class reference operator ::class. Class references can be obtained both statically from the class itself or dynamically from an instance. When acquired from an instance, these are known as bounded class references, which point to the exact runtime type of the object.

Example: Class References

				
					// Sample class
class ReflectionSample

fun main() {
    // Reference obtained using class name
    val classRef = ReflectionSample::class
    println("Static class reference: $classRef")

    // Reference obtained using an instance
    val instance = ReflectionSample()
    println("Bounded class reference: ${instance::class}")
}
				
			

Output:

				
					Static class reference: class ReflectionSample
Bounded class reference: class ReflectionSample

				
			
Function References

In Kotlin, you can obtain a reference to any named function by using the :: operator. Function references can be passed as parameters or stored in variables. When dealing with overloaded functions, you may need to specify the function type explicitly.

Example: Function References

				
					fun sum(a: Int, b: Int): Int = a + b
fun concat(a: String, b: String): String = "$a$b"

fun isEven(a: Int): Boolean = a % 2 == 0

fun main() {
    // Function reference for a single function
    val isEvenRef = ::isEven
    val numbers = listOf(1, 2, 3, 4, 5, 6)
    println(numbers.filter(isEvenRef))

    // Function reference for an overloaded function (explicit type)
    val concatRef: (String, String) -> String = ::concat
    println(concatRef("Hello, ", "Kotlin!"))

    // Implicit function reference usage
    val result = sum(3, 7)
    println(result)
}

				
			

Output:

				
					[2, 4, 6]
Hello, Kotlin!
10

				
			
Property References

Property references allow you to work with properties just like you do with functions. You can retrieve the property value using the get function, and you can modify it using set if it’s mutable.

Example: Property References

				
					class SampleProperty(var value: Double)

val x = 42

fun main() {
    // Property reference for a top-level property
    val propRef = ::x
    println(propRef.get()) // Output: 42
    println(propRef.name)  // Output: x

    // Property reference for a class property
    val classPropRef = SampleProperty::value
    val instance = SampleProperty(12.34)
    println(classPropRef.get(instance))  // Output: 12.34
}

				
			

Output:

				
					42
x
12.34
				
			
				
					Nested Class Property - Function Executed

				
			
Constructor References

Constructor references in Kotlin allow you to reference the constructor of a class in a similar manner to functions and properties. These references can be used to invoke constructors dynamically.

Example: Constructor References

				
					class SampleClass(val value: Int)

fun main() {
    // Constructor reference
    val constructorRef = ::SampleClass
    val instance = constructorRef(10)
    println("Value: ${instance.value}")  // Output: Value: 10
}
				
			

Output:

				
					Value: 10
				
			

Operator Overloading

In Kotlin, you have the flexibility to overload standard operators to work seamlessly with user-defined types. This means that you can provide custom behavior for operators like +, -, *, and more, making code that uses your custom types more intuitive. Kotlin allows overloading for unary, binary, relational, and other operators by defining specific functions using the operator keyword.

Unary Operators

Unary operators modify a single operand. The corresponding functions for unary operators must be defined in the class that they will operate on.

Operator ExpressionCorresponding Function
+x, -xx.unaryPlus(), x.unaryMinus()
!xx.not()

Here, x is the instance on which the operator is applied.

Example: Unary Operator Overloading

				
					class UnaryExample(var message: String) {
    // Overloading the unaryMinus operator
    operator fun unaryMinus() {
        message = message.reversed()
    }
}

fun main() {
    val obj = UnaryExample("KOTLIN")
    println("Original message: ${obj.message}")
    
    // Using the overloaded unaryMinus function
    -obj
    println("After applying unary operator: ${obj.message}")
}

				
			

Output:

				
					Original message: KOTLIN
After applying unary operator: NILTOK

				
			
Increment and Decrement Operators

Increment (++) and decrement (--) operators can be overloaded using the following functions. These functions typically return a new instance after performing the operation.

Operator ExpressionCorresponding Function
++x or x++x.inc()
--x or x--x.dec()

Example: Increment and Decrement Operator Overloading

				
					class IncDecExample(var text: String) {
    // Overloading the increment function
    operator fun inc(): IncDecExample {
        return IncDecExample(text + "!")
    }

    // Overloading the decrement function
    operator fun dec(): IncDecExample {
        return IncDecExample(text.dropLast(1))
    }

    override fun toString(): String {
        return text
    }
}

fun main() {
    var obj = IncDecExample("Hello")
    println(obj++)  // Output: Hello
    println(obj)    // Output: Hello!
    println(obj--)  // Output: Hello
    println(obj)    // Output: Hello
}

				
			

Output:

				
					Hello
Hello!
Hello
Hello
				
			
Binary Operators

Binary operators operate on two operands. The following table shows how to define functions for common binary operators.

Operator ExpressionCorresponding Function
x1 + x2x1.plus(x2)
x1 - x2x1.minus(x2)
x1 * x2x1.times(x2)
x1 / x2x1.div(x2)
x1 % x2x1.rem(x2)

Example: Overloading the + Operator

				
					class DataHolder(var name: String) {
    // Overloading the plus operator
    operator fun plus(number: Int) {
        name = "Data: $name, Number: $number"
    }

    override fun toString(): String {
        return name
    }
}

fun main() {
    val obj = DataHolder("Info")
    obj + 42  // Calling the overloaded plus operator
    println(obj)  // Output: Data: Info, Number: 42
}

				
			

Output:

				
					Data: Info, Number: 42
				
			
Other Operators

Kotlin provides the flexibility to overload a wide variety of operators, some of which include range, contains, indexing, and invocation.

Operator ExpressionCorresponding Function
x1 in x2x2.contains(x1)
x[i]x.get(i)
x[i] = valuex.set(i, value)
x()x.invoke()
x1 += x2x1.plusAssign(x2)

Example: Overloading the get Operator for Indexing

				
					class CustomList(val items: List<String>) {
    // Overloading the get operator to access list items
    operator fun get(index: Int): String {
        return items[index]
    }
}

fun main() {
    val myList = CustomList(listOf("Kotlin", "Java", "Python"))
    println(myList[0])  // Output: Kotlin
    println(myList[2])  // Output: Python
}
				
			

Output:

				
					Kotlin
Python
				
			

Destructuring Declarations in Kotlin

Kotlin offers a distinctive way of handling instances of a class through destructuring declarations. A destructuring declaration lets you break down an object into multiple variables at once, making it easier to work with data.

Example:

				
					val (id, pay) = employee
				
			

In this example, id and pay are initialized using the properties of the employee object. These variables can then be used independently in the code:

				
					println("$id $pay")
				
			

Destructuring declarations rely on component() functions. For each variable in a destructuring declaration, the corresponding class must provide a componentN() function, where N represents the variable’s position (starting from 1). In Kotlin, data classes automatically generate these component functions.

Destructuring Declaration Compiles to:

				
					val id = employee.component1()
val pay = employee.component2()

				
			

Example: Returning Two Values from a Function

				
					// Data class example
data class Info(val title: String, val year: Int)

// Function returning a data class
fun getInfo(): Info {
    return Info("Inception", 2010)
}

fun main() {
    val infoObj = getInfo()
    // Accessing properties using the object
    println("Title: ${infoObj.title}")
    println("Year: ${infoObj.year}")

    // Using destructuring declaration
    val (title, year) = getInfo()
    println("Title: $title")
    println("Year: $year")
}

				
			

Output:

				
					Title: Inception
Year: 2010
Title: Inception
Year: 2010
				
			
Underscore for Unused Variables

Sometimes you may not need all the variables in a destructuring declaration. To skip a variable, you can replace its name with an underscore (_). In this case, the corresponding component function is not called.

Destructuring in Lambdas

As of Kotlin 1.1, destructuring declarations can also be used within lambda functions. If a lambda parameter is of type Pair or any type that provides component functions, you can destructure it within the lambda.

Example: Destructuring in Lambda Parameters

				
					fun main() {
    val people = mutableMapOf<Int, String>()
    people[1] = "Alice"
    people[2] = "Bob"
    people[3] = "Charlie"

    println("Original map:")
    println(people)

    // Destructuring map entry into key and value
    val updatedMap = people.mapValues { (_, name) -> "Hello $name" }
    println("Updated map:")
    println(updatedMap)
}

				
			

Output:

				
					Original map:
{1=Alice, 2=Bob, 3=Charlie}
Updated map:
{1=Hello Alice, 2=Hello Bob, 3=Hello Charlie}

				
			

In this example, the mapValues function uses destructuring to extract the value and update it. The underscore (_) is used for the key, as it is not needed.

Equality evaluation

Kotlin offers a distinct feature that allows comparison of instances of a particular type in two different ways. This feature sets Kotlin apart from other programming languages. The two types of equality in Kotlin are:

Structural Equality

Structural equality is checked using the == operator and its inverse, the != operator. By default, when you use x == y, it is translated to a call of the equals() function for that type. The expression:

				
					x?.equals(y) ?: (y === null)
				
			

It means that if x is not null, it calls the equals(y) function. If x is null, it checks whether y is also referentially equal to null. Note: When x == null, the code automatically defaults to referential equality (x === null), so there’s no need to optimize the code in this case. To use == on instances, the type must override the equals() function. For example, when comparing strings, the structural equality compares their contents.

Referential Equality

Referential equality in Kotlin is checked using the === operator and its inverse !==. This form of equality returns true only when both instances refer to the same location in memory. When used with types that are converted to primitive types at runtime, the === check is transformed into ==, and the !== check is transformed into !=.

Here is a Kotlin program to demonstrate structural and referential equality:

				
					class Circle(val radius: Int) {
    override fun equals(other: Any?): Boolean {
        if (other is Circle) {
            return other.radius == radius
        }
        return false
    }
}

// main function
fun main(args: Array<String>) {
    val circle1 = Circle(7)
    val circle2 = Circle(7)
    
    // Structural equality
    if (circle1 == circle2) {
        println("Two circles are structurally equal")
    }
    
    // Referential equality
    if (circle1 !== circle2) {
        println("Two circles are not referentially equal")
    }
}

				
			

Output:

				
					Two circles are structurally equal
Two circles are not referentially equal
				
			

Comparator

In programming, when defining a new type, there’s often a need to establish an order for its instances. To compare instances, Kotlin provides the Comparable interface. However, for more flexible and customizable ordering based on different parameters, Kotlin offers the Comparator interface. This interface compares two objects of the same type and arranges them in a defined order.

Functions
  • compare: This method compares two instances of a type. It returns 0 if both are equal, a negative number if the second instance is greater, or a positive number if the first instance is greater.

				
					abstract fun compare(a: T, b: T): Int
				
			
Extension Functions
  • reversed: This function takes a comparator and reverses its sorting order.

				
					fun <T> Comparator<T>.reversed(): Comparator<T>
				
			
  • then: Combines two comparators. The second comparator is only used when the first comparator considers the two values to be equal.
				
					infix fun <T> Comparator<T>.then(comparator: Comparator<in T>): Comparator<T>

				
			

Example demonstrating compare, then, and reversed functions:

				
					// A simple class representing a car
class Car(val make: String, val year: Int) {
    override fun toString(): String {
        return "$make ($year)"
    }
}

// Comparator to compare cars by make
class MakeComparator : Comparator<Car> {
    override fun compare(o1: Car?, o2: Car?): Int {
        if (o1 == null || o2 == null) return 0
        return o1.make.compareTo(o2.make)
    }
}

// Comparator to compare cars by year
class YearComparator : Comparator<Car> {
    override fun compare(o1: Car?, o2: Car?): Int {
        if (o1 == null || o2 == null) return 0
        return o1.year.compareTo(o2.year)
    }
}

fun main() {
    val cars = arrayListOf(
        Car("Toyota", 2020),
        Car("Ford", 2018),
        Car("Toyota", 2015),
        Car("Ford", 2022),
        Car("Tesla", 2021)
    )

    println("Original list:")
    println(cars)

    val makeComparator = MakeComparator()
    // Sorting cars by make
    cars.sortWith(makeComparator)
    println("List sorted by make:")
    println(cars)

    val yearComparator = YearComparator()
    val combinedComparator = makeComparator.then(yearComparator)
    // Sorting cars by make, then by year
    cars.sortWith(combinedComparator)
    println("List sorted by make and year:")
    println(cars)

    val reverseComparator = combinedComparator.reversed()
    // Reverse sorting the cars
    cars.sortWith(reverseComparator)
    println("List reverse sorted:")
    println(cars)
}

				
			

Output:

				
					Original list:
[Toyota (2020), Ford (2018), Toyota (2015), Ford (2022), Tesla (2021)]
List sorted by make:
[Ford (2018), Ford (2022), Tesla (2021), Toyota (2015), Toyota (2020)]
List sorted by make and year:
[Ford (2018), Ford (2022), Tesla (2021), Toyota (2015), Toyota (2020)]
List reverse sorted:
[Toyota (2020), Toyota (2015), Tesla (2021), Ford (2022), Ford (2018)]

				
			
Additional Extension Functions
  • thenBy: This function converts the instances of a type to a Comparable and compares them using the transformed values.

				
					fun <T> Comparator<T>.thenBy(selector: (T) -> Comparable<*>?): Comparator<T>
				
			
  • thenByDescending: Similar to thenBy, but sorts the instances in descending order.
				
					inline fun <T> Comparator<T>.thenByDescending(crossinline selector: (T) -> Comparable<*>?): Comparator<T>

				
			

Example demonstrating thenBy and thenByDescending functions:

				
					class Product(val price: Int, val rating: Int) {
    override fun toString(): String {
        return "Price = $price, Rating = $rating"
    }
}

fun main() {
    val comparator = compareBy<Product> { it.price }
    val products = listOf(
        Product(100, 4),
        Product(200, 5),
        Product(150, 3),
        Product(100, 3),
        Product(200, 4)
    )

    println("Sorted first by price, then by rating:")
    val priceThenRatingComparator = comparator.thenBy { it.rating }
    println(products.sortedWith(priceThenRatingComparator))

    println("Sorted by rating, then by descending price:")
    val ratingThenPriceDescComparator = compareBy<Product> { it.rating }
        .thenByDescending { it.price }
    println(products.sortedWith(ratingThenPriceDescComparator))
}

				
			

Output:

				
					Sorted first by price, then by rating:
[Price = 100, Rating = 3, Price = 100, Rating = 4, Price = 150, Rating = 3, Price = 200, Rating = 4, Price = 200, Rating = 5]
Sorted by rating, then by descending price:
[Price = 150, Rating = 3, Price = 100, Rating = 3, Price = 100, Rating = 4, Price = 200, Rating = 4, Price = 200, Rating = 5]

				
			
Additional Functions
  • thenComparator: Combines a primary comparator with a custom comparison function.

				
					fun <T> Comparator<T>.thenComparator(comparison: (a: T, b: T) -> Int): Comparator<T>
				
			
  • thenDescending: Combines two comparators and sorts the elements in descending order based on the second comparator if the values are equal according to the first.
				
					infix fun <T> Comparator<T>.thenDescending(comparator: Comparator<in T>): Comparator<T>
				
			

Example demonstrating thenComparator and thenDescending functions:

				
					fun main() {
    val pairs = listOf(
        Pair("Apple", 5),
        Pair("Banana", 2),
        Pair("Apple", 3),
        Pair("Orange", 2),
        Pair("Banana", 5)
    )

    val comparator = compareBy<Pair<String, Int>> { it.first }
        .thenComparator { a, b -> compareValues(a.second, b.second) }

    println("Pairs sorted by first element, then by second:")
    println(pairs.sortedWith(comparator))

    val descendingComparator = compareBy<Pair<String, Int>> { it.second }
        .thenDescending(compareBy { it.first })

    println("Pairs sorted by second element, then by first in descending order:")
    println(pairs.sortedWith(descendingComparator))
}

				
			

Output:

				
					Pairs sorted by first element, then by second:
[(Apple, 3), (Apple, 5), (Banana, 2), (Banana, 5), (Orange, 2)]
Pairs sorted by second element, then by first in descending order:
[(Banana, 5), (Apple, 5), (Banana, 2), (Orange, 2), (Apple, 3)]

				
			

Triple

In programming, functions are invoked to perform specific tasks. A key benefit of using functions is their ability to return values after computation. For instance, an add() function consistently returns the sum of the input numbers. However, a limitation of functions is that they typically return only one value at a time. When there’s a need to return multiple values of different types, one approach is to define a class with the desired variables and then return an object of that class. This method, though effective, can lead to increased verbosity, especially when dealing with multiple functions requiring multiple return values.

To simplify this process, Kotlin provides a more elegant solution through the use of Pair and Triple.

What is Triple?

Kotlin offers a simple way to store three values in a single object using the Triple class. This is a generic data class that can hold any three values. The values in a Triple have no inherent relationship beyond being stored together. Two Triple objects are considered equal if all three of their components are identical.

Class Definition:

				
					data class Triple<out A, out B, out C> : Serializable
				
			
Parameters:
  • A: The type of the first value.
  • B: The type of the second value.
  • C: The type of the third value.
Constructor:

In Kotlin, constructors initialize variables or properties of a class. To create an instance of Triple, you use the following syntax:

				
					Triple(first: A, second: B, third: C)

				
			

Example: Creating a Triple

				
					fun main() {
    val (a, b, c) = Triple(42, "Hello", true)
    println(a)
    println(b)
    println(c)
}

				
			

Output:

				
					42
Hello
true
				
			
Properties:

You can either deconstruct the values of a Triple into separate variables (as shown above), or you can access them using the properties first, second, and third:

  • first: Holds the first value.
  • second: Holds the second value.
  • third: Holds the third value.

Example: Accessing Triple Values Using Properties

				
					fun main() {
    val triple = Triple("Kotlin", 1.6, listOf(100, 200, 300))
    println(triple.first)
    println(triple.second)
    println(triple.third)
}
				
			

Output:

				
					Kotlin
1.6
[100, 200, 300]
				
			
Functions:
  • toString(): This function returns a string representation of the Triple.Example: Using toString()
				
					fun main() {
    val triple1 = Triple(10, 20, 30)
    println("Triple as string: " + triple1.toString())

    val triple2 = Triple("A", listOf("X", "Y", "Z"), 99)
    println("Another Triple as string: " + triple2.toString())
}

				
			

Output:

				
					Triple as string: (10, 20, 30)
Another Triple as string: (A, [X, Y, Z], 99)
				
			
Extension Functions:

Kotlin also allows you to extend existing classes with new functionality through extension functions.

  • toList(): This extension function converts the Triple into a list. Example: Using toList()
				
					fun main() {
    val triple1 = Triple(1, 2, 3)
    val list1 = triple1.toList()
    println(list1)

    val triple2 = Triple("Apple", 3.1415, listOf(7, 8, 9))
    val list2 = triple2.toList()
    println(list2)
}
				
			

Output:

				
					[1, 2, 3]
[Apple, 3.1415, [7, 8, 9]]

				
			

Pair

In programming, we often use functions to perform specific tasks. One of the advantages of functions is their ability to be called multiple times, consistently returning a result after computation. For example, an add() function always returns the sum of two given numbers.

However, functions typically return only one value at a time. When there’s a need to return multiple values of different data types, one common approach is to create a class containing the required variables, then instantiate an object of that class to hold the returned values. While effective, this approach can make the code verbose and complex, especially when many functions return multiple values.

To simplify this, Kotlin provides the Pair and Triple data classes.

What is Pair?

Kotlin offers a simple way to store two values in a single object using the Pair class. This generic class can hold two values, which can be of the same or different data types. The two values may or may not have a relationship. Comparison between two Pair objects is based on their values: two Pair objects are considered equal if both of their values are identical.

Class Definition:

				
					data class Pair<out A, out B> : Serializable
				
			

Parameters:

  • A: The type of the first value.
  • B: The type of the second value.

Constructor:

Kotlin constructors are special functions that are called when an object is created, primarily to initialize variables or properties. To create an instance of Pair, use the following syntax:

				
					Pair(first: A, second: B)

				
			

Example: Creating a Pair

				
					fun main() {
    val (a, b) = Pair(42, "World")
    println(a)
    println(b)
}
				
			

Output:

				
					42
World
				
			
Properties:

You can either destructure a Pair into separate variables (as shown above), or access the values using the properties first and second:

  • first: Holds the first value.
  • second: Holds the second value.

Example: Accessing Pair Values Using Properties

				
					fun main() {
    val pair = Pair("Hello Kotlin", "This is a tutorial")
    println(pair.first)
    println(pair.second)
}
				
			

Output:

				
					Hello Kotlin
This is a tutorial
				
			
Functions:
  • toString(): This function returns a string representation of the Pair. Example: Using toString()
				
					fun main() {
    val pair1 = Pair(10, 20)
    println("Pair as string: " + pair1.toString())

    val pair2 = Pair("Alpha", listOf("Beta", "Gamma", "Delta"))
    println("Another Pair as string: " + pair2.toString())
}
				
			

Output:

				
					Pair as string: (10, 20)
Another Pair as string: (Alpha, [Beta, Gamma, Delta])

				
			
Extension Functions:

Kotlin allows extending existing classes with new functionality using extension functions.

  • toList(): This extension function converts the Pair into a list.

Example: Using toList()

				
					fun main() {
    val pair1 = Pair(3, 4)
    val list1 = pair1.toList()
    println(list1)

    val pair2 = Pair("Apple", "Orange")
    val list2 = pair2.toList()
    println(list2)
}

				
			

Output:

				
					[3, 4]
[Apple, Orange]
				
			

apply vs with

In Kotlin, apply is an extension function that operates within the context of the object it is invoked on. It allows you to configure or manipulate the object’s properties within its scope and returns the same object after performing the desired changes. The primary use of apply is not limited to just setting properties; it can execute more complex logic before returning the modified object.

Key characteristics of apply:

  • It is an extension function on a type.
  • It requires an object reference to execute within an expression.
  • After completing its operation, it returns the modified object.

Definition of apply:

				
					inline fun T.apply(block: T.() -> Unit): T {
    block()
    return this
}
				
			

Example of apply:

				
					fun main() {
    data class Example(var value1: String, var value2: String, var value3: String)

    // Creating an instance of Example class
    var example = Example("Hello", "World", "Before")

    // Using apply to change the value3
    example.apply { this.value3 = "After" }

    println(example)
}

				
			

Example Demonstrating Interface Implementation:

				
					interface Machine {
    fun powerOn()
    fun powerOff()
}

class Computer : Machine {
    override fun powerOn() {
        println("Computer is powered on.")
    }

    override fun powerOff() {
        println("Computer is shutting down.")
    }
}

fun main() {
    val myComputer = Computer()
    myComputer.powerOn()
    myComputer.powerOff()
}

				
			

Output:

				
					Example(value1=Hello, value2=World, value3=After)

				
			

In this example, the third property value3 of the Example class is modified from "Before" to "After" using apply.

Kotlin: with

Similar to apply, the with function in Kotlin is used to modify properties of an object. However, unlike apply, with does not require the object reference explicitly. Instead, the object is passed as an argument, and the operations are performed without using the dot operator for the object reference.

Definition of with:

				
					inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    return receiver.block()
}
				
			

Example of with:

				
					fun main() {
    data class Example(var value1: String, var value2: String, var value3: String)

    var example = Example("Hello", "World", "Before")

    // Using with to modify value1 and value3
    with(example) {
        value1 = "Updated"
        value3 = "After"
    }

    println(example)
}
				
			

Output:

				
					Example(value1=Updated, value2=World, value3=After)
				
			

In this case, using with, we update the values of value1 and value3 without needing to reference the object with a dot operator.

Difference Between apply and with
  • apply is invoked on an object and runs within its context, requiring the object reference.
  • with does not require an explicit object reference and simply passes the object as an argument.
  • apply returns the object itself, while with can return a result of the block’s execution.