Google News
logo
Kotlin Interview Questions
Kotlin is a statically typed programming language developed by JetBrains, the same company behind popular IDEs like IntelliJ IDEA. It was designed to address the limitations and challenges faced by developers when working with Java, while maintaining full compatibility with the Java Virtual Machine (JVM) and existing Java codebases.

The primary goals behind the development of Kotlin were :

1. Interoperability : Kotlin was created to seamlessly interoperate with existing Java code, libraries, and frameworks. It allows developers to call Java code from Kotlin and vice versa without any friction. This interoperability enables a smooth transition for developers and allows Kotlin to be adopted gradually in existing Java projects.

2. Safety : Kotlin aims to provide enhanced safety features compared to Java. It introduces null safety, where null references are explicitly handled at compile-time, reducing the occurrence of null pointer exceptions. The language also incorporates various other features to minimize common programming errors and increase overall code reliability.

3. Conciseness : Kotlin offers a more concise syntax compared to Java, resulting in reduced boilerplate code. It eliminates many of the verbose constructs present in Java, allowing developers to write more expressive and readable code with fewer lines.

4. Modern Language Features : Kotlin includes modern language features, such as support for functional programming concepts like higher-order functions, lambda expressions, and extension functions. These features enable developers to write more expressive and efficient code, leveraging functional programming paradigms.

5. Tooling and Community Support : JetBrains, the company behind Kotlin, provides excellent tooling support through its popular IDEs, such as IntelliJ IDEA. Kotlin has an active and growing community, which contributes to the language's ecosystem with libraries, frameworks, and resources.
Kotlin is designed to have seamless interoperability with Java, allowing developers to leverage existing Java code and libraries within Kotlin projects and vice versa. Here are some key aspects of how Kotlin and Java interoperate :

1. Calling Java Code from Kotlin :
   * Kotlin can directly call Java code without any additional configuration or wrappers. You can use Java classes, methods, and libraries in your Kotlin code as if they were Kotlin entities.
   * Kotlin provides null safety features that help prevent null pointer exceptions when calling nullable Java code from Kotlin. Kotlin handles nullability through type inference and the use of nullable and non-nullable types.

2. Calling Kotlin Code from Java :
   * Kotlin code can be called from Java in a similar manner as calling Java code. Kotlin classes, functions, and properties are visible to Java code without any special handling.
   * Kotlin's extension functions, which allow adding functions to existing classes, can be used seamlessly from Java code as if they were regular static utility methods.

3. Interoperability of Types :
   * Kotlin maps most Java types to their corresponding Kotlin counterparts. For example, Java's `String` is mapped to Kotlin's `String`, and Java's `List` is mapped to Kotlin's `List`.
   * Kotlin has its own nullability system, distinguishing nullable types from non-nullable types. Java types are treated as platform types in Kotlin, meaning the nullability is unknown. This allows Kotlin to handle nullability more safely, but developers must use appropriate null safety operators when working with Java types.

4. Java Annotations :
   * Kotlin fully supports Java annotations. You can use Java annotations in Kotlin code, define new annotations in Kotlin, and even write Kotlin code within Java annotations.

5. Java Interop Annotations :
   * Kotlin provides a set of annotations specifically for Java interoperability. These annotations allow fine-grained control over how Kotlin code is represented and accessed from Java, such as controlling the generated Java bytecode or specifying default values for properties.

6. Gradual Adoption :
   * Kotlin is designed to facilitate gradual adoption in existing Java projects. You can introduce Kotlin code into a Java codebase seamlessly, with both languages coexisting and interoperating without issues.
   * Kotlin and Java files can be mixed within the same project, and the IDEs provide support for converting Java code to Kotlin and vice versa.
3 .
What do you know about the history of Kotlin?
Kotlin was developed by the JetBrains team. This project was started in 2010 to develop a language for Android apps development, and officially its first version was released in February 2016. Kotlin was developed under the Apache 2.0 license.
Kotlin offers several key features that contribute to its popularity and make it a powerful programming language. Here are some of the most important key features of Kotlin :

* Concise Syntax : Kotlin has a concise and expressive syntax that reduces boilerplate code compared to Java. It eliminates many unnecessary ceremony and provides more readable and expressive code.

* Null Safety : Kotlin has built-in null safety features, which help prevent null pointer exceptions. It differentiates between nullable and non-nullable types, forcing developers to handle nullability explicitly. This reduces the occurrence of null-related bugs.

* Extension Functions : Kotlin allows the addition of extension functions to existing classes without modifying their source code. This feature enhances code reusability and allows developers to write more expressive and concise code by extending the functionality of classes from external libraries or APIs.

* Smart Casts : Kotlin incorporates smart casts, which eliminate the need for explicit type casting in certain scenarios. The compiler automatically casts a variable to a non-nullable type when it can guarantee that the variable is not null at a particular point in the code. This results in cleaner and safer code.

* Data Classes : Kotlin provides a concise way to define data classes. These classes automatically generate useful methods like `equals()`, `hashCode()`, `toString()`, and `copy()`. Data classes are ideal for holding data and are commonly used in Kotlin for modeling objects.
* Coroutines : Kotlin has built-in support for coroutines, which are lightweight concurrency primitives. Coroutines enable writing asynchronous code in a sequential and more readable manner, allowing developers to handle asynchronous operations without blocking the main thread.

* Higher-Order Functions : Kotlin supports higher-order functions, which are functions that can take functions as parameters or return functions as results. This functional programming feature enables the use of powerful functional programming techniques, such as passing behavior as an argument and composing functions.

* Interoperability with Java : Kotlin is fully interoperable with Java, allowing developers to call Java code from Kotlin and vice versa without any issues. This makes it easy to adopt Kotlin gradually in existing Java projects or utilize Java libraries and frameworks within Kotlin projects.

* Type Inference : Kotlin provides excellent type inference capabilities, allowing developers to omit type declarations in many cases. The compiler can infer the type based on the assigned value, reducing the need for explicit type annotations and making the code more concise.

* IDE Support : Kotlin is developed by JetBrains, the company behind popular IDEs like IntelliJ IDEA. As a result, Kotlin enjoys excellent tooling support and integration with IntelliJ IDEA, Android Studio, and other JetBrains IDEs. The IDEs provide features like auto-completion, error checking, refactoring tools, and easy conversion between Kotlin and Java code.
Compares string in Kotlin are possible in the following ways :

1. Using “==” operator :

You can use ah operator for comparison of two string. In Kotlin == operator is used.

2. Using compareTo() extension function

Syntax of compareTo() function is given below :
fun String.compareTo(
      other: String,
      ignoreCase: Boolean = false
): Int​

Another code example
fun main(args: Array & lt; String & gt;) {

    val x: String = "Kotlin is  simple"
    val y: String = "Kotlin language is" + " easy"
    if (x == y) {
          println(" x and y are similar.")
    } else {
          println(" x and y are not similar.")
    }
}​
Type inference is a feature in Kotlin that allows the compiler to automatically determine the type of a variable or expression based on its context and assigned value. In other words, the compiler can infer the type without the need for explicit type annotations.

When a variable is declared without specifying its type, the compiler analyzes the assigned value and infers the most specific type that matches the value. This reduces the need for developers to explicitly declare the type, making the code more concise and readable.

Here's an example to illustrate type inference in Kotlin :
val number = 42 // The type of 'number' is inferred as Int
val name = "John" // The type of 'name' is inferred as String
val pi = 3.14 // The type of 'pi' is inferred as Double
val flag = true // The type of 'flag' is inferred as Boolean​

In the above code snippet, the types of the variables (`number`, `name`, `pi`, and `flag`) are inferred based on the assigned values. The compiler analyzes the values (`42`, `"John"`, `3.14`, and `true`) and deduces the most appropriate type.

Type inference in Kotlin is smart and takes into account the available information and context. It works not only for variable declarations but also for function return types and other expressions.

It's important to note that while type inference is powerful and convenient, explicit type annotations can still be used when necessary or to improve code readability. Additionally, type inference may not always be possible if the assigned value is ambiguous or if the type cannot be determined from the context.
Nullable types in Kotlin allow variables to hold either a non-null value or a null value. By default, Kotlin enforces null safety, aiming to eliminate null pointer exceptions at compile-time. Nullable types are denoted by appending a question mark (`?`) to the type declaration.

In Kotlin, variables declared without the nullable modifier (`?`) are considered non-nullable, meaning they cannot hold a null value. This is different from Java, where any reference type can hold a null value by default. By making nullability explicit in Kotlin's type system, the compiler can enforce null safety and provide compile-time checks to prevent null-related errors.

Here's an example that demonstrates nullable types in Kotlin :
var name: String? = "John" // 'name' is nullable, it can hold a String or null
var age: Int = 25 // 'age' is non-nullable, it can only hold an Int

name = null // Assigning null to a nullable variable is allowed
age = null // Compilation error: Null cannot be a value of a non-null type Int​
In the above code snippet, the `name` variable is declared as nullable by appending `?` to the type declaration (`String?`). It can hold a String value or a null value. On the other hand, the `age` variable is declared as non-nullable (`Int`), and assigning null to it would result in a compilation error.

When working with nullable types, Kotlin provides several null-safe operators and functions to handle potential null values, such as the safe call operator (`?.`), the Elvis operator (`?:`), and the safe cast operator (`as?`). These constructs allow developers to safely access properties, call methods, or provide default values in scenarios where the value might be null.

By explicitly marking nullable types, Kotlin encourages developers to handle nullability explicitly, reducing the occurrence of null pointer exceptions and improving the overall reliability and safety of the code.

It's worth noting that Kotlin also offers platform types (`T!`), which are used when interoperating with Java code that has unknown nullability information. Platform types are treated as nullable in Kotlin and require careful handling to ensure null safety.
In Kotlin, `val` and `var` are used to declare variables, but they have different characteristics:

1. `val`:

* The `val` keyword is used to declare a read-only variable, meaning its value cannot be changed once it is assigned.

* It is similar to declaring a constant or an immutable variable.

* The variable assigned with `val` is effectively a final variable.

* The value assigned to a `val` variable must be determined at the time of declaration or in the constructor if it's a member variable.

Example :
       val pi = 3.14​


2. `var`:

* The `var` keyword is used to declare a mutable variable, meaning its value can be changed after it is assigned.

* It allows reassignment of values to the variable.

Example :
     var counter = 0
     counter += 1​
Key differences between `val` and `var`:

* Immutability : `val` variables are read-only and cannot be reassigned, while `var` variables are mutable and can be reassigned with new values.

* Usage : `val` is used when you need a variable whose value remains constant or unchangeable. It is similar to declaring a constant. `var` is used when you need a variable whose value can change over time.

* Intention : By using `val`, you communicate the intention that the value should not change, which can help improve code readability and prevent accidental modifications.

* Favoring `val`: It is generally recommended to use `val` whenever possible, as it promotes immutability and reduces the chances of introducing bugs due to mutable state. Immutable variables are often preferred in functional programming and concurrent programming paradigms.

In `val` is used for read-only variables, while `var` is used for mutable variables. The choice between them depends on whether you need the variable to hold a constant value or a value that can change.
Kotlin introduces null safety features to address one of the common sources of errors in programming: null pointer exceptions. It enforces null safety at compile-time, reducing the likelihood of encountering null-related errors during runtime. Here's how Kotlin handles null safety :

1. Nullable Types : In Kotlin, the type system differentiates between nullable and non-nullable types. By default, variables in Kotlin are non-nullable, meaning they cannot hold a null value. However, nullable types can be explicitly declared by appending a question mark (`?`) to the type declaration. Nullable types allow variables to hold either a non-null value or a null value.

2. Safe Calls (`?.`) : The safe call operator (`?.`) is used to safely access properties or call methods on nullable objects. If an object reference is null, the safe call operator returns null instead of throwing a null pointer exception. It provides a concise and safe way to handle nullable objects.

3. Elvis Operator (`?:`) : The Elvis operator (`?:`) is used to provide a default value when a nullable expression evaluates to null. It allows developers to specify an alternative value to use if a nullable expression is null.
4. Non-Null Assertion (`!!`) : The non-null assertion operator (`!!`) is used to assert that an expression is non-null. It converts a nullable type to a non-nullable type explicitly. If the expression evaluates to null, a `NullPointerException` will be thrown. It should be used with caution and only when the developer is certain that the value is not null.

5. Type System and Smart Casts : Kotlin's type system performs smart casts based on nullability checks. When the compiler can determine that a nullable variable is not null at a certain point in the code, it automatically casts it to a non-nullable type. This eliminates the need for explicit type casting and allows for safer handling of nullable types.

6. Nullability in Function Signatures : Kotlin's type system incorporates nullability information in function signatures. It allows developers to specify whether a function parameter or return type is nullable or non-nullable. This provides clarity and helps ensure proper handling of nullable values in function calls.

7. Null Safety and Java Interoperability : Kotlin provides seamless interoperability with Java, which has no built-in null safety features. Kotlin treats Java types as platform types, indicating that their nullability is unknown. This ensures that null safety checks are enforced when calling Java code from Kotlin and vice versa.
The Elvis operator (`?:`) is a useful operator in Kotlin that provides a concise way to handle nullability and provide default values when dealing with nullable expressions. It allows you to specify an alternative value to be used when a nullable expression evaluates to null.

The syntax of the Elvis operator is as follows:
expression ?: defaultValue​

Here's how the Elvis operator works :

* If the expression on the left side of the `?:` operator is not null, the value of that expression is returned.
* If the expression on the left side is null, the value of the expression on the right side (the defaultValue) is returned instead.

The Elvis operator is particularly handy when assigning values to variables or providing default values in situations where a nullable expression might result in null. It simplifies the code by handling nullability and providing a fallback value in a single line.

Here's an example that demonstrates the usage of the Elvis operator :
val nullableValue: String? = getNullableValue()
val nonNullValue: String = nullableValue ?: "Default Value"

// Using the Elvis operator in an assignment
val result: Int = computeResult() ?: throw IllegalStateException("Result is null")​
In the above code snippet, `nullableValue` is a nullable String that may or may not be null. The Elvis operator is used to assign the value of `nullableValue` to `nonNullValue`. If `nullableValue` is not null, its value is assigned to `nonNullValue`. Otherwise, the default value `"Default Value"` is assigned.

In the second example, the Elvis operator is used in an assignment where `computeResult()` returns a nullable `Int`. If the result is not null, it is assigned to `result`. However, if the result is null, an `IllegalStateException` is thrown.

The Elvis operator simplifies null handling and allows for concise and expressive code when dealing with nullable values and providing default values or fallback behavior.
Kotlin provides a rich set of data types that can be used to represent different kinds of values. Here are the various data types available in Kotlin:

1. Numbers :
   * `Byte`: 8-bit signed integer (-128 to 127).
   * `Short`: 16-bit signed integer (-32,768 to 32,767).
   * `Int`: 32-bit signed integer (-2,147,483,648 to 2,147,483,647).
   * `Long`: 64-bit signed integer (-9,223,372,036,854,775,808 to 9,223,372,036,854,775,807).
   * `Float`: 32-bit floating-point number.
   * `Double`: 64-bit floating-point number.

2. Characters :
   * `Char`: Represents a single Unicode character.

3. Booleans :
   * `Boolean`: Represents either `true` or `false`.

4. Strings :
   * `String`: Represents a sequence of characters.

5. Arrays :
   * `Array`: Represents a fixed-size collection of elements of the same type.
   * Kotlin also provides specialized array types like `IntArray`, `CharArray`, etc., for arrays of specific primitive types.

6. Collections :
   * Kotlin standard library provides a rich set of collection types such as `List`, `Set`, and `Map` for working with collections of elements.

7. Ranges :
   * Kotlin allows the creation of ranges using the range operator (`..`) to represent a sequence of values between a start and end point.

8. Nullability :
   * Kotlin supports nullable types by allowing a data type to hold either a non-null value or a null value. Nullable types are denoted by appending `?` to the type declaration.

9. User-defined Types :
   * Kotlin allows the creation of custom data types using classes, interfaces, enums, and sealed classes.

Additionally, Kotlin provides type inference, which allows the compiler to automatically determine the data type based on the assigned value, reducing the need for explicit type declarations in many cases.
The Kotlin programing language seems to be simpler and cleaner than Java. It removes a lot of redundancies in code as compared to Java. Kotlin also offers some useful features that Java doesn't yet support, making the code more idiomatic. Kotlin has been added to Android Studio's list of supported languages recently. So, there is much to expect from Kotlin in easing out the development efforts and good support in the future.
Kotlin is very much similar to the Java programming language. Like Java, the Kotlin code is also compiled into the Java bytecode and executed at runtime by the Java Virtual Machine, i.e., JVM. For example, when a Kotlin file named Main.kt is compiled, it will eventually turn into a class, and then the bytecode of the class will be generated. The name of the bytecode file will be MainKt.class, and this file will be executed by the JVM.
In Kotlin, data classes are a special type of class that are primarily used to hold and represent data. They provide a concise way to declare classes whose main purpose is to store data and provide commonly needed functionalities. Here are the key characteristics and differences of data classes compared to regular classes:

1. Default Functionality :
   * Data classes automatically provide implementations for common functions such as `equals()`, `hashCode()`, `toString()`, and `copy()`. These functions are generated by the compiler based on the properties defined in the primary constructor of the data class.
   * This default functionality saves developers from writing boilerplate code for these commonly used operations.

2. Property Declaration :
   * Data classes allow property declaration directly in the primary constructor.
   * The properties defined in the primary constructor are automatically treated as immutable (val) properties.
  
    Example :
     data class Person(val name: String, val age: Int)​
3. Comparison and Copying :
   * Data classes provide an automatic implementation of the `equals()` function, which compares the values of properties for equality.
   * The `hashCode()` function is also generated based on the properties of the data class, ensuring that equal objects have the same hash code.
   * The `copy()` function is generated, allowing you to create a copy of an existing data class instance with the option to modify some properties.

4. Component Functions :
   * Data classes generate component functions (`component1()`, `component2()`, etc.) that allow destructuring declarations to extract properties easily.
   * This enables syntax like `val (name, age) = person` to extract values into individual variables.

5. Inheritance and Interfaces :
   * Data classes can inherit from other classes and implement interfaces, just like regular classes.
   * However, if a data class explicitly defines any of the functions (`equals()`, `hashCode()`, or `toString()`) or uses `copy()` with named arguments, the compiler won't generate them automatically.

6. Customization :
   * Developers can override the generated functions with custom implementations if needed.
   * Custom properties and functions can be added to data classes as well.
The `lateinit` modifier in Kotlin is used to indicate that a non-null property will be initialized at a later point before it is accessed. It is primarily used with mutable properties that cannot be initialized during object construction or immediately upon declaration.

Here's how `lateinit` works :

1. Declaration : The `lateinit` modifier is applied to a `var` property declaration, indicating that it will be initialized later.
   lateinit var name: String​

2. Initialization : The `lateinit` property must be assigned a value before it is accessed, using the assignment operator (`=`).
   name = "John"​

3. Usage: Once the `lateinit` property is initialized, it can be used like any other property.
   println(name)​
It's important to note the following characteristics and considerations when using `lateinit`:

* `lateinit` can only be applied to `var` properties (mutable properties), as it allows the property value to be assigned or changed after initialization.

* `lateinit` can only be used with non-null types, as it implies that the property will eventually be assigned a non-null value.

*  Accessing a `lateinit` property before it is initialized will result in a `lateinit property has not been initialized` exception.

* `lateinit` properties are not thread-safe. If multiple threads can access and modify the property concurrently, appropriate synchronization mechanisms should be used.

* The use of `lateinit` should be limited to cases where the property cannot be assigned a value during object construction and where it is guaranteed to be initialized before accessing it.

* `lateinit` is commonly used in frameworks like dependency injection or when working with certain lifecycle callbacks, where properties need to be initialized by an external entity.
In Kotlin, the `init` block is used to initialize the properties or execute code during the initialization phase of an object. It is a special block that is part of a class and is executed when an instance of that class is created. The `init` block is commonly used to perform initialization logic that cannot be achieved through direct property initialization or constructor parameters.

Here's how the `init` block works :

1. Syntax : The `init` block is defined within the body of a class and does not have any explicit keyword. It is denoted by the `init` keyword followed by a code block enclosed in curly braces.
   class MyClass {
       // Properties
       // ...

       // Init block
       init {
           // Initialization code
           // ...
       }
   }​

2. Execution : When an object of the class is created, the `init` block is executed before the constructor body. It allows you to perform any required initialization tasks or computations.

3. Access to Properties : Inside the `init` block, you can access and manipulate the properties of the class, as well as call other functions or perform any necessary operations.
4. Multiple `init` Blocks : A class can have multiple `init` blocks, and they are executed in the order they appear in the class body. This allows you to organize and separate different initialization tasks.

Here's an example that demonstrates the usage of `init` blocks :
class Person(firstName: String, lastName: String) {
    val fullName: String

    init {
        fullName = "$firstName $lastName"
        println("Person initialized")
    }

    init {
        println("Additional initialization logic")
        // Additional initialization code
    }
}

fun main() {
    val person = Person("John", "Doe")
    println(person.fullName)
}​

In the above example, the `Person` class has two `init` blocks. The first `init` block initializes the `fullName` property by concatenating the `firstName` and `lastName` parameters. The second `init` block demonstrates additional initialization logic. When an instance of `Person` is created, both `init` blocks are executed in the order they are defined.

The `init` block is useful for performing complex initialization tasks, initializing properties based on constructor parameters, or executing other required initialization logic before using an object. It provides a way to consolidate initialization code within the class and ensures that it is executed whenever an object is created.
We can create a singleton in Kotlin by using an object.

Syntax :
object SomeSingleton  ​
The above Kotlin object will be compiled to the following equivalent Java code:
public final class SomeSingleton {  
   public static final SomeSingleton INSTANCE;  
   private SomeSingleton() {  
      INSTANCE = (SomeSingleton)this;  
      System.out.println("init complete");  
   }  
   static {  
      new SomeSingleton();  
   }  
}  ​
The above way is preferred to implement singletons on a JVM because it enables thread-safe lazy initialization without relying on a locking algorithm like the complex double-checked locking.
In Kotlin, the primary constructor is a part of the class header. Unlike Java, it doesn't need you to declare a constructor in the body of the class.

Kotlin facilitates you to declare the constructor in the class header itself:

See the following example :
class Person constructor(name: String, age: Int, salary: Int) {  
} ​
Just like functions or methods, it takes a series of parameters with their type. These parameters initialize the variables present in the class.

If you do not have any annotations or modifiers (public, private, protected), you can omit the constructor keyword like the following example.
class Person (name: String, age: Int, salary: Int) {  
}  ​
By removing the constructor keyword, you can get code that is simplified and easy to understand.
In Kotlin, a companion object is a special object that is tied to a specific class. It allows you to define properties and functions that can be accessed directly on the class itself, without the need for an instance of that class. The companion object is similar to static members in other programming languages, but with additional flexibility and capabilities.

Here are some key points about companion objects :

1. Declaration : The companion object is declared within the body of a class using the `companion` keyword.
   class MyClass {
       // ...

       companion object {
           // Properties and functions
           // ...
       }
   }​

2. Accessing Members : Members of the companion object can be accessed directly on the class name, similar to accessing static members in other languages.
   MyClass.myFunction()
   MyClass.myProperty​

3. Naming : By default, the name of the companion object is "Companion". However, you can provide a custom name by explicitly declaring the companion object.
   class MyClass {
       // ...

       companion object MyCompanion {
           // Properties and functions
           // ...
       }
   }​

4. Behavior : The companion object can access private members of the class, including private constructors, and can be used to encapsulate utility functions, constants, factory methods, or other shared functionality that relates to the class.

5. Implementing Interfaces : The companion object can also implement interfaces, allowing it to provide a common interface for accessing shared functionality.
6. Extension Functions : You can define extension functions on the companion object, which can be called directly on the class name, similar to static utility functions.

Here's an example to illustrate the usage of companion objects :
class MyClass {
    companion object {
        const val CONSTANT_VALUE = 42

        fun myFunction() {
            println("Hello from myFunction()")
        }
    }
}

fun main() {
    println(MyClass.CONSTANT_VALUE)
    MyClass.myFunction()
}​

In the above example, the companion object of the `MyClass` class contains a constant value (`CONSTANT_VALUE`) and a function (`myFunction()`). These members can be accessed directly on the class name without creating an instance of `MyClass`. The output of the program will be:
42
Hello from myFunction()​
Companion objects provide a way to define class-level members and share functionality across instances of a class. They are useful for encapsulating common functionality and constants, without the need for an explicit instance.
In Kotlin, there are three types of constructors that can be used :

1. Primary Constructor :
   * The primary constructor is defined as part of the class header and is declared after the class name.
   * It can include parameters that serve as properties of the class.
   * The primary constructor cannot contain any code or initialization logic. Initialization code can be placed in `init` blocks.
   Example :
     class MyClass(val name: String, val age: Int) {
         // Initialization code can go in init blocks
         init {
             // ...
         }
     }​


2. Secondary Constructors :
   * Secondary constructors are defined inside the class body.
   * They are prefixed with the `constructor` keyword.
   * Secondary constructors provide additional ways to instantiate objects by providing different sets of parameters or customization options.
   * Secondary constructors must delegate to the primary constructor using the `this` keyword or to another secondary constructor using the `this` keyword followed by the appropriate constructor call.
   Example :
     class MyClass {
         constructor(name: String) {
             // Secondary constructor logic
         }

         constructor(name: String, age: Int) : this(name) {
             // Secondary constructor logic
         }
     }​
3. Init Blocks :
   * Although not technically constructors, `init` blocks are used to initialize properties or execute code during the initialization phase of an object.
   * They are executed when an instance of the class is created, after the primary constructor is called.
   * A class can have multiple `init` blocks, which are executed in the order they appear in the class body.
   Example :
     class MyClass(val name: String, val age: Int) {
         init {
             // Initialization code
         }

         init {
             // Additional initialization code
         }
     }​

It's important to note that the primary constructor is the main constructor of the class and is called when an instance of the class is created. Secondary constructors provide additional options for object creation with different parameter sets, but they must ultimately delegate to the primary constructor. Init blocks are used to execute initialization code during the creation of an object.

These constructor types in Kotlin provide flexibility in defining class properties, customizing object creation, and performing initialization tasks.
Extension functions in Kotlin allow you to add new functions to existing classes, including classes defined in external libraries, without modifying their source code. They provide a way to extend the functionality of classes without the need for inheritance or modifying their internal implementation. Extension functions enhance the expressiveness and readability of code by enabling the addition of new behavior to existing types.

Here are some key points about extension functions :

1. Syntax : Extension functions are defined outside of the class they extend, using the following syntax:
   fun ClassName.functionName(parameters): ReturnType {
       // Function implementation
   }​

2. Usage : Once an extension function is defined, it can be invoked directly on objects of the extended class, as if it were a member function of that class.
   val result = myObject.functionName()​

3. Scope : Extension functions are available within the scope where they are imported or defined. By default, they are not visible outside their containing file or package. However, you can import them explicitly to make them accessible in other files or packages.

4. Access to Properties : Inside an extension function, you can access the public properties and methods of the class being extended, just like in regular member functions.

5. Function Overloading : You can define multiple extension functions with the same name but different parameter signatures. The appropriate function is resolved based on the parameter types at the call site.
6. Nullability : Extension functions can be defined for nullable types, allowing them to be called on nullable objects. The null-safety rules apply to extension functions as well.

Here's a simple example to illustrate the usage of extension functions :
fun String.removeWhitespace(): String {
    return this.replace(" ", "")
}

fun Int.isEven(): Boolean {
    return this % 2 == 0
}

fun main() {
    val text = "Hello, World!"
    val modifiedText = text.removeWhitespace()
    println(modifiedText) // Output: Hello,World!

    val number = 7
    val isNumberEven = number.isEven()
    println(isNumberEven) // Output: false
}​
In the above example, two extension functions are defined. The `removeWhitespace()` extension function extends the `String` class and removes whitespace characters from a string. The `isEven()` extension function extends the `Int` class and checks whether a number is even. These extension functions can be invoked directly on objects of their respective types, as shown in the `main()` function.

Extension functions are a powerful feature in Kotlin that promote code reusability, readability, and maintainability. They allow you to add functionality to existing classes without modifying their source code, making it easier to work with classes from external libraries or enhance the capabilities of standard classes.
In Kotlin, a lambda expression is a concise way to define anonymous functions, also known as function literals. It allows you to create small, inline functions without explicitly declaring a function name. Lambda expressions are particularly useful when working with higher-order functions, functional programming, or when you need to pass behavior as an argument.

Here are some key points about lambda expressions :

1. Syntax : The syntax of a lambda expression consists of a parameter list, followed by the arrow (`->`), and then the body of the function.
   val lambdaName: (ParameterType) -> ReturnType = { parameter ->
       // Function body
       // ...
   }​

2. Parameter List : The parameter list defines the input parameters for the lambda expression. It can be empty or contain one or more parameters. If there is only one parameter, you can omit the parameter list and refer to it as `it` within the lambda body.
   val double: (Int) -> Int = { number ->
       number * 2
   }

   val square: (Int) -> Int = { it * it }​

3. Arrow (`->`) : The arrow separates the parameter list from the body of the lambda expression. It indicates the start of the function body.
4. Function Body : The function body contains the code that is executed when the lambda expression is invoked. It can be a single expression or a block of multiple statements. If it's a block, the last expression is treated as the return value.
   val sum: (Int, Int) -> Int = { a, b ->
       val result = a + b
       result // Last expression is the return value
   }

   val greet: () -> Unit = {
       println("Hello, World!")
   }​

5. Type Inference : In many cases, Kotlin can infer the types of lambda parameters and return values, so you don't need to explicitly declare them. However, you can provide explicit type annotations when necessary.

6. Lambda as Arguments : Lambda expressions are often used as arguments for higher-order functions, which are functions that take other functions as parameters. This enables powerful functional programming constructs like map, filter, reduce, etc.
   val numbers = listOf(1, 2, 3, 4, 5)

   val doubled = numbers.map { it * 2 }

   val evenNumbers = numbers.filter { it % 2 == 0 }

   val sum = numbers.reduce { acc, number -> acc + number }​
Lambda expressions provide a concise and flexible way to define anonymous functions in Kotlin. They are commonly used in functional programming, for defining behavior inline, and as arguments for higher-order functions. Lambda expressions make code more expressive and allow for more concise and readable functional-style programming constructs.
In Kotlin, the primary constructor and secondary constructors serve different purposes and have distinct characteristics. Here are the key differences between the primary constructor and secondary constructors:

1. Declaration :
   * Primary Constructor : The primary constructor is declared as part of the class header, directly after the class name. It can include parameters that serve as properties of the class.
   * Secondary Constructors : Secondary constructors are defined inside the class body using the `constructor` keyword. They provide additional ways to instantiate objects with different parameter sets.

2. Number :
   * Primary Constructor : A class can have only one primary constructor, which is the main constructor of the class.
   * Secondary Constructors : A class can have multiple secondary constructors. They are additional constructors that supplement the primary constructor.

3. Delegation :
   * Primary Constructor : The primary constructor can delegate to another constructor of the same class using the `this` keyword. It allows parameter values to be passed from the primary constructor to secondary constructors or between secondary constructors.
   * Secondary Constructors : Secondary constructors must delegate to the primary constructor or to another secondary constructor in the same class using the `this` keyword. Delegation to the primary constructor is done using the `this` keyword followed by the appropriate constructor call.

4. Initialization :
   * Primary Constructor : The primary constructor can include property declarations directly in its parameter list. It automatically initializes the properties with the provided values.
   * Secondary Constructors : Secondary constructors cannot declare properties directly. They can only perform additional initialization logic but cannot directly initialize properties.
5. Execution :
   * Primary Constructor : The primary constructor is called when an instance of the class is created. It is the entry point for initializing the class and executing any initialization code or `init` blocks.
   * Secondary Constructors : Secondary constructors are called explicitly using the `constructor` keyword. They are invoked when an object is created using the secondary constructor, providing an alternative way to instantiate objects.

6. Code Organization :
   * Primary Constructor : Initialization code and logic related to property initialization are typically placed in `init` blocks associated with the primary constructor. The primary constructor itself does not contain executable code.
   * Secondary Constructors : Secondary constructors can contain executable code directly within their body, allowing for additional initialization or custom logic.

Here's an example that demonstrates the usage of primary and secondary constructors:
class Person(val name: String, val age: Int) {
    // Primary constructor with property declarations

    constructor(name: String) : this(name, 0) {
        // Secondary constructor delegating to the primary constructor with age set to 0
    }

    constructor() : this("", 0) {
        // Secondary constructor delegating to the primary constructor with empty name and age set to 0
    }
}​

In the above example, the `Person` class has a primary constructor with `name` and `age` as parameters. It also has two secondary constructors that delegate to the primary constructor, providing default values for `age` when only the `name` is provided or when no parameters are given.
In Kotlin, safe calls (`?.`) and null checks (`!!`) are two different approaches to handle null values and avoid NullPointerExceptions. Here's the difference between them:

1. Safe Calls (`?.`) :
   * Safe calls allow you to safely access properties or call methods on a nullable object.
   * If the object is not null, the property or method is accessed normally.
   * If the object is null, the expression is short-circuited, and the result of the entire expression is null.
   * Safe calls ensure that the code does not throw a NullPointerException.
   Example:
     val length: Int? = text?.length​

2. Null Checks (`!!`) :
   * Null checks assert that an expression is not null.
   * If the expression is not null, it is accessed or used as usual.
   * If the expression is null, a NullPointerException is thrown immediately.
   * Null checks are explicit assertions that you are guaranteeing that the expression is not null.
   Example:
     val length: Int = text!!.length​
Here are some important points to consider when using these operators :

* Safe calls (`?.`) provide a safer approach to accessing properties and methods on nullable objects. They allow you to handle null values gracefully and avoid NullPointerExceptions by returning null instead.

* Null checks (`!!`) are more risky because they explicitly assert that an expression is not null. If the expression is actually null, a NullPointerException will be thrown, potentially causing a program crash.

* Safe calls (`?.`) are typically used when you expect the possibility of a null value and want to handle it gracefully by providing an alternative behavior or a default value.

* Null checks (`!!`) are used when you are certain that an object reference is not null and want to enforce it, bypassing the nullability checks of the compiler. They should be used with caution and only when you are confident that the expression cannot be null.

It's generally recommended to use safe calls (`?.`) and handle null values gracefully by providing appropriate fallback or alternative behaviors. Null checks (`!!`) should be used sparingly and only when you have explicit knowledge that an expression cannot be null.
In Kotlin, a higher-order function is a function that can take one or more functions as parameters and/or return a function as its result. It treats functions as first-class citizens, allowing them to be assigned to variables, passed as arguments, and returned from other functions.

Here are some key points about higher-order functions :

1. Functions as Parameters :
   * Higher-order functions can receive other functions as parameters. These functions are typically referred to as "function types" or "lambda expressions" and can be invoked within the higher-order function.
   * Function parameters can specify the expected function type using a syntax similar to `(parameterName: FunctionType)`.
   Example :
     fun performOperation(operation: (Int, Int) -> Int) {
         val result = operation(5, 3)
         println("Result: $result")
     }​

2. Functions as Return Types :
   * Higher-order functions can return functions as their results. The returned function can be invoked later, outside the scope of the higher-order function.
   * The return type of the higher-order function includes the function type that will be returned.
   Example :
     fun getOperation(): (Int, Int) -> Int {
         return { a, b -> a + b }
     }
​
3. Lambda Expressions and Function References :
   * Higher-order functions often work with lambda expressions or function references. Lambda expressions are a concise way to define anonymous functions, while function references refer to existing named functions.
   * Lambda expressions can be used inline when calling higher-order functions, while function references provide a way to pass named functions as arguments.
   Example :
     val multiply: (Int, Int) -> Int = { a, b -> a * b }

     performOperation { a, b -> a + b }
     performOperation(multiply)​

4. Function Composition and Transformation :
   * Higher-order functions enable powerful functional programming techniques such as function composition and transformation of functions.
   * Function composition allows you to combine multiple functions into a single function, where the output of one function is the input of the next.
   * Function transformation involves modifying the behavior of a function by wrapping it in another function.
   Example :
     fun compose(f: (Int) -> Int, g: (Int) -> Int): (Int) -> Int {
         return { x -> f(g(x)) }
     }​

Higher-order functions in Kotlin enable functional programming paradigms and allow for more expressive and flexible code. They promote code reuse, modularity, and the separation of concerns. Higher-order functions, combined with lambda expressions or function references, open up possibilities for concise and powerful programming constructs.
In Kotlin, the `run` function is a scope function that is used to execute a block of code on an object within its context. It provides a concise way to perform operations on an object and access its properties and functions without the need for repetitive object references.

Here are the key features and purposes of the `run` function :

1. Object Context :
   * The `run` function is invoked on an object, and the object becomes the context within which the block of code is executed.
   * Inside the block, you can directly access the properties and functions of the object without explicitly referencing the object name.
   Example :
     val person = Person("John", 25)
     
     val description = person.run {
         "Name: $name, Age: $age"
     }
​

2. Return Value :
   * The return value of the `run` function is the result of the last expression within the block.
   * You can use this feature to perform computations or transformations on the object and return the desired result.
   Example :
     val person = Person("John", 25)
     
     val modifiedPerson = person.run {
         age += 1
         this
     }​
3. Nullable Context :
   * When used with nullable objects, the `run` function allows you to perform operations on the object only if it is not null.
   * If the object is null, the block of code is not executed, and the result of the `run` function is null.
   * This can be helpful in handling nullability gracefully and avoiding explicit null checks.
   Example :
     val person: Person? = ...
     
     val description = person?.run {
         "Name: $name, Age: $age"
     }​

4. Chaining and Scoping :
   * The `run` function can be used in conjunction with other scope functions like `let`, `apply`, `also`, and `with` to chain operations and further customize the behavior.
   * By combining different scope functions, you can achieve different effects and make your code more concise and readable.
   Example :
     val person = Person("John", 25)
     
     val result = person.let { p ->
         p.age += 1
         p.name
     }.run {
         "Modified name: $this"
     }​

The `run` function provides a convenient way to work with objects and execute a block of code within their context. It helps eliminate repetitive object references, enables concise transformations, and allows for nullable context handling. It is particularly useful when you want to perform operations on an object and obtain a result without introducing additional variables or breaking the flow of the code.
In Kotlin, `let`, `run`, `with`, `apply`, and `also` are scope functions that provide a concise way to work with objects and perform operations within a specific context. Although they have similarities, each scope function has its distinct characteristics and intended use cases. Here's a breakdown of the differences between these scope functions:

1. `let`:
   * Use `let` to execute a block of code on a non-null object and access it as a parameter (`it`).
   * It is typically used for null-checking and performing transformations on the object.
   * The return value is the result of the last expression within the block.
   Example :
     val nullableValue: String? = ...
     val length = nullableValue?.let { value ->
         // Safe access to 'value'
         value.length
     } ?: 0​

2. `run` :
   * Use `run` to execute a block of code on an object and access its properties and functions without repetition.
   * It is useful for performing operations on an object within its context.
   * The return value is the result of the last expression within the block.
   Example :
     val person = Person("John", 25)
     val description = person.run {
         // Access properties without repetition
         "Name: $name, Age: $age"
     }​

3. `with`:
   * Use `with` to execute a block of code on an object, similar to `run`, but it does not provide an explicit receiver.
   * It is typically used to work with properties or functions of an object without repetition.
   * The return value is the result of the last expression within the block.
   Example :
     val person = Person("John", 25)
     val description = with(person) {
         // Access properties without repetition
         "Name: $name, Age: $age"
     }​
4. `apply` :
   * Use `apply` to configure properties and perform initialization on an object.
   * It returns the object itself, allowing method chaining.
   * It is often used for configuring properties of an object in a builder-style pattern.
   Example :
     val person = Person().apply {
         name = "John"
         age = 25
     }​

5. `also` :
   * Use `also` to perform additional actions on an object, similar to `apply`, but it does not affect the object's properties.
   * It is useful for side effects, such as logging or additional processing, while preserving the original object.
   * The return value is the object itself.
   Example :
     val person = Person("John", 25)
     val modifiedPerson = person.also {
         // Perform additional actions without modifying the object
         log("Person: $it")
     }​

In summary, each scope function in Kotlin has its own purpose and usage scenario:

* `let`: Safely accesses properties of a non-null object and performs transformations.
* `run`: Executes code on an object within its context, accessing properties and functions.
* `with`: Executes code on an object without an explicit receiver, reducing repetition.
* `apply`: Configures properties and performs initialization on an object, allowing method chaining.
* `also`: Performs additional actions on an object, such as logging or side effects, while preserving the object.

By leveraging these scope functions, you can write more expressive and concise code, handle nullability gracefully, and work efficiently with objects in Kotlin.
In java to store data items, we create a class by setting the variables. The setters and gathers are override to hash(), tostring(), and copy() functions.

Whereas in kotlin, we need to add just the keyword data to the class, and everything that remained is done automatically by default.
data class company(var  name: string)

Function main(args  array<string>)
{
Val company= company("Manhas Industries")
}​
In Kotlin, the traditional `switch` statement from languages like Java is replaced with the more powerful and flexible `when` expression. The `when` expression allows you to perform conditional branching based on the value of an expression. It offers a more concise and versatile syntax compared to the `switch` statement.

Here's how you can use `when` as a replacement for `switch`:

1. Simple `when` Expression :
   when (variable) {
       value1 -> {
           // Code block for value1
       }
       value2 -> {
           // Code block for value2
       }
       else -> {
           // Default code block
       }
   }​

2. Multiple Values :
   when (variable) {
       value1, value2 -> {
           // Code block for value1 or value2
       }
       else -> {
           // Default code block
       }
   }​

3. Range Checks :
   when (variable) {
       in range1 -> {
           // Code block for values within range1
       }
       in range2 -> {
           // Code block for values within range2
       }
       else -> {
           // Default code block
       }
   }
4. Checking Type :
   when (variable) {
       is Type1 -> {
           // Code block for Type1
       }
       is Type2 -> {
           // Code block for Type2
       }
       else -> {
           // Default code block
       }
   }​

5. Function Calls and Expressions :
   when {
       condition1() -> {
           // Code block if condition1() is true
       }
       condition2() -> {
           // Code block if condition2() is true
       }
       else -> {
           // Default code block
       }
   }​

The `when` expression is more flexible than the traditional `switch` statement as it allows for complex conditions, type checks, range checks, and multiple values. It eliminates the need for `break` statements and supports more expressive and readable code. Additionally, the `when` expression can be used as an expression itself, allowing it to be assigned to a variable or used directly in function return statements.

The `when` expression in Kotlin serves as a powerful replacement for the `switch` statement, providing enhanced functionality and improved readability.
Formal inheritance structure does not compile in the kotlin. By using an open modifier we can finalize classes.
open  class B
{
}
class  c = B()
{
} ​
The Ranges operator helps to iterate a loop with range.

Example :
for(i  1...15)

print (i)​
Basis Kotlin Java
Null Safety By default, all sorts of variables in Kotlin are non-nullable (that is, we can't assign null values to any variables or objects). Kotlin code will fail to build if we try to assign or return null values. If we absolutely want a null value for a variable, we can declare it as follows: value num: Int? = null  NullPointerExceptions are a big source of annoyance for Java developers. Users can assign null to any variable, however, when accessing an object reference with a null value, a null pointer exception is thrown, which the user must manage.
Coroutines Support  We can perform long-running expensive tasks in several threads in Kotlin, but we also have coroutines support, which halt execution at a given moment without blocking threads while doing long-running demanding operations. The corresponding thread in Java will be blocked anytime we launch a long-running network I/0 or CPU-intensive task. Android is a single-threaded operating system by default. Java allows you to create and execute numerous threads in the background, but managing them is a difficult operation.
Data Classes  If we need to have data-holding classes in Kotlin, we may define a class with the keyword "data" in the class declaration, and the compiler will take care of everything, including constructing constructors, getter, and setter methods for various fields. Let's say we need a class in Java that only holds data and nothing else. Constructors, variables to store data, getter and setter methods, hashcode(), function toString(), and equals() functions are all required to be written explicitly by the developer.
Functional Programming Kotlin is procedural and functional programming (a programming paradigm where we aim to bind everything in functional units) language that has numerous useful features such as lambda expressions, operator overloading, higher-order functions, and lazy evaluation, among others. Java does not allow functional programming until Java 8, however it does support a subset of Java 8 features when developing Android apps.
Extension Functions Kotlin gives developers the ability to add new functionality to an existing class. By prefixing the name of a class to the name of the new function, we can build extended functions. In Java, we must create a new class and inherit the parent class if we want to enhance the functionality of an existing class. As a result, Java does not have any extension functions.
Data Type Inference  We don't have to declare the type of each variable based on the assignment it will handle in Kotlin. We can specify explicitly if we want to. When declaring variables in Java, we must declare the type of each variable explicitly.
Smart Casting Smart casts in Kotlin will take care of these casting checks with the keyword "is-checks," which checks for immutable values and conducts implicit casting. We must examine the type of variables in Java and cast them appropriately for our operation.
Checked Exceptions We don't have checked exceptions in Kotlin. As a result, developers do not need to declare or catch exceptions, which has both benefits and drawbacks. We have checked exceptions support in Java, which enables developers to declare and catch exceptions, resulting in more robust code with better error handling.
Difference between the fold and reduce in Kotlin :

Fold : The fold takes an initial value and the first invocation of the lambda you pass to it. It will receive that initial value and the first element of the collection as parameters.
listOf(1, 2, 3).fold(0) { sum, element -> sum + element }  ​
The first call to the lambda will be with parameters 0 and 1. The ability to pass in an initial value is useful if you have to provide a default value or parameter for your operation.

Reduce : The "reduce" doesn't take an initial value. Instead, it starts with the first element of the collection as the accumulator.
listOf(1, 2, 3).reduce { sum, element -> sum + element }  ​
In the above example, it is denoted by sum. The first call to the lambda here will be with parameters 1 and 2.
In Kotlin, we can assign null values to a variable using the null safety property. To check if a value has null value, we can use if-else or can use the Elvis operator i.e. ?:

For example :
var name:String? = "Mindorks"  
val namenameLength = name?.length ?: -1  
println(nameLength)  ​

In the above example, the Elvis operator(?:) we are using will return the length of the name if the value is not null; otherwise, if the value is null, then it will return -1.
In Kotlin, `listOf`, `mutableListOf`, and `arrayListOf` are functions used to create different types of lists.

Here's the difference between them :

1. `listOf` :
   * The `listOf` function creates an immutable list (read-only) in Kotlin.
   * The elements of the list cannot be modified once the list is created.
   * It returns an instance of `List<T>`, where `T` is the type of the elements in the list.
   Example:
     val immutableList = listOf("apple", "banana", "orange")​

2. `mutableListOf` :
   * The `mutableListOf` function creates a mutable list in Kotlin.
   * The elements of the list can be modified (added, removed, or modified) after the list is created.
   * It returns an instance of `MutableList<T>`, where `T` is the type of the elements in the list.
   Example :
     val mutableList = mutableListOf("apple", "banana", "orange")
     mutableList.add("grape")
     mutableList.removeAt(0)​

3. `arrayListOf` :
   * The `arrayListOf` function is similar to `mutableListOf` and creates a mutable list in Kotlin.
   * It returns an instance of `ArrayList<T>`, which is a specific implementation of `MutableList<T>`.
   * The elements of the list can be modified, and `ArrayList` provides additional functionality and performance optimizations compared to a regular mutable list.
   Example :
     val arrayList = arrayListOf("apple", "banana", "orange")
     arrayList.add("grape")
     arrayList.removeAt(0)​
In Kotlin coroutines, `launch` and `async` are both functions used to initiate concurrent execution of code. However, they have distinct differences in terms of their behavior and return values. Here's an explanation of the difference between `launch` and `async`:

1. `launch` :
   * The `launch` function is used to launch a new coroutine without expecting any result.
   * It is suitable for tasks where the focus is on concurrent execution without the need to retrieve a specific result.
   * The `launch` function returns a `Job` object, which represents the coroutine and can be used to control or cancel the coroutine if needed.
   Example :
     val job = GlobalScope.launch {
         // Code to be executed concurrently
     }​

2. `async`:
   * The `async` function is used to launch a new coroutine and explicitly return a `Deferred` object, representing a future result of the computation.
   * It is suitable for tasks where you need to perform concurrent computations and retrieve a result or combine multiple results later.
   * The `Deferred` object allows you to explicitly await the result using the `await()` function, which suspends the coroutine until the result is available.
   * The `async` function returns a `Deferred<T>` object, where `T` is the type of the result.
   Example :
     val deferred = GlobalScope.async {
         // Code to be executed concurrently and return a result
         return@async "Result"
     }
     val result = deferred.await()​

In the key difference between `launch` and `async` lies in the return value and purpose:

* `launch` is used for concurrent execution without expecting a specific result. It returns a `Job` object.
* `async` is used for concurrent execution with an explicitly returned result. It returns a `Deferred<T>` object and allows you to await the result using `await()`.

Both `launch` and `async` are essential components of Kotlin coroutines, and the choice between them depends on whether you need to retrieve a result from the concurrent computation or simply perform concurrent tasks without expecting a specific outcome.
The `suspend` modifier in Kotlin is used to define functions that can be suspended and resumed later without blocking the thread. It is a fundamental part of Kotlin coroutines, which are used for asynchronous and concurrent programming. Here's the purpose and significance of the `suspend` modifier :

1. Asynchronous and Non-Blocking Execution :
   * Functions marked with the `suspend` modifier can perform long-running or blocking operations without blocking the calling thread.
   * When a `suspend` function is called, it can suspend its execution at specific suspension points without blocking the thread, allowing other code to run in the meantime.
   * This enables efficient concurrency and non-blocking behavior, making it possible to write asynchronous code in a more sequential and readable manner.

2. Integration with Coroutines :
   * The `suspend` modifier is specifically designed to work with Kotlin coroutines.
   * Coroutines are lightweight threads that can be created and managed efficiently, allowing for highly concurrent and asynchronous programming.
   * Coroutines use the `suspend` modifier to indicate that a function can suspend its execution, enabling other coroutines or threads to continue execution.

3. Sequential and Structured Code :
   * Using `suspend` functions, along with other coroutine constructs, allows for the creation of sequential and structured asynchronous code.
   * Coroutine builders like `launch` and `async` can invoke `suspend` functions and handle the suspension and resumption of execution transparently.
   * This helps in writing asynchronous code that resembles synchronous code in terms of readability and maintainability.

4. Suspending Operations :
   * The `suspend` modifier is often used with functions that perform operations that can be suspended, such as I/O operations, network requests, or database queries.
   * These operations typically involve waiting for external resources to respond, and using `suspend` functions allows them to be performed asynchronously without blocking the thread.
In Kotlin, smart casts are a feature that allows the compiler to automatically cast an object to a more specific type based on certain conditions. It is a mechanism that enhances type safety and reduces the need for explicit type checks and casts in your code. Here's how smart casts work and their benefits:

1. Type Checks :
   - Smart casts are triggered when you perform a type check using the `is` operator or use the `!is` operator for a negated type check.
   - When the compiler determines that a type check is true, it automatically casts the object to that specific type within the corresponding block of code.

2. Automatic Casting :
   * After a successful type check, the compiler treats the object as if it were of the checked type within the scope where the type check is performed.
   * This allows you to access properties and invoke functions specific to that type without the need for explicit casts.

3. Eliminates Explicit Casts :
   * Smart casts help eliminate the need for explicit casts (`as` operator) in your code, making it more concise and less error-prone.
   * The compiler tracks the type information and performs the necessary casts for you, reducing the risk of runtime ClassCastException errors.

4. Scoping :
   * Smart casts have scoping rules, meaning the casted object is available only within the scope where the type check is performed.
   * The object is casted to the more specific type within that scope, and outside that scope, it is treated as the original type.
5. Nullable Types :
   * Smart casts also work with nullable types (`T?`).
   * When you perform a null check (`!= null` or `== null`), the compiler smart casts the nullable type to a non-null type within the corresponding block of code.

Here's an example to illustrate the usage of smart casts :
fun processString(str: String?) {
    if (str != null) {
        // Compiler smart casts 'str' to non-null 'String' type
        println(str.length)
    }
}​

In the example, the smart cast is triggered after the null check (`str != null`). Within the block, the compiler automatically treats `str` as a non-null `String` type, allowing access to its `length` property without the need for an explicit cast.

Smart casts in Kotlin provide convenience and safety by reducing the verbosity of type checks and explicit casts. They improve code readability and make working with different types more intuitive, while also ensuring type safety at compile-time.
In Kotlin, a sealed class is a special type of class that restricts the inheritance hierarchy of its subclasses. It allows you to define a closed set of subclasses that are known and limited in advance. Here are some key points about sealed classes:

1. Limited Subclass Hierarchy :
   * A sealed class can have a predefined and limited number of subclasses.
   * All the subclasses of a sealed class must be declared within the same Kotlin file where the sealed class is declared.
   * This ensures that the subclasses of a sealed class are known and restricted within a specific scope.

2. Restricted Inheritance :
   * The subclasses of a sealed class must be declared as nested classes or subclasses within the same file.
   * Sealed classes cannot be instantiated directly.
   * This restricts the inheritance hierarchy of the sealed class to only those subclasses defined within the same file.

3. Useful for Representing Restricted Types :
   * Sealed classes are commonly used to represent restricted types or algebraic data types, where the value can only be one of the defined subclasses.
   * Each subclass can represent a specific case or variation of the sealed class.
4. Exhaustive when Statements :
   * Sealed classes work well with when expressions or statements in Kotlin.
   * The compiler can perform exhaustive checks in when statements, ensuring that all possible subclasses are handled, eliminating the need for an else branch.

Here's an example to illustrate the usage of sealed classes :
sealed class Result
data class Success(val data: String) : Result()
data class Error(val message: String) : Result()
object Loading : Result()

fun processResult(result: Result) {
    when (result) {
        is Success -> println(result.data)
        is Error -> println(result.message)
        Loading -> println("Loading")
    }
}​

In the example, `Result` is a sealed class with three subclasses: `Success`, `Error`, and `Loading`. These subclasses represent different cases or variations of the `Result` type. The sealed class ensures that these subclasses are limited and known in advance. The `processResult` function uses a when expression to handle each possible case of the sealed class.

Sealed classes provide a powerful mechanism for modeling restricted types and representing a closed set of subclasses. They improve code safety by limiting the possibilities and ensuring exhaustive checks in when statements.
Kotlin provides the facility of lazy initialization, which specifies that your variable will not be initialized unless you use that variable in your code. It will be initialized only once. After that, you use the same value.

In lazy initialization, the lazy() function is used that takes a lambda and returns an instance of lazy, which can serve as a delegate for implementing a lazy property: the first call to get() executes the lambda passed to lazy() and remembers the result, subsequent calls to get() simply return the remembered result.
val test: String by lazy {  
    val testString = "some value"  
} ​
Lateinit means late initialization. It is used when you do not want to initialize a variable in the constructor and instead initialize it later.

You should declare that variable with lateinit keyword to guarantee the initialization, not before using it. It will not allocate memory until it is initialized. You cannot use lateinit for primitive type properties like Int, Long, etc.
lateinit var test: String  
fun doSomething() {  
    test = "Some value"  
    println("Length of string is "+test.length)  
    test = "change value"  
}  ​
This is mainly used in the following cases :

Android : variables that get initialized in lifecycle methods.

Using Dagger for DI : injected class variables are initialized outside and independently from the constructor.

Setup for unit tests : test environment variables are initialized in a @Before - annotated method.

Spring Boot annotations (e.g., @Autowired).

Following are the different ways to iterate over any data structure in Kotlin :

For Loop : The for loop is used to scan any data structure that supplies an iterator in this case. It is not used in the same way as the for loop in other programming languages such as Java or C.

In Kotlin, the for loop has the following Syntax :
for(item in collection) {
     // code
}​
Here, collection refers to the data structure to be iterated and item refers to each element of the data structure.

Example :
fun main(args: Array<String>) {
   var numbersArray = arrayOf(1,2,3,4,5,6,7,8,9,10)
 
   for (num in numbersArray){
       if(num % 2 == 0){
           print("$num ")
       }
   }
}

Output :
2 4 6 8 10​


While Loop : It is made up of a code block and a condition to be checked for each iteration. First, the while condition is assessed, and if it is true, the code within the block is executed. Because the condition is verified every time before entering the block, it repeats until the condition turns false. The while loop can be thought of as a series of if statements that are repeated.

The while loop's syntax is as follows :
while(condition) {
         // code
}​
Example :
fun main(args: Array<String>) {
   var number = 1
   while(number <= 5) {
       println(number)
       number++;
   }
}​
Output  :
1
2
3
4
5​

Do While Loop : The condition is assessed after all of the statements inside the block have been executed. If the do-while condition is true, the code block is re-executed. As long as the expression evaluates to true, the code block execution procedure is repeated. The loop ends if the expression becomes false, and control is passed to the sentence following the do-while loop. Because it verifies the condition after the block is executed, it's also known as a post-test loop.

The do-while loop's syntax is as follows :
do {
     // code
{
while(condition)​
Example :
fun main(args: Array<String>) {
   var number = 4
   var sum = 0
 
   do {
       sum += number
       number--
   }while(number > 0)
   println("Sum of first four natural numbers is $sum")
}​
Output :
Sum of first four natural numbers is 10​
Following are the different ways by which we can concatenate two strings in Kotlin:

Using String Interpolation : We use the technique of string interpolation to concatenate the two strings. Basically, we substitute the strings in place of their placeholders in the initialisation of the third string.
val s1 = "FreeTime"
val s2 = "Learning"
val s3 = "$s1 $s2" // stores "FreeTime Learning"​

Using the + or plus() operator : We use the ‘+’ operator to concatenate the two strings and store them in a third variable.
val s1 = "FreeTime"
val s2 = "Learning"
val s3 = s1 + s2 // stores "FreeTimeLearning"
val s4 = s1.plus(s2) // stores "FreeTimeLearning"​

Using StringBuilder : We concatenate two strings using the StringBuilder object. First, we append the first string and then the second string.
val s1 = "FreeTime"
val s2 = "Learning"
val s3 =  StringBuilder()     
s3.append(s1).append(s2)
val s4 = s3.toString() // stores "FreeTimeLearning"​
In Kotlin, a coroutine is a lightweight concurrency design pattern that allows you to write asynchronous and non-blocking code in a more sequential and structured manner. Coroutines enable you to perform concurrent operations without the complexities of traditional threading models. Here are some key points about coroutines :

1. Asynchronous and Non-Blocking Execution:
   * Coroutines enable writing asynchronous code that doesn't block the thread.
   * They provide a way to suspend and resume execution at certain points, allowing other coroutines or threads to run in the meantime.
   * This allows you to write code that looks sequential and handles concurrency in a more straightforward way.

2. Language-Level Feature :
   * Coroutines are a language-level feature introduced in Kotlin.
   * They are built on top of standard language constructs and don't require external libraries or frameworks.
   * Kotlin provides a rich set of coroutine-related functions and utilities in the standard library to work with coroutines effectively.

3. Built-in Coroutine Builders :
   * Kotlin provides several coroutine builders, such as `launch`, `async`, `runBlocking`, and `withContext`, to initiate coroutines.
   * Coroutine builders allow you to start a new coroutine with specific behavior and control flow.
4. Suspending Functions :
   * Coroutines work with suspending functions, which are functions that can be suspended and resumed later without blocking the thread.
   * Suspending functions are marked with the `suspend` modifier and can perform long-running or blocking operations without blocking the calling thread.

5. Coroutine Context and Dispatchers :
   * Coroutines run within a specific context defined by the coroutine builder.
   * The context specifies the execution context, such as the thread pool or dispatcher, in which the coroutine runs.
   * Kotlin provides dispatchers like `Dispatchers.IO`, `Dispatchers.Default`, and `Dispatchers.Main` to control the thread or thread pool used by coroutines.

6. Exception Handling :
   * Coroutines provide structured exception handling mechanisms to handle exceptions within coroutines.
   * You can use `try-catch-finally` blocks within coroutines to handle exceptions in a familiar way.

7. Integration with Java :
   * Kotlin coroutines can seamlessly interoperate with existing Java code, allowing you to use coroutines in mixed Kotlin and Java projects.

Coroutines in Kotlin provide a powerful and efficient way to handle concurrency, asynchronous operations, and non-blocking code. They simplify the complexities associated with traditional thread-based programming models and enable you to write more readable and maintainable code that handles concurrency in a more sequential and structured manner.
In Kotlin, the `filter` and `map` functions are both higher-order functions used for transforming and manipulating collections. However, they have distinct purposes and behaviors. Here's an explanation of the difference between `filter` and `map`:

1. `filter` Function :
   * The `filter` function is used to filter elements from a collection based on a given predicate (a lambda expression that returns a Boolean value).
   * It returns a new collection that contains only the elements for which the predicate evaluates to `true`.
   * The original collection remains unchanged.
   Example :
     val numbers = listOf(1, 2, 3, 4, 5)
     val filtered = numbers.filter { it % 2 == 0 } // Keep only even numbers
     // filtered: [2, 4]​


2. `map` Function :
   * The `map` function is used to transform each element in a collection by applying a transformation function (a lambda expression) to each element.
   * It returns a new collection that contains the transformed elements.
   * The original collection remains unchanged.
   Example :
     val numbers = listOf(1, 2, 3, 4, 5)
     val mapped = numbers.map { it * it } // Square each number
     // mapped: [1, 4, 9, 16, 25]​
In the key differences between `filter` and `map` are:

* `filter` is used to select or filter elements from a collection based on a predicate, returning a new collection with only the matching elements.
* `map` is used to transform each element in a collection by applying a transformation function, returning a new collection with the transformed elements.

While `filter` allows you to select elements that meet certain conditions, `map` enables you to transform each element into something else. Both functions are valuable for working with collections and can often be used together in combination to achieve the desired result.
The `groupBy` function in Kotlin is a higher-order function used to group elements of a collection based on a specific key or property. It allows you to categorize and organize elements into groups based on a common characteristic.

Here's an explanation of the purpose and usage of the `groupBy` function:

1. Grouping Elements :
   * The `groupBy` function takes a lambda expression or a function that defines the key for grouping elements.
   * For each element in the collection, the key is computed, and elements with the same key are grouped together.

2. Resulting Map :
   * The `groupBy` function returns a map where each key corresponds to a group, and the corresponding value is a list of elements in that group.
   * The map is structured in a way that allows efficient lookup of elements based on the grouping key.
   * Each group is represented by a key-value pair in the map.
3. Example :
   data class Person(val name: String, val age: Int)

   val people = listOf(
       Person("Alice", 20),
       Person("Bob", 25),
       Person("Charlie", 20),
       Person("Dave", 25)
   )

   val groupedByAge = people.groupBy { it.age }
   // Result: {20=[Person(name=Alice, age=20), Person(name=Charlie, age=20)],
   //          25=[Person(name=Bob, age=25), Person(name=Dave, age=25)]}​

   In this example, the `people` list is grouped by the `age` property using the `groupBy` function. The resulting map (`groupedByAge`) has keys corresponding to the distinct age values (20 and 25), and the values are lists of `Person` objects with the same age.

The `groupBy` function is useful in various scenarios where you need to categorize elements based on specific criteria. It allows you to efficiently group elements and provides a convenient way to organize and process data.
In Kotlin, property delegation is a powerful feature that allows you to delegate the implementation of property accessors (getters and setters) to another object, known as the delegate. It enables you to offload the responsibility of handling property access to a separate class or instance, reducing boilerplate code and providing additional flexibility. Here's an explanation of property delegation and its usage:

1. Delegate Pattern :
   * Property delegation follows the delegate pattern, where the responsibility of property access is delegated to another object.
   * The delegate object typically implements the `ReadOnlyProperty` or `ReadWriteProperty` interface, depending on whether the property is read-only or read-write.
   * The delegate handles the actual implementation of property accessors.

2. Delegated Properties :
   * Delegated properties are created using the `by` keyword followed by the delegate instance.
   * The delegate must have compatible property accessor signatures (e.g., `getValue` and `setValue`) matching the delegated property.

3. Standard Delegates :
   * Kotlin provides several standard delegates in the standard library, including `lazy`, `observable`, `vetoable`, and more.
   * These standard delegates offer common functionalities such as lazy initialization, property change observation, vetoing property changes, etc.

4. Custom Delegates :
   * You can create custom delegate classes by implementing the `ReadOnlyProperty` or `ReadWriteProperty` interfaces.
   * Custom delegates allow you to define custom behavior for property access, validation, caching, or any other logic as needed.
5. Example :
   class Example {
       var value: String by Delegate()
   }

   class Delegate {
       private var backingField: String = "Default"

       operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
           return backingField
       }

       operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
           backingField = value
       }
   }

   fun main() {
       val example = Example()
       println(example.value) // Prints "Default"
       example.value = "New Value"
       println(example.value) // Prints "New Value"
   }​

In this example, the `Example` class has a `value` property that is delegated to the `Delegate` class. The `Delegate` class handles the actual storage and retrieval of the property value. When accessing or modifying the `value` property, the corresponding methods in the `Delegate` class (`getValue` and `setValue`) are invoked.

Property delegation in Kotlin provides a clean and modular way to handle property access and behavior. It reduces boilerplate code and allows you to encapsulate complex logic within separate delegate classes. It is particularly useful when you need to add additional functionality to properties or reuse common property access patterns across multiple classes.
In Kotlin, the `==` and `===` operators are used for equality comparisons, but they have different behaviors based on the type of comparison being performed:

1. `==` Operator (Structural Equality) :
   * The `==` operator is used for structural equality comparisons.
   * When used to compare two objects or values, it checks if their content or values are equal.
   * For most data types, the `==` operator is equivalent to the `equals()` method.
    Example :
     val a: String = "Hello"
     val b: String = "Hello"
     val c: String? = "Hello"
     println(a == b) // true
     println(a == c) // true​

* In the example, the `==` operator checks if the content of `a` and `b` (both strings) are equal, which is true. It also compares `a` with the nullable `String` `c`, and the result is true.
2. `===` Operator (Referential Equality) :
   * The `===` operator is used for referential equality comparisons.
   * When used to compare two objects or values, it checks if they refer to the same memory location (i.e., the same object instance).
   * The `===` operator is similar to the `==` operator in Java when comparing object references.
   Example :
     val x: String = "Hello"
     val y: String = "Hello"
     val z: String? = "Hello"
     println(x === y) // true
     println(x === z) // false​

* In the example, the `===` operator compares the references of `x` and `y` (both strings) and returns true because they refer to the same object instance. However, comparing `x` with the nullable `String` `z` using `===` returns false since they don't refer to the same object instance.
In Kotlin, the `!!` operator is called the "not-null assertion operator," and its purpose is to assert that a value is not null. It is used when you are certain that a nullable reference is not null at a specific point in your code, and you want to communicate this assurance to the compiler. Here's an explanation of the purpose and usage of the `!!` operator:

1. Asserting Non-Nullability :
   * The `!!` operator is used to assert that a nullable reference is not null at a particular point in the code.
   * It tells the compiler that you are confident the value is not null, overriding the null safety checks.

2. Nullable Types :
   * In Kotlin, nullable types are indicated by adding a `?` to the type declaration.
   * Nullable types allow variables to hold null values, and the compiler enforces null safety rules to prevent null pointer exceptions.

3. Potential Nullability Risks :
   * When using the `!!` operator, you are explicitly telling the compiler that you are aware of the potential nullability risks and are taking responsibility for handling null values.
   * If the value is actually null at runtime, a `NullPointerException` will be thrown.
4. Use with Caution :
   * The `!!` operator should be used sparingly and with caution because it bypasses the null safety checks provided by the Kotlin type system.
   * It is typically used in situations where you have manually checked for nullability or where you are confident that a value cannot be null.

5. Preferred Approaches :
   * In general, it is recommended to use safe calls (`?.`) or null checks (`!= null`) along with safe operators like the Elvis operator (`?:`) or safe casts (`as?`) to handle nullable references in a safer manner.
   * These approaches maintain the null safety guarantees of Kotlin and provide more robust handling of null values.

It's important to note that using the `!!` operator excessively or without proper understanding can lead to null pointer exceptions, which Kotlin aims to prevent. Therefore, it's generally recommended to utilize safe practices and favor null safety features provided by the language whenever possible.
Following are the main usage of @JvmStatic, @JvmOverloads, and @JvmFiled in Kotlin :

@JvmStatic : The @JvmStatic annotation is used to tell the compiler that the method is a static method, and you can use it in Java code.

@JvmOverloads : The @JvmOverloads annotation is required when we need to use the default values passed as an argument in Kotlin code from the Java code.

@JvmField : The @JvmField annotation is used to access the fields of a Kotlin class from Java code without any getters and setters. We need to use the @JvmField in the Kotlin code.
Following are some extension methods that Kotlin provides to java.io.File :

* bufferedReader() : It is used for reading the contents of a file into BufferedReader.

* readBytes() : It is used for reading the contents of the file to ByteArray.

* readLines() : It is used for reading lines in the file to List.

* readText() : It is used for reading contents of the file to a single String.

* forEachLine() : It is used for reading a file line by line in Kotlin.
Following are the key differences between lateinit and lazy in Kotlin :

* In Kotlin, lazy can only be used for val properties while lateinit can only be applied to var because it can't be compiled to a final field. Thus no immutability can be guaranteed.

* You have to use lateinit, if you want your property to be initialized from outside in a way probably unknown beforehand.

Lateinit vs. lazy in Kotlin :
Lateinit Lazy
The lateinit can be initialized from anywhere the object is seen. The lazy can only be initialized from the initializer lambda.
In lateinit, multiple initializations are possible. The lazy can be initialized a single time only.
The lateinit is non-thread safe. It is up to the user to initialize it correctly in a multi-threaded environment. The lazy support thread-safety by default and ensures that the initializer is invoked once.
It is not eligible for nonnull properties. It is also not eligible for nonnull properties.
You can use it only for var. You can use it only for val.
It adds an isInitialized method to check whether the value has been initialized before. In this, the property is never able to un-initialize.
It is not allowed on properties of primitive types. It is allowed on properties of primitive types.
The program's design clarity is improved by using mutable and immutable lists. This is done to have the developer think about and clarify the collection's purpose.

We use a mutable list if the collection will alter as part of the design. On the other hand, we use an immutable list if the model is only meant to be viewed.

Val and var serve a distinct purpose than immutable and mutable lists. The val and var keywords specify how a variable's value/reference should be handled. We use var when the value or reference of a variable can be altered at any moment. On the other hand, we use val when a variable's value/reference can only be assigned once and cannot be modified later in the execution.

Immutable lists are frequently preferred for a variety of reasons :

* They promote functional programming, in which state is passed on to the next function, which constructs a new state based on it, rather than being altered. This is evident in Kotlin collection methods like map, filter, reduce, and so forth.

* It's often easier to understand and debug software that doesn't have any side effects (you can be sure that the value of an object will always be the one at its definition).

* Because no write access is required in multi-threaded systems, immutable resources cannot induce race conditions.

However, there are some disadvantages of using immutable lists as well. They are as follows :

* Copying large collections simply to add/remove a single piece is very expensive.

* When you need to alter single fields frequently, immutability can make the code more difficult. Data classes in Kotlin provide a built-in copy() method that allows you to clone an instance while changing only part of the fields' values.
In Kotlin, you can define an enum class using the `enum` keyword. Enum classes are used to represent a fixed number of constants or values. Each constant in an enum class is an instance of the enum class itself. Here's the syntax to define an enum class in Kotlin :
enum class EnumClassName {
    CONSTANT1,
    CONSTANT2,
    CONSTANT3,
    // ...
}​
Let's break down the syntax :

* The `enum` keyword is used to indicate the declaration of an enum class.
* `EnumClassName` is the name you choose for your enum class. You can use any valid identifier as the class name.
* Inside the curly braces `{}`, you list the constant values of the enum class, separated by commas.

Here's an example of defining an enum class called `Color` with three constants: `RED`, `GREEN`, and `BLUE`:
enum class Color {
    RED,
    GREEN,
    BLUE
}​
You can use enum constants like any other value in Kotlin. For example, you can access them using dot notation (`Color.RED`, `Color.GREEN`, etc.), compare them for equality (`==`), iterate over them, and use them in switch-like expressions (when statements) to perform different actions based on the enum value.

Enums in Kotlin can also have additional properties, functions, and even implement interfaces if needed. Each enum constant can have its own set of properties and functions. Enums provide a convenient and type-safe way to define a set of related constant values and are widely used for representing a fixed set of options or states in applications.
launch / join :  The launch command is used to start and stop a coroutine. It's as though a new thread has been started. If the code inside the launch throws an exception, it's considered as an uncaught exception in a thread, which is typically written to stderr in backend JVM programs and crashes Android applications. Join is used to wait for the launched coroutine to complete before propagating its exception. A crashed child coroutine, on the other hand, cancels its parent with the matching exception.

async / await : The async keyword is used to initiate a coroutine that computes a result. You must use await on the result, which is represented by an instance of Deferred. Uncaught exceptions in async code are held in the resultant Deferred and are not transmitted anywhere else. They are not executed until processed.
Destructuring declarations in Kotlin allow you to extract individual components from structured objects, such as data classes, arrays, or other types that provide component functions or extension functions with component-like signatures. It provides a concise way to assign multiple variables at once by extracting values from the object.

Here's how destructuring declarations work :

1. Destructuring Declarations with Data Classes :
   * When using destructuring declarations with data classes, the properties of the data class can be easily extracted into separate variables.
   * The number of variables on the left side of the assignment operator (`=`) must match the number of properties in the data class.
   Example :
     data class Person(val name: String, val age: Int)

     val person = Person("Alice", 25)
     val (name, age) = person
     println(name) // Alice
     println(age) // 25​

2. Destructuring Declarations with Arrays :
   * Destructuring declarations can also be used with arrays. In this case, each element of the array is assigned to a separate variable.
    Example :
     val numbers = arrayOf(1, 2, 3)
     val (a, b, c) = numbers
     println(a) // 1
     println(b) // 2
     println(c) // 3​
3. Destructuring Declarations with Custom Types :
   * Destructuring declarations can be used with custom types as long as the type provides component functions or extension functions with component-like signatures.
   * The component functions should return the individual components in the desired order.
    Example :
     class Point(val x: Int, val y: Int) {
         operator fun component1() = x
         operator fun component2() = y
     }

     val point = Point(3, 5)
     val (x, y) = point
     println(x) // 3
     println(y) // 5​

4. Ignoring Values :
   * If you're not interested in a particular value during destructuring, you can use an underscore (`_`) as the variable name to indicate that the value should be ignored.
   Example :
     val (_, age) = person
     println(age) // 25​
Destructuring declarations provide a convenient way to extract values from structured objects and assign them to individual variables in a concise and readable manner. It improves code readability and reduces boilerplate code when working with complex objects or collections.