Blog

  • Regex & Ranges

    Regular Expression

    Kotlin provides robust support for regular expressions through the Regex class, which allows for efficient string pattern matching. A Regex object represents a regular expression and can be used for various string-matching purposes.

    Constructors:
    • Regex(pattern: String): Creates a regular expression from the provided pattern.
    • Regex(pattern: String, option: RegexOption): Creates a regular expression with a specific option from the RegexOption enum.
    • Regex(pattern: String, options: Set<RegexOption>): Creates a regular expression with a set of options.
    Properties:
    • val options: Set<RegexOption>: Contains the set of options used for regex creation.
    • val pattern: String: Stores the pattern as a string.
    Functions in Regex

    1. containsMatchIn(): This function checks if there is a match for the regex pattern within a given input and returns a boolean.

    fun main() {
        val pattern = Regex("^a")  // Matches any string starting with 'a'
        println(pattern.containsMatchIn("abc"))  // true
        println(pattern.containsMatchIn("bac"))  // false
    }

    Output:

    fun main() {
        val pattern = Regex("^a")  // Matches any string starting with 'a'
        println(pattern.containsMatchIn("abc"))  // true
        println(pattern.containsMatchIn("bac"))  // false
    }

    2. find(): This function returns the first match of the regex in the input starting from a specified index.

    fun main() {
        val pattern = Regex("ll")  // Matches "ll"
        val match: MatchResult? = pattern.find("HelloHello", 5)
        println(match?.value)  // ll
    }

    Output:

    ll

    3. findAll() : This function finds all the matches for a regex in the input starting from a specified index and returns them as a sequence.

    fun main() {
        val pattern = Regex("ab.")
        val matches: Sequence<MatchResult> = pattern.findAll("abcabdeabf", 0)
        matches.forEach { match -> println(match.value) }
    }

    Output:

    abc
    abd

    4. matches(): This function checks if the entire input string matches the regular expression.

    fun main() {
        val pattern = Regex("g([ee]+)ks?")
        println(pattern.matches("geeks"))  // true
        println(pattern.matches("geeeeeeks"))  // true
        println(pattern.matches("geeksforgeeks"))  // false
    }

    Output:

    true
    true
    false

    5. matchEntire(): This function tries to match the entire input string to the regex pattern and returns the match if successful, otherwise returnsnull.

    fun main() {
        val pattern = Regex("geeks?")
        println(pattern.matchEntire("geeks")?.value)  // geeks
        println(pattern.matchEntire("geeeeeks")?.value)  // null
    }

    Output:

    geeks
    null

    6. replace(): This function replaces all occurrences of the pattern in the input string with a replacement string.

    7.replaceFirst(): Replaces only the first occurrence.

    fun main() {
        val pattern = Regex("xyz")
        println(pattern.replace("xyzxyz", "abc"))  // abcabc
        println(pattern.replaceFirst("xyzxyz", "abc"))  // abcxyz
    }

    Output:

    abcabc
    abcxyz

    8. split(): This function splits the input string into parts based on the regular expression pattern.

    fun main() {
        val pattern = Regex("\\s+")  // Split by whitespace
        val result = pattern.split("Kotlin is great")
        result.forEach { println(it) }
    }

    Output:

    Kotlin
    is
    great

    Ranges

    In Kotlin, a range is a collection of values defined by a start point, an end point, and a step. The range includes both the start and stop values, and the step value, which is the increment or decrement, is 1 by default. Kotlin’s range can work with comparable types like numbers and characters.

    Creating Ranges in Kotlin

    There are three primary ways to create a range:

    1. Using the .. operator
    2. Using the rangeTo() function
    3. Using the downTo() function

    1. Using the .. Operator: The .. operator creates a range from the start to the end value, including both.

    Example 1: Integer Range

    fun main() {
        println("Integer range:")
        for (num in 1..5) {
            println(num)
        }
    }

    Output:

    Integer range:
    1
    2
    3
    4
    5

    Example 2: Character Range

    fun main() {
        println("Character range:")
        for (ch in 'a'..'e') {
            println(ch)
        }
    }

    Output:

    Character range:
    a
    b
    c
    d
    e

    2. Using the rangeTo() Function : The rangeTo() function is another way to create ranges, similar to using the .. operator.

    Example 1: Integer Range

    fun main() {
        println("Integer range:")
        for (num in 1.rangeTo(5)) {
            println(num)
        }
    }

    Output:

    Integer range:
    1
    2
    3
    4
    5

    Example 2: Character Range

    fun main() {
        println("Character range:")
        for (ch in 'a'.rangeTo('e')) {
            println(ch)
        }
    }

    Output:

    Character range:
    a
    b
    c
    d
    e

    3. Using the downTo() Function: The downTo() function creates a range that decreases from the starting value to the ending value.

    Example 1: Integer Range in Descending Order

    fun main() {
        println("Integer range in descending order:")
        for (num in 5.downTo(1)) {
            println(num)
        }
    }

    Output:

    Integer range in descending order:
    5
    4
    3
    2
    1

    Example 2: Character Range in Descending Order

    fun main() {
        println("Character range in reverse order:")
        for (ch in 'e'.downTo('a')) {
            println(ch)
        }
    }

    Output:

    Character range in reverse order:
    e
    d
    c
    b
    a
    Using the forEach Loop

    The forEach loop can also be used to traverse over a range.

    fun main() {
        println("Integer range:")
        (2..5).forEach(::println)
    }

    Output:

    Integer range:
    2
    3
    4
    5
    step(): Customizing the Increment:

    The step() function allows you to specify the increment or step value in the range. By default, the step value is 1.

    Example: Step Usage

    fun main() {
        // Custom step value
        for (i in 3..10 step 2) {
            print("$i ")
        }
        println()
        println((11..20 step 2).first)  // Print first value
        println((11..20 step 4).last)   // Print last value
        println((11..20 step 5).step)   // Print step value
    }

    Output:

    3 5 7 9
    11
    19
    5
    reversed(): Reversing the Range:

    The reversed() function reverses the range.

    fun main() {
        val range = 2..8
        for (x in range.reversed()) {
            print("$x ")
        }
    }

    Output:

    8 7 6 5 4 3 2
    Predefined Functions for Ranges

    Kotlin offers predefined functions like min()max()sum(), and average() to work with ranges.

    fun main() {
        val predefined = (15..20)
        println("The minimum value of range is: ${predefined.minOrNull()}")
        println("The maximum value of range is: ${predefined.maxOrNull()}")
        println("The sum of all values of range is: ${predefined.sum()}")
        println("The average value of range is: ${predefined.average()}")
    }

    Output:

    The minimum value of range is: 15
    The maximum value of range is: 20
    The sum of all values of range is: 105
    The average value of range is: 17.5

    Checking if a Value Lies in a Range

    You can check if a value lies within a range using the in keyword.

    fun main() {
        val i = 2
        if (i in 5..10) {
            println("$i lies within the range")
        } else {
            println("$i does not lie within the range")
        }
    }

    Output:

    2 does not lie within the range
  • Kotlin and Null Safety

    Kotlin’s type system is designed to eliminate the risk of null reference errors from the code. NullPointerExceptions (NPE) often cause unexpected runtime crashes and application failures. Kotlin aims to prevent this billion-dollar mistake by handling null references at compile-time.

    If you’re coming from Java or another language with null references, you’ve likely experienced NullPointerExceptions. Kotlin’s compiler throws a NullPointerException if it detects an unhandled null reference without proceeding with further execution.

    Common causes of NullPointerException:
    • Explicitly calling throw NullPointerException()
    • Using the !! operator
    • Uninitialized data, such as passing an uninitialized this reference as an argument
    • Java interoperability issues, such as trying to access a member on a null reference, or using generics with incorrect nullability
    Nullable and Non-Nullable Types in Kotlin

    In Kotlin, references are either nullable or non-nullable. Non-nullable references cannot hold null values. If you attempt to assign null to a non-nullable reference, the compiler will raise an error.

    var s1: String = "Hello"
    s1 = null  // Error: compilation issue

    However, to declare a variable that can hold null, we use a nullable type by appending ? to the type:

    var s2: String? = "Hello Kotlin"
    s2 = null  // No compilation error

    If you want to access the length of a nullable string, you must use safe calls or handle the possibility of a null value:

    val length = s2?.length  // Safe call, returns null if s2 is null

    Example: Non-Nullable Type in Kotlin

    fun main() {
        var s1: String = "Kotlin"
    
        println("The length of the string s1 is: ${s1.length}")
    }

    Output:

    The length of the string s1 is: 6

    In this case, assigning null to s1 would result in a compilation error.

    Example: Nullable Type in Kotlin

    null

    Output:

    var s2: String? = "Hello Kotlin"
    s2 = null  // No compilation error

    Here, Kotlin allows assigning null to s2 since it is a nullable type, but accessing its properties requires safe calls.

    Checking for null in Conditions

    You can use if-else blocks to check if a variable is null:

    fun main() {
        var s: String? = "Kotlin"
    
        if (s != null) {
            println("String length is ${s.length}")
        } else {
            println("Null string")
        }
    
        s = null
        if (s != null) {
            println("String length is ${s.length}")
        } else {
            println("Null string")
        }
    }

    Output:

    String length is 6
    Null string
    Safe Call Operator ?.

    The ?. operator simplifies null checks. If the value before the ?. is null, the expression after ?. is not evaluated, and null is returned.

    fun main() {
        var firstName: String? = "Alice"
        var lastName: String? = null
    
        println(firstName?.toUpperCase())  // Output: ALICE
        println(lastName?.toUpperCase())   // Output: null
    }
    let() Function with Safe Call

    The let() function executes only when the reference is not null:

    fun main() {
        var firstName: String? = "Alice"
    
        firstName?.let { println(it.toUpperCase()) }  // Output: ALICE
    }

    Example using let() with Nullable Values

    fun main() {
        val stringList: List<String?> = listOf("Hello", "World", null, "Kotlin")
        val filteredList = stringList.filterNotNull()
    
        filteredList.forEach { println(it) }
    }

    Output:

    Hello
    World
    Kotlin
    The Elvis Operator ?:

    The Elvis operator returns a default value when the expression on the left is null:

    fun main() {
        var str: String? = null
        val length = str?.length ?: -1
        println(length)  // Output: -1
    }

    Example using the Elvis Operator

    fun main() {
        var str: String? = "Kotlin"
        println(str?.length ?: "-1")  // Output: 6
    
        str = null
        println(str?.length ?: "-1")  // Output: -1
    }
    Not Null Assertion Operator !!

    The !! operator forces Kotlin to treat a nullable type as non-null. It will throw a KotlinNullPointerException if the value is null.

    fun main() {
        var str: String? = "Kotlin"
        println(str!!.length)
    
        str = null
        println(str!!.length)  // Throws KotlinNullPointerException
    }

    Output:

    6
    Exception in thread "main" kotlin.KotlinNullPointerException

    Explicit Type Casting

    In Kotlin, smart casting allows us to use the is or !is operator to check the type of a variable. Once the type is confirmed, the compiler automatically casts the variable to the desired type. However, in explicit type casting, we use the as operator.

    Explicit type casting can be performed in two ways:

    • Unsafe cast operatoras
    • Safe cast operatoras?
    • Unsafe Cast Operator: as : When using the as operator, we manually cast a variable to the target type. However, if the casting fails, it results in a runtime exception. This is why it’s considered unsafe. Example
    fun main() {
        val text: String = "This works!"
        val result: String = text as String  // Successful cast
        println(result)
    }

    Output:

    This works!

    While this example works fine, using the as operator to cast an incompatible type will throw a ClassCastException at runtime.

    For instance, attempting to cast an Integer to a String:

    fun main() {
        val number: Any = 42
        val result: String = number as String  // Throws exception
        println(result)
    }

    Output:

    Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String

    Similarly, trying to cast a nullable type to a non-nullable type will result in a TypeCastException:

    fun main() {
        val text: String? = null
        val result: String = text as String  // Throws exception
        println(result)
    }

    Output:

    This works!

    Similarly, trying to cast a nullable type to a non-nullable type will result in a TypeCastException:

    fun main() {
        val text: String? = null
        val result: String = text as String  // Throws exception
        println(result)
    }

    Output:

    Exception in thread "main" kotlin.TypeCastException: null cannot be cast to non-null type kotlin.String

    To prevent this, we should cast to a nullable target type:

    fun main() {
        val text: String? = null
        val result: String? = text as String?  // Successful cast
        println(result)
    }

    Output:

    null

    Safe Cast Operator: as?: Kotlin provides a safer option with the as? operator, which returns null if the casting fails, instead of throwing an exception.

    Here’s an example of using as? for safe typecasting:

    Safe Cast Operator: as?
    Kotlin provides a safer option with the as? operator, which returns null if the casting fails, instead of throwing an exception.
    
    Here’s an example of using as? for safe typecasting:

    Output:

    Safe casting
    null
    99
  • Classes and Object

    Classes and Object

    In Kotlin, classes and objects are key constructs to represent real-world entities. A class serves as a blueprint for creating objects, defining their structure (properties) and behavior (functions). Each object created from a class holds its own state and behavior. Multiple objects can be instantiated from a single class, each having unique values.

    A class in Kotlin may define properties and methods, which can later be accessed by creating objects of that class.

    Example of a Kotlin Class:

    class Animal {
        var name: String = ""
        var type: String = ""
        var age: Int = 0
    
        fun getDetails(): String {
            return "Animal: $name, Type: $type, Age: $age years"
        }
    }
    
    fun main() {
        val myPet = Animal()
        myPet.name = "Bella"
        myPet.type = "Dog"
        myPet.age = 3
    
        println(myPet.getDetails())
    }

    Output:

    Animal: Bella, Type: Dog, Age: 3 years
    Object-Oriented Programming in Kotlin:

    Kotlin combines both functional and object-oriented paradigms. Earlier, we’ve explored functional aspects such as higher-order functions and lambdas. Now, let’s dive into the object-oriented nature of Kotlin.

    Core OOP Concepts in Kotlin:

    • Class: A class acts as a blueprint for objects, defining similar properties and methods. In Kotlin, classes are declared using the class keyword. Syntax:
    class ClassName {  // class header
        // properties
        // member functions
    }
    • Class Name: Every class must have a name.
    • Class Header: Includes parameters and constructors.
    • Class Body: Enclosed by curly braces {}, containing properties and methods. Both the class header and body are optional, and the body can be omitted if empty.
    class EmptyClass

    Creating a Constructor:

    class ClassName constructor(parameters) {
        // properties
        // member functions
    }

    Example of a Kotlin Class with Constructor:

    val obj = ClassName()

    Output:

    class Employee {
        var name: String = ""
        var age: Int = 0
        var gender: Char = 'M'
        var salary: Double = 0.0
    
        fun setDetails(n: String, a: Int, g: Char, s: Double) {
            name = n
            age = a
            gender = g
            salary = s
        }
    
        fun displayInfo() {
            println("Employee Name: $name")
            println("Age: $age")
            println("Gender: $gender")
            println("Salary: $salary")
        }
    }
    Objects in Kotlin:

    An object is an instance of a class, allowing access to the class properties and methods. You can create multiple objects from a class, each representing unique instances.

    • State: Defined by object attributes (properties).
    • Behavior: Defined by object methods.
    • Identity: Each object has a unique identity allowing interaction with other objects.

    Creating an Object:

    val obj = ClassName()

    Accessing Class Properties:

    obj.propertyName

    Accessing Class Methods:

    obj.methodName()

    Accessing Class Properties:

    obj.methodName()

    Kotlin Program with Multiple Objects:

    class Employee {
        var name: String = ""
        var age: Int = 0
        var gender: Char = 'M'
        var salary: Double = 0.0
    
        fun setDetails(n: String, a: Int, g: Char, s: Double) {
            name = n
            age = a
            gender = g
            salary = s
        }
    
        fun setName(n: String) {
            this.name = n
        }
    
        fun displayDetails() {
            println("Employee Name: $name")
            println("Age: $age")
            println("Gender: $gender")
            println("Salary: $salary")
        }
    }
    
    fun main(args: Array<String>) {
        // Creating multiple objects of Employee class
        val emp1 = Employee()
        val emp2 = Employee()
    
        // Setting and displaying details for the first employee
        emp1.setDetails("John", 35, 'M', 55000.0)
        emp1.displayDetails()
    
        // Setting and displaying name for the second employee
        emp2.setName("Emily")
        println("Second Employee Name: ${emp2.name}")
    }

    Output:

    Employee Name: John
    Age: 35
    Gender: M
    Salary: 55000.0
    Second Employee Name: Emily

    Nested Class and Inner Class

    In Kotlin, you can declare a class within another class. This is known as a nested class. Nested classes are useful when you want to logically group classes that are only used in one place. A nested class by default does not have access to the members of the outer class, unless declared as an inner class using the inner keyword, which allows it to access outer class properties and methods.

    Example:

    class Vehicle {
        var brand: String
        var model: String
        var year: Int
    
        inner class Engine {
            var horsepower: Int = 0
            var type: String = ""
    
            fun getEngineDetails(): String {
                return "$horsepower HP $type engine in a $brand $model"
            }
        }
    
        fun getVehicleInfo(): String {
            return "$brand $model, Year: $year"
        }
    }
    
    fun main() {
        val myVehicle = Vehicle()
        myVehicle.brand = "Honda"
        myVehicle.model = "Civic"
        myVehicle.year = 2022
    
        val vehicleEngine = myVehicle.Engine()
        vehicleEngine.horsepower = 180
        vehicleEngine.type = "V6"
    
        println(vehicleEngine.getEngineDetails())
    }

    Output:

    180 HP V6 engine in a Honda Civic
    Understanding Nested Classes:

    In Kotlin, a class can be declared inside another class, forming a nested class. By default, a nested class does not have access to the outer class’s members. To access a nested class’s properties, you must create an instance of the nested class.

    Syntax of a Nested Class:

    class OuterClass {
        // Outer class properties or methods
    
        class NestedClass {
            // Nested class properties or methods
        }
    }

    Example of Accessing Nested Class Properties:

    class OuterClass {
        val message = "Outer Class"
    
        class NestedClass {
            val firstName = "John"
            val lastName = "Doe"
        }
    }
    
    fun main() {
        // Accessing properties of the Nested Class
        println(OuterClass.NestedClass().firstName)
        println(OuterClass.NestedClass().lastName)
    }

    Output:

    John
    Doe

    Example of Accessing Nested Class Functions:

    class OuterClass {
        var outerProperty = "Outer Class Property"
    
        class NestedClass {
            var nestedProperty = "Nested Class Property"
    
            fun combineProperties(suffix: String): String {
                return nestedProperty + suffix
            }
        }
    }
    
    fun main() {
        val nestedObj = OuterClass.NestedClass()
        val result = nestedObj.combineProperties(" - Function Executed")
        println(result)
    }

    Output:

    Nested Class Property - Function Executed
    Inner Classes in Kotlin:

    An inner class is a type of nested class that has access to the outer class’s members. You can declare an inner class using the inner keyword. This gives the inner class the ability to access members of the outer class.

    Example of an Inner Class:

    class Person {
        var name = "John"
        var age = 30
    
        inner class Address {
            var city = "New York"
            var street = "5th Avenue"
    
            fun getFullAddress(): String {
                return "$name lives at $street, $city"
            }
        }
    }
    
    fun main() {
        val personAddress = Person().Address()
        println(personAddress.getFullAddress())
    }

    Output:

    John lives at 5th Avenue, New York
    Differences Between Nested and Inner Classes:
    • Nested Classes: Cannot access outer class members unless passed explicitly.
    • Inner Classes: Have access to all members (properties and methods) of the outer class.

    Example of Inner Class Accessing Outer Class Property:

    class Organization {
        var company = "Tech Corp"
    
        inner class Department {
            var departmentName = "IT"
    
            fun getDepartmentDetails(): String {
                return "Company: $company, Department: $departmentName"
            }
        }
    }
    
    fun main() {
        val department = Organization().Department()
        println(department.getDepartmentDetails())
    }

    Output:

    Company: Tech Corp, Department: IT
    Pros and Cons of Using Nested and Inner Classes in Kotlin:
    Advantages:

    1. Encapsulation: Helps in grouping related functionality together, improving code clarity and organization.
    2. Reusability: You can reuse nested and inner classes within the outer class or across other classes, making the code more maintainable.
    3. Accessibility: Inner classes have access to the outer class’s members, which facilitates data sharing between the inner and outer classes.

    Disadvantages:

    1. Increased Complexity: Using nested and inner classes can make the code more complex, especially when used extensively or with multiple layers of nesting.
    2. Performance Overhead: Excessive usage of nested and inner classes might impact performance, particularly when the classes are deeply nested.
    3. Difficult Debugging: Debugging can be more challenging when using multiple levels of nesting in classes.

    Setters and Getters

    In Kotlin, you can customize how properties are set and retrieved by defining custom setters and getters. By default, Kotlin generates these functions for you, but you can override them for additional behavior.

    Example of a Property with Default Setter and Getter:

    class Company {
        var name: String = "Default"
    }
    
    fun main() {
        val c = Company()
        c.name = "KotlinGeeks"  // Invokes the setter
        println(c.name)         // Invokes the getter (Output: KotlinGeeks)
    }

    The above code is implicitly generating the getter and setter for the name property. However, if we want to customize this behavior, we can manually define the getter and setter functions.

    Custom Getter and Setter

    In Kotlin, we can define custom logic for setting and getting a property’s value. Below is an example of how to create a custom getter and setter for a property:

    class Company {
        var name: String = ""
            get() = field.toUpperCase()  // Custom getter
            set(value) {                 // Custom setter
                field = value
            }
    }
    
    fun main() {
        val c = Company()
        c.name = "Kotlin World"
        println(c.name)  // Output: KOTLIN WORLD
    }

    In this example, the getter converts the name to uppercase when retrieving it, while the setter stores the name as-is.

    Private Setter Example

    You can also use private setters to restrict modifications to a property only from within the class:

    class Company {
        var name: String = "TechGeeks"
            private set
    
        fun updateName(newName: String) {
            name = newName
        }
    }
    
    fun main() {
        val company = Company()
        println("Company Name: ${company.name}")
    
        company.updateName("GeeksforGeeks")
        println("Updated Company Name: ${company.name}")
    }

    Output:

    Company Name: TechGeeks
    Updated Company Name: GeeksforGeeks
    Custom Getter and Setter with Validation

    You can also add validation logic inside custom setters. For instance, we can enforce restrictions on certain properties like email or age:

    class User(val email: String, pwd: String, userAge: Int, gender: Char) {
        var password: String = pwd
            set(value) {
                field = if (value.length > 6) value else throw IllegalArgumentException("Password is too short")
            }
    
        var age: Int = userAge
            set(value) {
                field = if (value >= 18) value else throw IllegalArgumentException("Age must be at least 18")
            }
    
        var gender: Char = gender
            set(value) {
                field = if (value == 'M' || value == 'F') value else throw IllegalArgumentException("Invalid gender")
            }
    }
    
    fun main() {
        val user = User("user@example.com", "Kotlin@123", 25, 'M')
        println("User email: ${user.email}")
        println("User age: ${user.age}")
    
        // Uncommenting these will throw exceptions
        // user.password = "123"
        // user.age = 17
        // user.gender = 'X'
    }

    Output:

    User email: user@example.com
    User age: 25
    Advantages of Custom Setters and Getters:
    • Validation: Enforce rules like password length or valid age.
    • Encapsulation: Hide the actual implementation of how a property is set or retrieved.
    • Customization: Customize how a property behaves when it is accessed or modified.

    By utilizing getters and setters effectively, Kotlin allows you to implement encapsulation, validation, and data manipulation while keeping your code clean and intuitive.

    Class Properties and Custom Accessors

    Encapsulation in Kotlin

    Encapsulation is one of the core principles of object-oriented programming (OOP). It refers to bundling the data (fields) and the methods (functions) that operate on the data into a single unit called a class. In Kotlin, encapsulation is implemented using properties, where data is stored in private fields, and access to this data is controlled through public getter and setter methods.

    Properties in Kotlin

    In Kotlin, properties are a key language feature, replacing fields and accessor methods commonly used in Java. A property in Kotlin can be declared as either mutable or immutable using the var or val keyword, respectively.

    • Mutable Property (var): A property that can be modified after initialization.
    • Immutable Property (val): A property that cannot be changed after initialization.
    Defining a Class with Properties:

    In Kotlin, defining properties in a class is straightforward, and Kotlin auto-generates the accessor methods (getter and setter) for you.

    class Person(
        val name: String,
        val isEmployed: Boolean
    )
    • Readable Property: A getter is automatically generated to retrieve the value.
    • Writable Property: Both a getter and a setter are generated for mutable properties.

    Example of a Class in Kotlin:

    class Person(
        val name: String,
        val isEmployed: Boolean
    )
    
    fun main() {
        val person = Person("Alice", true)
        println(person.name)
        println(person.isEmployed)
    }

    Output:

    Alice
    true

    In Kotlin, the constructor is invoked without the new keyword, and instead of explicitly calling getter methods, properties are accessed directly. This approach makes the code more concise and easier to read. The setter for a mutable property works similarly, allowing direct assignment.

    Custom Accessors in Kotlin

    In Kotlin, you can customize the behavior of property accessors by providing your own implementations for the getter and setter methods.

    Example of a Custom Getter:

    class Rectangle(val height: Int, val width: Int) {
        val isSquare: Boolean
            get() = height == width
    }
    
    fun main() {
        val rectangle = Rectangle(41, 43)
        println(rectangle.isSquare)  // Output: false
    }

    In this example, the property isSquare has a custom getter that calculates whether the rectangle is a square. There’s no need for a backing field because the value is computed on demand.

    Key Points:

    • Kotlin automatically generates getters and setters for properties.
    • Immutable properties (val) only have a getter.
    • Mutable properties (var) have both a getter and setter.
    • You can define custom getters and setters if needed.

    Changing the Program

    Here’s a modified version of the program, keeping the same functionality but with different variable names and a different context:

    class Box(val length: Int, val breadth: Int) {
        val isCube: Boolean
            get() = length == breadth
    }
    
    fun main() {
        val box = Box(10, 20)
        println("Is the box a cube? ${box.isCube}")
    }

    Output:

    Is the box a cube? false

    Explanation:

    In this program:

    • A class Box is defined with properties length and breadth.
    • A custom getter for the isCube property checks whether the box has equal sides.
    • In the main function, we create an object of the Box class and check if the box is a cube.

    This approach shows how you can encapsulate data and functionality within a class in Kotlin while providing customized behavior through accessors.

    Constructor

    constructor in Kotlin is a special member function invoked when an object of a class is created, mainly used to initialize variables or properties. Kotlin provides two types of constructors:

    1. Primary Constructor
    2. Secondary Constructor

    A class can have one primary constructor and multiple secondary constructors. The primary constructor is responsible for basic initialization, while secondary constructors allow additional logic or overloads.

    1. Primary Constructor: The primary constructor is defined in the class header and typically initializes class properties. It’s optional, and if no constructor is declared, Kotlin provides a default constructor.

    class Sum(val a: Int, val b: Int) {
        // code
    }

    In cases where no annotations or access modifiers are used, the constructor keyword can be omitted.

    class Sum(val a: Int, val b: Int) {
        // code
    }

    Example of Primary Constructor

    // main function
    fun main() {
        val sum = Sum(5, 6)
        println("The total of 5 and 6 is: ${sum.result}")
    }
    
    // primary constructor
    class Sum(a: Int, b: Int) {
        var result = a + b
    }

    Output:

    The total of 5 and 6 is: 11

    Primary Constructor with Initialization Block

    In Kotlin, the primary constructor can’t contain logic directly. Initialization code must be placed in an init block, which executes when an object is created.

    Example with init Block

    class Person(val name: String) {
        init {
            println("Initialization block running...")
            println("Name is: $name")
        }
    }
    
    fun main() {
        val person = Person("John")
    }

    Output:

    Initialization block running...
    Name is: John

    Default Values in Primary Constructor

    Similar to default parameters in functions, you can define default values for primary constructor parameters.

    class Employee(val id: Int = 101, val name: String = "Unknown") {
        init {
            println("Employee ID: $id, Name: $name")
        }
    }
    
    fun main() {
        val emp1 = Employee(102, "Alice")
        val emp2 = Employee(103)
        val emp3 = Employee()
    }

    Output:

    Employee ID: 102, Name: Alice
    Employee ID: 103, Name: Unknown
    Employee ID: 101, Name: Unknown

    2. Secondary Constructor: secondary constructor is useful for additional logic or creating multiple overloads. It’s defined with the constructor keyword, and you can have multiple secondary constructors.

    Example of Secondary Constructor

    class Add {
        constructor(a: Int, b: Int) {
            val sum = a + b
            println("The sum of $a and $b is: $sum")
        }
    }
    
    fun main() {
        Add(5, 6)
    }

    Output:

    The sum of 5 and 6 is: 11

    Default Values in Primary Constructor

    Similar to default parameters in functions, you can define default values for primary constructor parameters.

    class Employee {
        constructor(id: Int, name: String) {
            println("Employee ID: $id, Name: $name")
        }
    
        constructor(id: Int, name: String, salary: Double) {
            println("Employee ID: $id, Name: $name, Salary: $salary")
        }
    }
    
    fun main() {
        Employee(102, "Alice")
        Employee(103, "Bob", 60000.0)
    }

    Output:

    Employee ID: 102, Name: Alice
    Employee ID: 103, Name: Bob, Salary: 60000.0

    Calling One Secondary Constructor from Another

    A secondary constructor can call another secondary constructor using the this() keyword.

    fun main() {
        Child(101, "John")
    }
    
    open class Parent {
        constructor(id: Int, name: String, salary: Double) {
            println("Parent - ID: $id, Name: $name, Salary: $salary")
        }
    }
    
    class Child : Parent {
        constructor(id: Int, name: String) : super(id, name, 50000.0) {
            println("Child - ID: $id, Name: $name")
        }
    }

    Output:

    Parent - ID: 101, Name: John, Salary: 50000.0
    Child - ID: 101, Name: John

    Modifiers

    In Kotlin, visibility modifiers control access to classes, their members (properties, methods, and nested classes), and constructors. The following visibility modifiers are available:

    1. private: Limits access to the containing class only. A private member cannot be accessed outside the class.
    2. internal: Restricts access to the same module. A module in Kotlin refers to a set of files compiled together.
    3. protected: Allows access within the containing class and its subclasses.
    4. public: The default modifier in Kotlin. A public member is accessible from anywhere in the code.

    These modifiers are used to restrict the accessibility of class members and their setters, ensuring encapsulation. Setters can be modified, but the visibility of getters remains the same as that of the property.

    1. Public Modifier : In Kotlin, the public modifier is the default visibility modifier and is used for members that should be accessible from anywhere in the code. Unlike Java, in Kotlin, if no visibility modifier is specified, it defaults to public.

    // by default public
    class Car {
        var model = "Sedan"
    }
    
    // explicitly public
    public class Truck {
        var capacity = 2000
        fun showCapacity() {
            println("This truck has a capacity of $capacity tons")
        }
    }
    
    fun main() {
        val car = Car()
        println(car.model)  // Accessible anywhere
    
        val truck = Truck()
        truck.showCapacity()  // Accessible anywhere
    }

    Output:

    open class Base {
        open fun greet() {
            println("Hello from Base")
        }
    }
    
    class Derived : Base() {
        override fun greet() {
            println("Hello from Derived")
        }
    }
    
    fun main() {
        val base: Base = Derived()
        base.greet()  // Output: Hello from Derived
    }

    2. Private Modifier : The private modifier restricts access to the containing class or file. Members declared private in a class cannot be accessed from outside the class. In Kotlin, multiple top-level declarations are allowed in the same file, and a private top-level declaration can be accessed by other members within the same file.

    // Accessible only in this file
    private class Plane {
        private val range = 1500
    
        fun showRange() {
            println("The plane has a range of $range miles")
        }
    }
    
    fun main() {
        val plane = Plane()
        plane.showRange() // OK, accessible within the same file
        // println(plane.range) // Error: 'range' is private
    }

    Output:

    The plane has a range of 1500 miles

    3. Internal Modifier: The internal modifier ensures that a class or member is accessible only within the same module. It is useful for controlling visibility across modules but not exposing certain members outside the module.

    internal class Computer {
        internal var brand = "TechBrand"
    
        internal fun showDetails() {
            println("This is a $brand computer")
        }
    }
    
    fun main() {
        val computer = Computer()
        computer.showDetails()  // Accessible within the same module
    }

    4. Protected Modifier: The protected modifier allows access to members within the class and its subclasses but not from outside the class. Unlike Java, in Kotlin, protected members are not accessible to other classes in the same package.

    open class Device {
        protected val batteryLife = 24  // accessible in subclasses
    }
    
    class Smartphone : Device() {
        fun getBatteryLife(): Int {
            return batteryLife  // OK, accessed from subclass
        }
    }
    
    fun main() {
        val phone = Smartphone()
        println("Battery life is: ${phone.getBatteryLife()} hours")
    }

    Output:

    Battery life is: 24 hours
    Overriding Protected Members

    To override a protected member, it must be declared open in the base class. The derived class can then override the member.

    open class Appliance {
        open protected val power = 1500  // Can be overridden
    }
    
    class Microwave : Appliance() {
        override val power = 1200
    
        fun showPower(): Int {
            return power  // Accessing overridden value
        }
    }
    
    fun main() {
        val microwave = Microwave()
        println("Power of the microwave: ${microwave.showPower()} watts")
    }

    Output:

    Power of the microwave: 1200 watts
    Advantages of Visibility Modifiers in Kotlin:

    1. Encapsulation: Visibility modifiers help encapsulate and hide the internal workings of a class, ensuring that only relevant parts are exposed.
    2. Modularity: By controlling member visibility, you can create self-contained modules that are easier to maintain and reuse.
    3. Abstraction: Exposing only necessary details creates a clearer abstraction and makes code easier to manage and debug.

    Disadvantages of Visibility Modifiers in Kotlin:

    1. Increased Complexity: Using multiple visibility levels in a project can make the code more difficult to understand.
    2. Overhead: The compiler may perform additional checks to enforce visibility, leading to minimal performance overhead.

    Inheritance

    Kotlin supports inheritance, allowing you to define a new class based on an existing one. The existing class is referred to as the superclass or base class, while the new class is called the subclass or derived class. The subclass inherits properties and methods from the superclass, and can also add new functionality or override the properties and methods inherited from the superclass.

    Inheritance is a core feature in object-oriented programming that promotes code reusability. It enables a new class to inherit the properties and behaviors of an existing class while also adding or modifying features.

    Syntax of Inheritance:

    open class BaseClass(val x: Int) {
        // Class body
    }
    
    class DerivedClass(x: Int) : BaseClass(x) {
        // Derived class body
    }

    In Kotlin, classes are final by default, meaning they cannot be inherited. To allow inheritance, you need to use the open keyword before the class declaration of the base class.

    Breakdown of Key Components:
    • Superclass (Base Class): This is the class from which properties and methods are inherited. It defines behaviors that can be overridden in its subclasses.
    • Subclass (Derived Class): This is the class that inherits properties and methods from the superclass. The subclass can enhance or modify the behavior of the base class by adding new properties and methods or overriding inherited ones.

    Example of Inheriting Properties and Methods:

    When a class inherits another class, it can use its properties and methods. The subclass can also call methods of the base class via an instance of the derived class.

    open class Base(val name: String) {
        init {
            println("Initialized in Base class")
        }
    
        open val size: Int = name.length.also { println("Size in Base: $it") }
    }
    
    class Derived(name: String, val lastName: String) : Base(name.capitalize().also { println("Base argument: $it") }) {
        init {
            println("Initialized in Derived class")
        }
    
        override val size: Int = (super.size + lastName.length).also { println("Size in Derived: $it") }
    }
    
    fun main() {
        val obj = Derived("john", "doe")
    }

    Output:

    Base argument: John
    Initialized in Base class
    Size in Base: 4
    Initialized in Derived class
    Size in Derived: 7

    Explanation:

    • The base class Base contains an init block and a property size, which gets initialized when an object is created.
    • The derived class Derived inherits Base, overrides the size property, and adds a new property lastName.
    • When we create an object of the Derived class, it initializes both the base and derived classes, and overrides the size property in the derived class.
    Use of Inheritance in a Practical Example:

    Suppose we have different types of employees, like WebDeveloperiOSDeveloper, and AndroidDeveloper. Each has common attributes like name and age, but different specific skills. Using inheritance, we can avoid code duplication by placing common attributes in a base class Employee.

    Without Inheritance:

    If you create separate classes for each type of employee, you would have to repeat the common properties (like name and age) in each class. This leads to redundant code, and if you want to add new properties (like salary), you’d need to modify each class.

    With Inheritance:

    You can create a base class Employee with common properties, and the specific classes WebDeveloperiOSDeveloper, and AndroidDeveloper can inherit from it, adding their unique features.

    Kotlin Program Demonstrating Inheritance:

    // Base class
    open class Employee(val name: String, val age: Int, val salary: Int) {
        init {
            println("Employee: Name = $name, Age = $age, Salary = $salary per month")
        }
    }
    
    // Derived class
    class WebDeveloper(name: String, age: Int, salary: Int) : Employee(name, age, salary) {
        fun developWebsite() {
            println("I develop websites.")
        }
    }
    
    // Derived class
    class AndroidDeveloper(name: String, age: Int, salary: Int) : Employee(name, age, salary) {
        fun developAndroidApp() {
            println("I develop Android apps.")
        }
    }
    
    // Derived class
    class iOSDeveloper(name: String, age: Int, salary: Int) : Employee(name, age, salary) {
        fun developIOSApp() {
            println("I develop iOS apps.")
        }
    }
    
    fun main() {
        val webDev = WebDeveloper("Alice", 28, 4000)
        webDev.developWebsite()
    
        val androidDev = AndroidDeveloper("Bob", 26, 4500)
        androidDev.developAndroidApp()
    
        val iosDev = iOSDeveloper("Charlie", 30, 5000)
        iosDev.developIOSApp()
    }

    Output:

    Employee: Name = Alice, Age = 28, Salary = 4000 per month
    I develop websites.
    Employee: Name = Bob, Age = 26, Salary = 4500 per month
    I develop Android apps.
    Employee: Name = Charlie, Age = 30, Salary = 5000 per month
    I develop iOS apps.
    Inheritance with Primary Constructors:

    If a subclass has a primary constructor, it must initialize the base class constructor using parameters. Below is an example where the base class has two parameters, and the subclass has three parameters.

    // Base class
    open class Person(val name: String, val age: Int) {
        init {
            println("Person's Name: $name, Age: $age")
        }
    }
    
    // Derived class
    class Manager(name: String, age: Int, val salary: Double) : Person(name, age) {
        init {
            println("Manager's Salary: $salary per year")
        }
    }
    
    fun main() {
        Manager("Jane", 35, 120000.0)
    }

    Output:

    Person's Name: Jane, Age: 35
    Manager's Salary: 120000.0 per year
    Secondary Constructors:

    Secondary constructors are useful for providing additional logic or different ways to create objects.

    open class Animal(val name: String) {
        init {
            println("Animal: $name")
        }
    }
    
    class Dog : Animal {
        constructor(name: String, breed: String) : super(name) {
            println("Dog Breed: $breed")
        }
    }
    
    fun main() {
        Dog("Buddy", "Golden Retriever")
    }

    Output:

    Animal: Buddy
    Dog Breed: Golden Retriever
    Calling Secondary Constructors from Another:

    You can call one secondary constructor from another using this().

    class Product {
        constructor(name: String) : this(name, 100) {
            println("Product Name: $name")
        }
    
        constructor(name: String, price: Int) {
            println("Product Name: $name, Price: $price")
        }
    }
    
    fun main() {
        Product("Laptop")
    }

    Output:

    Product Name: Laptop, Price: 100
    Product Name: Laptop

    Interfaces

    In Kotlin, an interface is a collection of abstract methods and properties that define a contract for the classes implementing it. Interfaces allow a class to conform to specific behavior without enforcing the class to inherit an implementation. Unlike abstract classes, interfaces cannot hold state, but they can have methods with default implementations.

    Interfaces in Kotlin serve as custom types that cannot be instantiated on their own. They define behaviors that implementing classes must provide, promoting polymorphism and reusability.

    Defining an Interface:

    You define an interface using the interface keyword, followed by the interface name and a set of abstract methods or properties that any implementing class must fulfill.

    Example:

    interface Machine {
        fun powerOn()
        fun powerOff()
    }
    Implementing an Interface:

    Classes or objects can implement an interface by providing definitions for all its abstract members. To implement an interface, the class name is followed by a colon and the interface name.

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

    Example Demonstrating Interface Implementation:

    interface Machine {
        fun powerOn()
        fun powerOff()
    }
    
    class Computer : Machine {
        override fun powerOn() {
            println("Computer is powered on.")
        }
    
        override fun powerOff() {
            println("Computer is shutting down.")
        }
    }
    
    fun main() {
        val myComputer = Computer()
        myComputer.powerOn()
        myComputer.powerOff()
    }

    Output:

    Computer is powered on.
    Computer is shutting down.
    Default Methods and Parameters in Interfaces:

    Interfaces can also define default implementations for methods and provide default parameter values. This allows classes to inherit functionality from the interface without needing to override every method.

    interface Calculator {
        fun add(a: Int, b: Int = 10)
        fun display() {
            println("This is a default method.")
        }
    }
    
    class CalculatorImpl : Calculator {
        override fun add(a: Int, b: Int) {
            println("The sum is ${a + b}")
        }
    
        override fun display() {
            super.display()
            println("This method has been overridden.")
        }
    }
    
    fun main() {
        val calc = CalculatorImpl()
        calc.add(5)
        calc.display()
    }

    Output:

    The sum is 15
    This is a default method.
    This method has been overridden.
    Properties in Interfaces:

    Just like methods, interfaces can also define properties. Since interfaces cannot maintain state, they either leave properties abstract or provide custom getters.

    interface VehicleProperties {
        val speed: Int
        val type: String
            get() = "Unknown"
    }
    
    class CarProperties : VehicleProperties {
        override val speed: Int = 120
        override val type: String = "Sedan"
    }
    
    fun main() {
        val car = CarProperties()
        println(car.speed)
        println(car.type)
    }

    Output:

    120
    Sedan
    Interface Inheritance:

    Interfaces can also inherit other interfaces, allowing you to build more complex structures by combining multiple interfaces.

    interface Dimensions {
        val length: Double
        val width: Double
    }
    
    interface Shape : Dimensions {
        fun area(): Double
    }
    
    class Rectangle(override val length: Double, override val width: Double) : Shape {
        override fun area(): Double = length * width
    }
    
    fun main() {
        val rect = Rectangle(5.0, 3.0)
        println("Area of rectangle: ${rect.area()}")
    }

    Output:

    Area of rectangle: 15.0
    Multiple Interface Implementation:

    Kotlin allows classes to implement multiple interfaces, which is a form of multiple inheritance. A class must provide implementations for all abstract members of the interfaces it implements.

    interface Speed {
        val maxSpeed: Int
    }
    
    interface Features {
        fun describe()
    }
    
    class SportsCar : Speed, Features {
        override val maxSpeed: Int = 200
    
        override fun describe() {
            println("This is a sports car with a maximum speed of $maxSpeed km/h.")
        }
    }
    
    fun main() {
        val car = SportsCar()
        car.describe()
    }

    Output:

    This is a sports car with a maximum speed of 200 km/h.
    Advantages of Using Interfaces in Kotlin:

    1. Abstraction: Interfaces enable the definition of a common contract for different classes, improving modularity and abstraction.
    2. Polymorphism: Multiple classes can implement the same interface, enabling polymorphic behavior and flexibility.
    3. Code Reusability: Interfaces allow different classes to share common behavior, reducing code duplication and promoting reusability.

    Disadvantages of Using Interfaces in Kotlin:

    1. Limited Implementation: Interfaces cannot maintain state and can only define abstract methods and properties.
    2. Increased Complexity: Implementing multiple interfaces can make code more complex, especially when dealing with large hierarchies.

    Sealed Classes

    Kotlin introduces a special kind of class that does not exist in Java: sealed classes. The purpose of sealed classes is to define a restricted class hierarchy where the set of possible subclasses is known and fixed at compile time. This feature allows more control and type safety when dealing with multiple types. A sealed class defines a group of subclasses within the same file.

    Declaration of Sealed Class

    The syntax to declare a sealed class is straightforward: it uses the sealed keyword before the class definition.

    Syntax:

    sealed class Example

    When a class is marked as sealed, its subclasses must be defined in the same file. A sealed class cannot be instantiated directly because its constructor is protected by default.

    Example of Sealed Class:

    sealed class Shape {
        class Circle(val radius: Double): Shape() {
            fun display() {
                println("Circle with radius $radius")
            }
        }
    
        class Rectangle(val length: Double, val width: Double): Shape() {
            fun display() {
                println("Rectangle with length $length and width $width")
            }
        }
    }
    
    fun main() {
        val circle = Shape.Circle(3.0)
        circle.display()
    
        val rectangle = Shape.Rectangle(4.0, 5.0)
        rectangle.display()
    }

    Output:

    Circle with radius 3.0
    Rectangle with length 4.0 and width 5.0

    Key Points about Sealed Classes:

    1. Subclassing in the Same File: All subclasses of a sealed class must be defined in the same Kotlin file. However, they don’t need to be defined inside the sealed class itself.
    2. Implicitly Abstract: Sealed classes are abstract by default, which means you cannot create an instance of a sealed class directly.

    Example: Defining a Subclass Outside of Sealed Class

    You can define subclasses of a sealed class outside of the sealed class body, but within the same file.

    sealed class Transport
    
    class Car(val brand: String): Transport() {
        fun details() {
            println("Car brand: $brand")
        }
    }
    
    class Bike(val model: String): Transport() {
        fun details() {
            println("Bike model: $model")
        }
    }
    
    fun main() {
        val car = Car("Toyota")
        val bike = Bike("Harley Davidson")
    
        car.details()
        bike.details()
    }
    Sealed Class with when Expression

    The when expression is commonly used with sealed classes because it allows you to handle all possible cases exhaustively, without the need for an else clause.

    sealed class Animal {
        class Dog : Animal()
        class Cat : Animal()
        class Elephant : Animal()
    }
    
    fun animalSound(animal: Animal) {
        when (animal) {
            is Animal.Dog -> println("Barks")
            is Animal.Cat -> println("Meows")
            is Animal.Elephant -> println("Trumpets")
        }
    }
    
    fun main() {
        animalSound(Animal.Dog())
        animalSound(Animal.Cat())
        animalSound(Animal.Elephant())
    }

    Output:

    Barks
    Meows
    Trumpets
    Sealed Class with External Subclass

    Sealed classes can also work with subclasses defined outside their body, but they must reside in the same file.

    sealed class Fruit(val name: String)
    
    class Apple : Fruit("Apple")
    class Banana : Fruit("Banana")
    
    fun fruitInfo(fruit: Fruit) {
        when (fruit) {
            is Apple -> println("${fruit.name} is crunchy.")
            is Banana -> println("${fruit.name} is soft.")
        }
    }
    
    fun main() {
        val apple = Apple()
        val banana = Banana()
    
        fruitInfo(apple)
        fruitInfo(banana)
    }

    Output:

    Apple is crunchy.
    Banana is soft.
    Advantages of Sealed Classes:

    1. Type Safety: Sealed classes allow Kotlin to perform exhaustive when checks at compile-time, ensuring all cases are covered.
    2. Control over Inheritance: Sealed classes restrict subclassing to the same file, giving more control over the hierarchy.
    3. Better Design for Hierarchies: By restricting subclass creation, sealed classes allow for more predictable and structured designs

    Enum Classes

    In programming, enum classes are used to define a set of constants under one type. In Kotlin, enums are much more than just a collection of constants. They can also contain properties, methods, and can implement interfaces. In contrast to Java, where enums are limited, Kotlin treats enums as full-fledged classes.

    Key Features of Kotlin Enums:

    1. Enum constants are objects that can have properties and methods.
    2. Each enum constant behaves like a separate instance of the enum class.
    3. Enum constants improve code readability by assigning meaningful names to values.
    4. You cannot create an instance of an enum class using a constructor.

    Defining an Enum Class:

    The syntax for declaring an enum class starts with the enum keyword, followed by the class name and the constants:

    enum class Season {
        SPRING,
        SUMMER,
        AUTUMN,
        WINTER
    }
    Initializing Enums with Parameters

    Enums in Kotlin can have constructors just like regular classes. These constructors can be used to initialize the constants with specific values.

    Example: Initializing Enum Constants with Parameters

    enum class Animal(val sound: String) {
        DOG("Bark"),
        CAT("Meow"),
        COW("Moo")
    }
    
    fun main() {
        val animalSound = Animal.DOG.sound
        println("The sound a dog makes: $animalSound")
    }
    Properties and Methods of Enum Classes

    Enums in Kotlin have two built-in properties:

    • ordinal: This gives the position of the enum constant, starting from 0.
    • name: This returns the name of the constant.

    They also have two methods:

    • values(): Returns a list of all the constants in the enum.
    • valueOf(): Returns the enum constant corresponding to the input string.

    Example: Enum Class with Properties and Methods

    enum class TrafficLight(val color: String) {
        RED("Red"),
        YELLOW("Yellow"),
        GREEN("Green")
    }
    
    fun main() {
        for (light in TrafficLight.values()) {
            println("${light.ordinal} = ${light.name} with color ${light.color}")
        }
    
        println(TrafficLight.valueOf("GREEN"))
    }

    Output:

    0 = RED with color Red
    1 = YELLOW with color Yellow
    2 = GREEN with color Green
    GREEN
    Adding Methods to Enum Classes

    Enum classes can have their own properties and methods, allowing you to provide behavior for each constant.

    Example: Enum Class with Properties and Companion Object

    enum class Days(val isHoliday: Boolean = false) {
        SUNDAY(true),
        MONDAY,
        TUESDAY,
        WEDNESDAY,
        THURSDAY,
        FRIDAY,
        SATURDAY(true);
    
        companion object {
            fun isWeekend(day: Days): Boolean {
                return day == SUNDAY || day == SATURDAY
            }
        }
    }
    
    fun main() {
        for (day in Days.values()) {
            println("${day.name} is a holiday: ${day.isHoliday}")
        }
    
        val today = Days.FRIDAY
        println("Is today a weekend? ${Days.isWeekend(today)}")
    }

    Output:

    SUNDAY is a holiday: true
    MONDAY is a holiday: false
    TUESDAY is a holiday: false
    WEDNESDAY is a holiday: false
    THURSDAY is a holiday: false
    FRIDAY is a holiday: false
    SATURDAY is a holiday: true
    Is today a weekend? false
    Enum Constants as Anonymous Classes

    Each enum constant can override methods and behave like an anonymous class. This feature allows each constant to have different behavior.

    Example: Enum with Anonymous Classes

    enum class Device(val type: String) {
        LAPTOP("Portable Device") {
            override fun description() {
                println("A laptop is a ${this.type}")
            }
        },
        MOBILE("Handheld Device") {
            override fun description() {
                println("A mobile phone is a ${this.type}")
            }
        };
    
        abstract fun description()
    }
    
    fun main() {
        Device.LAPTOP.description()
        Device.MOBILE.description()
    }

    Output:

    A laptop is a Portable Device
    A mobile phone is a Handheld Device
    Enum Class with when Expression

    The when expression works very well with enum classes since all the possible values are already known. This eliminates the need for the else clause.

    Example: Using Enum Class with when Expression

    enum class Weather {
        SUNNY,
        CLOUDY,
        RAINY,
        WINDY
    }
    
    fun describeWeather(weather: Weather) {
        when (weather) {
            Weather.SUNNY -> println("The weather is sunny!")
            Weather.CLOUDY -> println("It's cloudy today.")
            Weather.RAINY -> println("It's raining outside.")
            Weather.WINDY -> println("Hold onto your hats, it's windy!")
        }
    }
    
    fun main() {
        describeWeather(Weather.SUNNY)
        describeWeather(Weather.RAINY)
    }

    Output:

    The weather is sunny!
    It's raining outside.
    Advantages of Using Enum Classes:

    1. Code Readability: Enum classes make the code more readable by assigning meaningful names to constants.
    2. Type Safety: They restrict the possible values a variable can have, ensuring type safety.
    3. Maintainability: Enum classes help organize related constants and behaviors in one place, improving maintainability.

    Extension Functions

    In Kotlin, you have the ability to enhance existing classes with new functionality without inheriting from them. This is made possible through extension functions. An extension function allows you to add methods to a class without modifying its source code. You define an extension function by appending the function to the class name, like so:

    package kotlin1.com.programmingKotlin.chapter1
    
    // A simple class demonstrating an extension function
    
    class Rectangle(val length: Double, val width: Double) {
        // A member function that calculates the area of the rectangle
        fun area(): Double {
            return length * width
        }
    }
    fun main() {
        // Extension function to calculate the perimeter of the rectangle
        fun Rectangle.perimeter(): Double {
            return 2 * (length + width)
        }
    
        // Creating an instance of Rectangle
        val rect = Rectangle(5.0, 4.0)
        // Calling the member function
        println("Area of the rectangle is ${rect.area()}")
        // Calling the extension function
        println("Perimeter of the rectangle is ${rect.perimeter()}")
    }

    Output:

    Area of the rectangle is 20.0
    Perimeter of the rectangle is 18.0

    Kotlin not only allows extension of user-defined classes but also library classes. You can easily extend classes from the Kotlin or Java standard libraries with custom methods.

    Example: Extending a Standard Library Class

    fun main() {
        // Extension function defined for String type
        fun String.lastChar(): Char {
            return this[this.length - 1]
        }
    
        println("Hello".lastChar()) // Output: 'o'
        println("World".lastChar()) // Output: 'd'
    }

    Generics

    In Kotlin, you have the ability to enhance existing classes with new functionality without inheriting from them. This is made possible through extension functions. An extension function allows you to add methods to a class without modifying its source code. You define an extension function by appending the function to the class name, like so:

    package kotlin1.com.programmingKotlin.chapter1
    
    // A simple class demonstrating an extension function
    
    class Rectangle(val length: Double, val width: Double) {
        // A member function that calculates the area of the rectangle
        fun area(): Double {
            return length * width
        }
    }
    fun main() {
        // Extension function to calculate the perimeter of the rectangle
        fun Rectangle.perimeter(): Double {
            return 2 * (length + width)
        }
    
        // Creating an instance of Rectangle
        val rect = Rectangle(5.0, 4.0)
        // Calling the member function
        println("Area of the rectangle is ${rect.area()}")
        // Calling the extension function
        println("Perimeter of the rectangle is ${rect.perimeter()}")
    }

    Output:

    Area of the rectangle is 20.0
    Perimeter of the rectangle is 18.0
    Extending Library Classes

    Kotlin not only allows extension of user-defined classes but also library classes. You can easily extend classes from the Kotlin or Java standard libraries with custom methods.

    Example: Extending a Standard Library Class

    fun main() {
        // Extension function defined for String type
        fun String.lastChar(): Char {
            return this[this.length - 1]
        }
    
        println("Hello".lastChar()) // Output: 'o'
        println("World".lastChar()) // Output: 'd'
    }
    Static Resolution of Extensions

    It’s important to understand that extension functions are resolved statically, meaning the actual type of the object does not influence which extension function is called.

    Example: Static Resolution of Extensions

    open class Animal(val sound: String)
    
    class Dog : Animal("Bark")
    
    fun Animal.makeSound(): String {
        return "Animal sound: $sound"
    }
    
    fun Dog.makeSound(): String {
        return "Dog sound: $sound"
    }
    
    fun displaySound(animal: Animal) {
        println(animal.makeSound())
    }
    
    fun main() {
        displaySound(Dog())  // Will call Animal's makeSound(), not Dog's
    }

    Output:

    Animal sound: Bark

    Here, although the object passed is of type Dog, the extension function for Animal is called because extensions are statically resolved.

    Nullable Receiver in Extension Functions

    Kotlin allows you to define extension functions that can be invoked on nullable types.

    Example: Extension Function with Nullable Receiver

    class User(val name: String)
    
    fun User?.greet() {
        if (this == null) {
            println("No user found")
        } else {
            println("Hello, ${this.name}")
        }
    }
    
    fun main() {
        val user: User? = User("John")
        user.greet()  // Output: Hello, John
    
        val noUser: User? = null
        noUser.greet()  // Output: No user found
    }
    Companion Object Extensions

    In Kotlin, if a class has a companion object, you can also extend the companion object with new functions or properties.

    Example: Extending Companion Object

    class Utility {
        companion object {
            fun show() {
                println("Companion object method in Utility")
            }
        }
    }
    
    // Extension function for companion object
    fun Utility.Companion.printInfo() {
        println("Extended companion object function")
    }
    
    fun main() {
        Utility.show()
        Utility.printInfo()
    }

    Output:

    Companion object method in Utility
    Extended companion object function
    Advantages of Extension Functions

    1. Code Reusability: By extending existing classes, you can avoid modifying original classes or creating new classes just for the purpose of adding more functionality.
    2. Cleaner Code: Instead of creating utility methods, you can directly add functions to the class that improves readability.
    3. No Inheritance Needed: You don’t need to inherit the class to add more methods

  • Collections

    In Kotlin, collections are a key feature used to store and manage groups of data. Kotlin offers several types of collections, such as ListsSetsMapsArrays, and Sequences, each with distinct characteristics for handling data efficiently.

    Types of Kotlin Collections

    1. List: An ordered collection that allows duplicate elements. Elements can be accessed by their index.
    2. Set: An unordered collection that holds unique elements, ensuring no duplicates.
    3. Map: A collection of key-value pairs where each key is unique, but values can be duplicated.
    4. Array: A fixed-size collection of elements of the same type.
    5. Sequence: A lazily evaluated collection that can be processed step-by-step.

    Example of Using a List in Kotlin:

    val cities = listOf("New York", "London", "Paris", "Tokyo")
    
    // Access elements
    println("First city: ${cities[0]}")
    println("Last city: ${cities.last()}")
    
    // Iterate over the list
    for (city in cities) {
        println(city)
    }
    
    // Filter the list
    val filteredCities = cities.filter { it.startsWith("L") }
    println("Filtered cities: $filteredCities")

    Output:

    First city: New York
    Last city: Tokyo
    New York
    London
    Paris
    Tokyo
    Filtered cities: [London]

    Explanation: In this example, we create an immutable list of fruits, access elements, iterate over the list, and filter elements that start with the letter “a”.

    ArrayList

    Explanation: In this example, we create an immutable list of fruits, access elements, iterate over the list, and filter elements that start with the letter “a”.

    Types of Collections in Kotlin

    1. Immutable Collections : Immutable collections are read-only and cannot be modified after creation. Some common functions include:

    • listOf()setOf(), and mapOf().

    Example of Immutable List:

    val immutableList = listOf("John", "Sarah", "Emma")
    for (person in immutableList) {
        println(person)
    }

    Output:

    John
    Sarah
    Emma

    Example of Immutable Set:

    val immutableSet = setOf(3, 5, 3, "Alice", "Bob")
    for (item in immutableSet) {
        println(item)
    }

    Output:

    3
    5
    Alice
    Bob

    Example of Immutable Map:

    val immutableMap = mapOf(1 to "Alice", 2 to "Bob", 3 to "Charlie")
    for (key in immutableMap.keys) {
        println(immutableMap[key])
    }

    Output:

    Alice
    Bob
    Charlie

    Key Characteristics of Immutable Collections:

    • Immutable lists, sets, and maps are fixed, and elements cannot be added, removed, or modified.

    2. Mutable Collections : Mutable collections support both read and write operations, allowing modification of the collection. Common methods include:

    • mutableListOf()mutableSetOf()mutableMapOf().

    Example of Mutable List:

    val mutableList = mutableListOf("John", "Sarah", "Emma")
    mutableList[1] = "Mike"
    mutableList.add("Lisa")
    for (person in mutableList) {
        println(person)
    }

    Output:

    John
    Mike
    Emma
    Lisa

    Example of Mutable Set:

    val mutableSet = mutableSetOf(8, 12)
    mutableSet.add(5)
    mutableSet.add(10)
    for (item in mutableSet) {
        println(item)
    }

    Output:

    8
    12
    5
    10

    Example of Mutable Map:

    val mutableMap = mutableMapOf(1 to "Alice", 2 to "Bob", 3 to "Charlie")
    mutableMap[1] = "Dave"
    mutableMap[4] = "Eve"
    for (value in mutableMap.values) {
        println(value)
    }

    Output:

    Dave
    Bob
    Charlie
    Eve
    Advantages of Kotlin Collections:
    • Improved Readability: The use of collections makes code more readable by abstracting complex operations on data.
    • Efficient Memory Management: Collections handle memory automatically, reducing the need for manual intervention.
    • Efficient Data Operations: Kotlin’s collections are designed to work efficiently with large datasets.
    • Type Safety: Collections enforce type safety, ensuring that only the correct type of data can be stored.
    Disadvantages of Kotlin Collections:
    • Performance Overhead: Using collections can add overhead, particularly when compared to working directly with primitive types or arrays.
    • Increased Memory Usage: Larger collections may consume more memory than necessary for simple operations.
    • Additional Complexity: In some cases, collections may add unnecessary complexity when performing advanced operations on data.

    The ArrayList class in Kotlin is a dynamic array that allows you to increase or decrease its size as needed. It provides both read and write operations and allows duplicate elements. Since it is not synchronized, ArrayList is not thread-safe, but it is efficient for single-threaded operations.

    Key Features of ArrayList:
    • Dynamic resizing: The array size grows or shrinks as required.
    • Read/write functionality: You can modify elements after creation.
    • Non-synchronized: It is not thread-safe, but efficient in single-threaded environments.
    • Duplicate elements: It allows duplicates, unlike Set.
    • Index-based access: You can access and manipulate elements via their index.
    Constructors

    1. ArrayList<E>(): Creates an empty ArrayList.
    2. ArrayList(capacity: Int): Creates an ArrayList of specified capacity.
    3. ArrayList(elements: Collection<E>): Fills an ArrayList with elements from a collection.

    Important Methods

    1. add(index: Int, element: E): Inserts an element at the specified index.

    Example:

    fun main() {
        val arrayList = ArrayList<String>()
        arrayList.add("Lion")
        arrayList.add("Tiger")
    
        println("Array list --->")
        for (item in arrayList) println(item)
    
        arrayList.add(1, "Elephant")
        println("Array list after insertion --->")
        for (item in arrayList) println(item)
    }

    Output:

    Array list --->
    Lion
    Tiger
    Array list after insertion --->
    Lion
    Elephant
    Tiger

    2. addAll(index: Int, elements: Collection<E>) : Adds all elements from a specified collection to the current list.

    Example:

    fun main() {
        val arrayList = ArrayList<String>()
        arrayList.add("Red")
        arrayList.add("Blue")
    
        val additionalColors = arrayListOf("Green", "Yellow")
    
        println("Original ArrayList -->")
        for (item in arrayList) println(item)
    
        arrayList.addAll(1, additionalColors)
        println("ArrayList after adding new colors -->")
        for (item in arrayList) println(item)
    }

    Output:

    Original ArrayList -->
    Red
    Blue
    ArrayList after adding new colors -->
    Red
    Green
    Yellow
    Blue

    3. get(index: Int): Returns the element at the specified index.

    Example:

    fun main() {
        val arrayList = ArrayList<Int>()
        arrayList.add(100)
        arrayList.add(200)
        arrayList.add(300)
    
        println("All elements:")
        for (item in arrayList) print("$item ")
    
        println("\nElement at index 1: ${arrayList.get(1)}")
    }

    Output:

    All elements:
    100 200 300
    Element at index 1: 200

    4. set(index: Int, element: E):Replaces the element at the specified index with the given element.

    Example:

    fun main() {
        val arrayList = ArrayList<String>()
        arrayList.add("Dog")
        arrayList.add("Cat")
        arrayList.add("Rabbit")
    
        println("Before update:")
        for (item in arrayList) println(item)
    
        arrayList.set(2, "Horse")
        println("After update:")
        for (item in arrayList) println(item)
    }

    Output:

    Before update:
    Dog
    Cat
    Rabbit
    After update:
    Dog
    Cat
    Horse

    5. indexOf(element: E): Returns the index of the first occurrence of the specified element, or -1 if the element is not found.

    Example:

    fun main() {
        val arrayList = ArrayList<String>()
        arrayList.add("Apple")
        arrayList.add("Banana")
        arrayList.add("Apple")
    
        for (item in arrayList) print("$item ")
        println("\nFirst occurrence of 'Apple': ${arrayList.indexOf("Apple")}")
    }

    Output:

    Apple Banana Apple
    First occurrence of 'Apple': 0

    6. remove(element: E): Removes the first occurrence of the specified element.

    Example:

    fun main() {
        val arrayList = ArrayList<String>()
        arrayList.add("Car")
        arrayList.add("Bike")
        arrayList.add("Bus")
    
        println("Before removal:")
        for (item in arrayList) println(item)
    
        arrayList.remove("Bike")
        println("After removal:")
        for (item in arrayList) println(item)
    }

    Output:

    Before removal:
    Car
    Bike
    Bus
    After removal:
    Car
    Bus

    7. clear():Removes all elements from the ArrayList.

    Example:

    fun main() {
        val arrayList = ArrayList<Int>()
        arrayList.add(1)
        arrayList.add(2)
        arrayList.add(3)
        arrayList.add(4)
    
        println("Before clearing:")
        for (item in arrayList) print("$item ")
    
        arrayList.clear()
        println("\nArrayList size after clear: ${arrayList.size}")
    }

    Output:

    Before clearing:
    1 2 3 4
    ArrayList size after clear: 0

    Full Example Using ArrayList:

    fun main() {
        val list = arrayListOf(5, 10, 15)
    
        println("Initial list: $list")
    
        // Add elements to the list
        list.add(20)
        list.add(1, 25) // Adds 25 at index 1
    
        println("After adding elements: $list")
    
        // Remove elements from the list
        list.remove(10)
        list.removeAt(0) // Removes element at index 0
    
        println("After removing elements: $list")
    
        // Update elements in the list
        list[1] = 30
    
        println("After updating elements: $list")
    
        // Access elements in the list
        val first = list[0]
        val last = list.last()
    
        println("First element: $first")
        println("Last element: $last")
    
        // Iterate over the list
        for (element in list) {
            println(element)
        }
    }

    Output:

    Initial list: [5, 10, 15]
    After adding elements: [5, 25, 10, 15, 20]
    After removing elements: [25, 15, 20]
    After updating elements: [25, 30, 20]
    First element: 25
    Last element: 20
    25
    30
    20

    listOf()

    In Kotlin, listOf() is used to create immutable lists, meaning once the list is created, its elements cannot be modified. The listOf() function takes a variable number of arguments and returns a new list containing those elements.

    Example:

    val numbers = listOf(10, 20, 30, 40, 50)

    In this example, the list numbers is immutable, and contains five integers. Since the list is immutable, you cannot change its contents once it is created.

    Accessing Elements in a List:

    Elements can be accessed using indices.

    Example:

    val firstNumber = numbers[0]  // returns 10
    val lastNumber = numbers[numbers.size - 1]  // returns 50

    In this example, we use indexing to access the first and last elements of the list. Note that indexing starts at 0.

    Iterating Over a List:

    You can iterate over the list using a for loop or other iteration methods.

    Example:

    for (number in numbers) {
        println(number)
    }

    This will print each element of the numbers list to the console.

    Advantages of Using listOf() in Kotlin:

    1. Immutability: Once created, the list cannot be modified, which makes it safer for multi-threaded environments.
    2. Type Safety: Since Kotlin is statically typed, the elements of the list will always be of the same type.
    3. Convenience: You can easily create a list with multiple elements using a single function.

    Disadvantages of Using listOf():

    1. Immutability: If you need to modify the list during runtime, you should use mutableListOf() instead.
    2. Performance Overhead: Since immutable lists cannot be modified, operations that modify the list (like adding or removing elements) require creating new lists, which can be inefficient.
    3. Limited Functionality: listOf() does not support operations like adding or removing elements. For these, you need a mutable list.

    Kotlin Program with listOf() Containing Integers:

    val set = setOf("Apple", "Banana", "Cherry", "Apple")
    println(set)  // Output: [Apple, Banana, Cherry]
    
    // set.add("Date")  // Error: Cannot add element to immutable set

    Output:

    List size: 3
    Index of 22: 1
    Element at index 2: 33
    Indexing Elements of a List:

    In Kotlin, elements of a list are indexed starting from 0.

    Example:

    fun main() {
        val numbers = listOf(5, 10, 15, 20, 25, 30, 35)
    
        val firstElement = numbers[0]
        println("First element: $firstElement")
    
        val seventhElement = numbers[6]
        println("Element at index 6: $seventhElement")
    
        val firstIndex = numbers.indexOf(5)
        println("Index of first occurrence of 5: $firstIndex")
    
        val lastIndex = numbers.lastIndexOf(5)
        println("Last index of 5: $lastIndex")
    
        val lastIndexOverall = numbers.lastIndex
        println("Last index of the list: $lastIndexOverall")
    }

    Output:

    First element: 5
    Element at index 6: 35
    Index of first occurrence of 5: 0
    Last index of 5: 0
    Last index of the list: 6
    Retrieving First and Last Elements:

    You can retrieve the first and last elements of the list without using the get() method.

    Example:

    fun main() {
        val numbers = listOf(3, 6, 9, 12, 15)
        println("First element: ${numbers.first()}")
        println("Last element: ${numbers.last()}")
    }

    Output:

    First element: 3
    Last element: 15
    List Iteration Methods:

    Kotlin provides several ways to iterate over lists.

    Example:

    fun main() {
        val fruits = listOf("Apple", "Banana", "Cherry", "Date", "Elderberry")
    
        // Method 1: Simple for loop
        for (fruit in fruits) {
            print("$fruit, ")
        }
        println()
    
        // Method 2: Using index
        for (i in fruits.indices) {
            print("${fruits[i]} ")
        }
        println()
    
        // Method 3: Using forEachIndexed
        fruits.forEachIndexed { index, fruit -> println("fruits[$index] = $fruit") }
    
        // Method 4: Using ListIterator
        val iterator: ListIterator<String> = fruits.listIterator()
        while (iterator.hasNext()) {
            val fruit = iterator.next()
            print("$fruit ")
        }
        println()
    }

    Output:

    Apple, Banana, Cherry, Date, Elderberry,
    Apple Banana Cherry Date Elderberry
    fruits[0] = Apple
    fruits[1] = Banana
    fruits[2] = Cherry
    fruits[3] = Date
    fruits[4] = Elderberry
    Apple Banana Cherry Date Elderberry
    Sorting Elements in a List:

    You can sort the elements in ascending or descending order using sorted() and sortedDescending().

    Example:

    fun main() {
        val numbers = listOf(42, 12, 7, 99, 23, 1)
    
        val ascendingOrder = numbers.sorted()
        println("Sorted in ascending order: $ascendingOrder")
    
        val descendingOrder = numbers.sortedDescending()
        println("Sorted in descending order: $descendingOrder")
    }

    Output:

    Sorted in ascending order: [1, 7, 12, 23, 42, 99]
    Sorted in descending order: [99, 42, 23, 12, 7, 1]
    contains() and containsAll() Functions:

    These methods check if a list contains certain elements.

    Example:

    fun main() {
        val numbers = listOf(3, 5, 7, 9, 11)
    
        val containsSeven = numbers.contains(7)
        if (containsSeven) {
            println("The list contains 7")
        } else {
            println("The list does not contain 7")
        }
    
        val containsThreeAndTen = numbers.containsAll(listOf(3, 10))
        if (containsThreeAndTen) {
            println("The list contains 3 and 10")
        } else {
            println("The list does not contain 3 and 10")
        }
    }

    Output:

    The list contains 7
    The list does not contain 3 and 10
    Kotlin Set Interface

    The Set interface in Kotlin represents an unordered collection of unique elements. Sets are particularly useful when you need to store data without duplicates. Kotlin supports two types of sets:

    1. Immutable Sets: These are created using setOf() and only support read-only operations.
    2. Mutable Sets: These are created using mutableSetOf() and support both read and write operations.

    Syntax for setOf():

    fun <T> setOf(vararg elements: T): Set<T>

    1. Immutable Set Using setOf(): This function creates an immutable set that can hold any number of elements of the same type, and it returns a set that does not contain duplicate elements.

    Example:

    fun main() {
        val setA = setOf("Kotlin", "Java", "Python")
        val setB = setOf('A', 'B', 'C')
        val setC = setOf(10, 20, 30, 40)
    
        // Traverse through the sets
        for (item in setA) print(item + " ")
        println()
        for (item in setB) print(item + " ")
        println()
        for (item in setC) print(item.toString() + " ")
    }

    Output:

    Kotlin Java Python
    A B C
    10 20 30 40

    2. Mutable Sets in Kotlin (using mutableSetOf()): mutable set is a collection that allows modifications after it is created. You can add, remove, or update elements in a mutable set. This is particularly useful when you need a set with dynamic content.

    Example:

    val colors = mutableSetOf("Red", "Green", "Blue")
    
    // Adding elements
    colors.add("Yellow")
    println(colors) // Output: [Red, Green, Blue, Yellow]
    
    // Removing elements
    colors.remove("Green")
    println(colors) // Output: [Red, Blue, Yellow]
    
    // Checking for an element
    println("Contains Blue? ${"Blue" in colors}") // Output: Contains Blue? true
    
    // Adding duplicate elements
    colors.add("Red") // Ignored, as "Red" already exists
    println(colors) // Output: [Red, Blue, Yellow]
    Set Indexing

    You can access elements at specific indexes in a set using functions like elementAt()indexOf(), and lastIndexOf().

    Example: Using Index in Set

    fun main() {
        val players = setOf("Virat", "Smith", "Root", "Kane", "Rohit")
    
        println("Element at index 2: " + players.elementAt(2))
        println("Index of 'Smith': " + players.indexOf("Smith"))
        println("Last index of 'Rohit': " + players.lastIndexOf("Rohit"))
    }

    Output:

    Element at index 2: Root
    Index of 'Smith': 1
    Last index of 'Rohit': 4
    Set First and Last Elements

    You can retrieve the first and last elements using first() and last() functions.

    Example: First and Last Element in Set

    fun main() {
        val elements = setOf(1, 2, 3, "A", "B", "C")
    
        println("First element: " + elements.first())
        println("Last element: " + elements.last())
    }

    Output:

    First element: 1
    Last element: C
    Set Basic Functions

    Basic functions like count()max()min()sum(), and average() help perform arithmetic operations on sets of numbers.

    Example: Basic Functions with Set

    fun main() {
        val numbers = setOf(5, 10, 15, 20, 25)
    
        println("Count of elements: " + numbers.count())
        println("Maximum element: " + numbers.maxOrNull())
        println("Minimum element: " + numbers.minOrNull())
        println("Sum of elements: " + numbers.sum())
        println("Average of elements: " + numbers.average())
    }

    Output:

    Count of elements: 5
    Maximum element: 25
    Minimum element: 5
    Sum of elements: 75
    Average of elements: 15.0
    Checking Elements with contains() and containsAll()

    The contains() function checks if a set contains a specific element, and containsAll() checks if a set contains all specified elements.

    Example: Using contains() and containsAll()

    fun main() {
        val captains = setOf("Kohli", "Smith", "Root", "Rohit")
    
        val element = "Rohit"
        println("Does the set contain $element? " + captains.contains(element))
    
        val elementsToCheck = setOf("Kohli", "Smith")
        println("Does the set contain all elements? " + captains.containsAll(elementsToCheck))
    }

    Output:

    Does the set contain Rohit? true
    Does the set contain all elements? true

    hashSetOf()

    hashSetOf creates a mutable set of unique elements backed by a hash table.

    Example:

    fun main() {
        val emptySetA = setOf<String>()
        val emptySetB = setOf<Int>()
    
        println("Is setA empty? " + emptySetA.isEmpty())
        println("Are both sets equal? " + (emptySetA == emptySetB))
    }

    Output:

    Is setA empty? true
    Are both sets equal? true

    Example: Set Operations Using setOf()

    fun main() {
        val fruits = setOf("Apple", "Banana", "Cherry")
    
        val containsApple = fruits.contains("Apple")
        val containsOrange = fruits.contains("Orange")
    
        println("Fruits: $fruits")
        println("Contains Apple: $containsApple")
        println("Contains Orange: $containsOrange")
    
        for (fruit in fruits) {
            println(fruit)
        }
    }

    Output:

    Fruits: [Apple, Banana, Cherry]
    Contains Apple: true
    Contains Orange: false
    Apple
    Banana
    Cherry
    Advantages of setOf() in Kotlin:
    • Immutability: The contents cannot be modified once created, making the set safe for use in multithreaded environments.
    • No Duplicates: The set automatically ensures that no duplicates are present, which reduces bugs.
    • Convenience: Creating a set with setOf() is simple and concise.
    Disadvantages of setOf() in Kotlin:
    • Immutability: You cannot modify the set after its creation, which may limit its use in situations where you need to add or remove elements dynamically.
    • No Advanced Features: For more advanced operations (like adding or removing elements), you will need to use a mutable set via mutableSetOf().

    mapOf()

    In Kotlin, a Map is a collection that stores data as key-value pairs. Each pair associates a unique key with a corresponding value, and the keys in a map must be distinct. A map can hold only one value per key.

    Kotlin distinguishes between immutable maps, which are created using mapOf(), and mutable maps, created using mutableMapOf(). An immutable map is read-only, while a mutable map allows both reading and writing.

    Syntax:

    fun <K, V> mapOf(vararg pairs: Pair<K, V>): Map<K, V>

    In each pair, the first value represents the key, and the second value represents the corresponding value. If multiple pairs have the same key, the map will store only the last value associated with that key. The entries in the map are traversed in the specified order.

    Example: Using mapOf()

    fun main() {
        // Creating a map of integer to string
        val map = mapOf(10 to "Apple", 20 to "Banana", 30 to "Cherry")
        // Printing the map
        println(map)
    }

    Output:

    {10=Apple, 20=Banana, 30=Cherry}

    Accessing Map Keys, Values, and Entries

    fun main() {
        // Creating a map of integers to strings
        val fruits = mapOf(1 to "Mango", 2 to "Orange", 3 to "Peach", 4 to "Grape")
    
        println("Map Entries: $fruits")
        println("Map Keys: ${fruits.keys}")
        println("Map Values: ${fruits.values}")
    }

    Output:

    Map Entries: {1=Mango, 2=Orange, 3=Peach, 4=Grape}
    Map Keys: [1, 2, 3, 4]
    Map Values: [Mango, Orange, Peach, Grape]
    Determining the Map Size

    You can determine the size of a map using either the size property or the count() method:

    fun main() {
        val countries = mapOf(1 to "USA", 2 to "Canada", 3 to "Japan", 4 to "Germany")
    
        // Method 1
        println("The size of the map is: ${countries.size}")
    
        // Method 2
        println("The size of the map is: ${countries.count()}")
    }

    Output:

    The size of the map is: 4
    The size of the map is: 4
    Creating an Empty Map

    You can create an empty map using mapOf():

    fun main() {
        // Creating an empty map
        val emptyMap = mapOf<String, Int>()
    
        println("Entries: ${emptyMap.entries}")
        println("Keys: ${emptyMap.keys}")
        println("Values: ${emptyMap.values}")
    }

    Output:

    Entries: []
    Keys: []
    Values: []
    Retrieving Values from a Map

    Here are several ways to retrieve values from a map:

    fun main() {
        val capitals = mapOf(1 to "Tokyo", 2 to "Paris", 3 to "London", 4 to "Berlin")
    
        // Method 1
        println("The capital at rank #1 is: ${capitals[1]}")
    
        // Method 2
        println("The capital at rank #3 is: ${capitals.getValue(3)}")
    
        // Method 3
        println("The capital at rank #4 is: ${capitals.getOrDefault(4, "Unknown")}")
    
        // Method 4
        val city = capitals.getOrElse(2) { "No City" }
        println(city)
    }

    Output:

    The capital at rank #1 is: Tokyo
    The capital at rank #3 is: London
    The capital at rank #4 is: Berlin
    Paris
    Checking if a Map Contains a Key or Value

    You can check if a map contains a specific key or value using the containsKey() and containsValue() methods:

    fun main() {
        val numbers = mapOf("one" to 1, "two" to 2, "three" to 3)
    
        val key = "two"
        if (numbers.containsKey(key)) {
            println("Yes, the map contains key $key")
        } else {
            println("No, it doesn't contain key $key")
        }
    
        val value = 4
        if (numbers.containsValue(value)) {
            println("Yes, the map contains value $value")
        } else {
            println("No, it doesn't contain value $value")
        }
    }

    Output:

    Yes, the map contains key two
    No, it doesn't contain value 4
    Handling Duplicate Keys

    If two pairs have the same key, the map will retain only the last value:

    fun main() {
        // Two pairs with the same key
        val map = mapOf(1 to "Alpha", 2 to "Beta", 1 to "Gamma")
        println("Entries in the map: ${map.entries}")
    }

    Output:

    Entries in the map: [1=Gamma, 2=Beta]
    Advantages of mapOf():
    • Provides a straightforward way to create a key-value mapping.
    • Read-only maps are safe for use across functions and threads without the risk of concurrent modification.
    Disadvantages of mapOf():
    • Immutable, meaning the map cannot be altered after creation. For modifiable maps, mutableMapOf() should be used.
    • May become inefficient with a large number of entries, as lookup times can degrade. For more frequent lookups, a data structure like a hash table or binary tree may be more appropriate.

    HashMap

    Kotlin HashMap is a collection that holds key-value pairs, implemented using a hash table. It implements the MutableMap interface, meaning that you can modify its contents. Each key within a HashMap is unique, and the map will store only one value per key. The HashMap is declared as HashMap<key, value> or HashMap<K, V>. This hash table-based implementation does not guarantee the order of the keys, values, or entries in the map.

    Constructors of Kotlin HashMap Class:

    Kotlin provides four constructors for the HashMap class, all of which have a public access modifier:

    • HashMap(): The default constructor, which creates an empty HashMap.
    • HashMap(initialCapacity: Int, loadFactor: Float = 0f): This constructor initializes a HashMap with a specified capacity and an optional load factor. If neither is provided, the default values are used.
    • HashMap(initialCapacity: Int): Constructs a HashMap with the specified initial capacity. If the capacity isn’t used, it is ignored.
    • HashMap(original: Map<out K, V>): Creates a new HashMap instance that contains the same mappings as the provided map.
    fun main(args: Array<String>) {
        // Define an empty HashMap with <String, Int>
        val heroesMap: HashMap<String, Int> = HashMap()
    
        // Printing the empty HashMap
        printMap(heroesMap)
    
        // Adding elements to the HashMap using put() function
        heroesMap["CaptainAmerica"] = 1940
        heroesMap["Hulk"] = 5000
        heroesMap["BlackWidow"] = 1000
        heroesMap["DoctorStrange"] = 1500
        heroesMap["AntMan"] = 700
    
        // Printing the non-empty HashMap
        printMap(heroesMap)
    
        // Using Kotlin's print function to get the same result
        println("heroesMap: $heroesMap\n")
    
        // Traversing the HashMap using a for loop
        for (key in heroesMap.keys) {
            println("Element at key $key : ${heroesMap[key]}")
        }
    
        // Creating another HashMap with the previous heroesMap object
        val copyOfHeroesMap: HashMap<String, Int> = HashMap(heroesMap)
    
        println("\nCopy of HeroesMap:")
        for (key in copyOfHeroesMap.keys) {
            println("Element at key $key : ${copyOfHeroesMap[key]}")
        }
    
        // Clearing the HashMap
        println("heroesMap.clear()")
        heroesMap.clear()
    
        println("After Clearing: $heroesMap")
    }
    
    // Function to print the HashMap
    fun printMap(map: HashMap<String, Int>) {
        if (map.isEmpty()) {
            println("The HashMap is empty")
        } else {
            println("HashMap: $map")
        }
    }

    Output:

    val hashMap = HashMap<Int, String>()
    hashMap[1] = "Apple"
    hashMap[2] = "Banana"
    hashMap[3] = "Cherry"
    
    println(hashMap)  // Output: {1=Apple, 2=Banana, 3=Cherry}
    
    hashMap[4] = "Date"
    println(hashMap)  // Output: {1=Apple, 2=Banana, 3=Cherry, 4=Date}
    
    hashMap.remove(2)
    println(hashMap)  // Output: {1=Apple, 3=Cherry, 4=Date}
    Explanation of Changes:

    1. Updated Key-Value Pairs: Changed the keys to other superhero names like “CaptainAmerica”, “Hulk”, etc., and assigned different values.
    2. Renamed Variables: Renamed the hashMap variable to heroesMap for better readability, and secondHashMap to copyOfHeroesMap.
    3. Simplified Code: Improved readability and kept the method calls the same while modifying variable names and values.

    Output:

    The HashMap is empty
    
    HashMap: {CaptainAmerica=1940, Hulk=5000, BlackWidow=1000, DoctorStrange=1500, AntMan=700}
    heroesMap: {CaptainAmerica=1940, Hulk=5000, BlackWidow=1000, DoctorStrange=1500, AntMan=700}
    
    Element at key CaptainAmerica : 1940
    Element at key Hulk : 5000
    Element at key BlackWidow : 1000
    Element at key DoctorStrange : 1500
    Element at key AntMan : 700
    
    Copy of HeroesMap:
    Element at key CaptainAmerica : 1940
    Element at key Hulk : 5000
    Element at key BlackWidow : 1000
    Element at key DoctorStrange : 1500
    Element at key AntMan : 700
    
    heroesMap.clear()
    After Clearing: {}

    Kotlin Program Using HashMap Functions (get()replace()put())

    fun main(args: Array<String>) {
        // Create an empty HashMap with <String, Int>
        val heroesMap: HashMap<String, Int> = HashMap()
    
        // Adding elements to the HashMap using put() function
        heroesMap.put("Batman", 5000)
        heroesMap.put("Superman", 2000)
        heroesMap.put("WonderWoman", 3000)
        heroesMap.put("Flash", 1500)
    
        // Printing the initial HashMap
        for (key in heroesMap.keys) {
            println("Element at key $key : ${heroesMap[key]}")
        }
    
        // Accessing elements using hashMap[key]
        println("\nheroesMap[\"Batman\"]: ${heroesMap["Batman"]}")
    
        // Updating a value using put() function
        heroesMap["Superman"] = 4000
        println("heroesMap.get(\"Superman\"): ${heroesMap.get("Superman")}\n")
    
        // Replacing values using replace() and put()
        heroesMap.replace("Flash", 1800)
        heroesMap.put("Superman", 4000)
    
        println("After replacing 'Flash' and updating 'Superman':")
    
        // Printing the updated HashMap
        for (key in heroesMap.keys) {
            println("Element at key $key : ${heroesMap[key]}")
        }
    }

    Output:

    Element at key Batman : 5000
    Element at key Superman : 2000
    Element at key WonderWoman : 3000
    Element at key Flash : 1500
    
    heroesMap["Batman"]: 5000
    heroesMap.get("Superman"): 4000
    
    After replacing 'Flash' and updating 'Superman':
    Element at key Batman : 5000
    Element at key Superman : 4000
    Element at key WonderWoman : 3000
    Element at key Flash : 1800
    Advantages of HashMap:
    • HashMap offers a versatile way to store key-value pairs, making it simple and convenient to use.
    • It provides highly efficient O(1) time complexity for common operations like adding, removing, and retrieving elements.
    • HashMap is capable of storing various types of data, including custom objects, making it adaptable to different scenarios.
    Disadvantages of HashMap:
    • HashMap tends to consume more memory than other data structures because it holds both keys and values.
    • By default, HashMap is not thread-safe, meaning concurrent access without proper synchronization can lead to data corruption or unexpected behavior. For multi-threaded access, a thread-safe alternative or synchronization mechanisms should be implemented.
    • HashMap does not guarantee the order of elements, which can be a drawback if the order of entries is important. In such cases, you may want to consider using a different structure like LinkedHashMap that maintains the order of insertion.
  • Functions

    Default and Named Arguments

    In most programming languages, when calling a function, we are required to specify all the arguments that the function accepts. However, in Kotlin, this constraint is lifted, making function calls more flexible. Kotlin allows us to omit certain arguments when calling a function by providing default values for parameters. This is one of the powerful features of Kotlin, which simplifies function calls and enhances readability.

    In Kotlin, function parameters are defined using Pascal notation (i.e., name: data_type) and separated by commas. By assigning default values to parameters, we can make them optional, allowing the function to use the provided defaults if arguments are not passed during the function call.

    There are two types of arguments in Kotlin –  

    1. Default arguments
    2. Named arguments

    Kotlin Default arguments 

    The arguments which need not specify explicitly while calling a function are called default arguments. 
    If the function is called without passing arguments then the default arguments are used as function parameters. In other cases, if arguments are passed during a function call then passed arguments are used as function parameters.

    There are three cases for default arguments-  

    1. No arguments are passed while calling a function
    2. Partial arguments are passed while calling a function
    3. All arguments are passed while calling a function

    1. No arguments are passed while calling a function 

    When no argument is passed while calling a function then the default arguments are used as function parameters. We need to initialize the variables while defining a function. 

    Kotlin program of calling student() function without passing an arguments

    // Function with default parameter values
    fun functionName(param1: Type = defaultValue1, param2: Type = defaultValue2) {
        // Function body
    }
    
    // Function call examples
    functionName()                        // Uses all default values
    functionName(param1 = value1)          // Overrides param1, uses default for param2
    functionName(param2 = value2)          // Overrides param2, uses default for param1
    functionName(param1 = value1, param2 = value2)  // Overrides both param1 and param2
    functionName(param2 = value2, param1 = value1)  // Named arguments allow reordering

    Example:

    // Function with default arguments for employee details
    fun employeeDetails(name: String = "John", department: String = "HR", id: Int = 101) {
        println("Employee Name: $name")
        println("Department: $department")
        println("Employee ID: $id")
    }
    
    fun main() {
        // Calling the function with no arguments (uses all default values)
        employeeDetails()
    
        // Calling the function with one argument (overrides the default name)
        employeeDetails(name = "Alice")
    
        // Calling the function with two arguments (overrides name and department)
        employeeDetails(name = "Bob", department = "IT")
    
        // Calling the function with all arguments (overrides all default values)
        employeeDetails(name = "Charlie", department = "Finance", id = 105)
    }

    Output:

    Hello, Guest!
    Hello, Alice!
    Hi, Bob!
    Welcome, Charlie!

    2. Partial arguments are passed while calling a function –

    Here some of the arguments are passed while calling a function and these are used as function parameters. If any formal parameter does not get value from the function call then the default value will be used for that parameter.

    Kotlin program of calling student() function with passing some arguments.

    // Default arguments in function definition: name, department, and salary
    fun employeeDetails(name: String = "John Doe", department: String = "HR", salary: Int = 50000) {
        println("Employee Name: $name")
        println("Department: $department")
        println("Salary: $salary")
    }
    
    fun main(args: Array<String>) {
        val employee_name = "Alice"
        val employee_department = "Finance"
    
        // Passing only two arguments: name and department
        employeeDetails(employee_name, employee_department)
    }

    Output:

    Employee Name: Alice
    Department: Finance
    Salary: 50000

    3. All arguments are passed while calling a function –

    Here, we have to pass all the arguments as defined in the function definition but data type of actual arguments must match with data type of formal arguments in the same order. 

    Kotlin program of calling student() function with passing all the arguments

    // Default arguments in function definition: name, category, and price
    fun productDetails(name: String = "Generic Product", category: String = "General", price: Double = 100.0) {
        println("Product Name: $name")
        println("Category: $category")
        println("Price: $$price")
    }
    
    fun main(args: Array<String>) {
        val product_name = "Laptop"
        val product_category = "Electronics"
        val product_price = 1500.0
    
        // Passing all the arguments of product name, category, and price in the same order as defined
        productDetails(product_name, product_category, product_price)
    }

    Output:

    Product Name: Laptop
    Category: Electronics
    Price: $1500.0

    Recursion

    Like other programming languages, we can use recursion in Kotlin. A function that calls itself is called a recursive function and this process of repetition is called recursion. Whenever a function is called then there are two possibilities:

    1. Normal function call
    2. Recursive function call

    1. Normal function call: When a function is called from main() block then it is called a normal function call. In below example, sum() is called at a time and it executes its instruction and terminate with returning the sum of number. If we want to execute the function again then we should call sum() from the main block one more time. 

    Calling sum() function from main() block

    2. Recursive Function Call : When a function invokes itself, it is known as a recursive function call. Every recursive function must have a termination condition, otherwise, the program may enter an infinite loop, potentially causing a stack overflow error.

    For example, calling the callMe() function from within its own body represents a recursive function call.

    Example 1:- Recursive function to calculate the sum of first N natural numbers

    // Recursive function to calculate the sum of first N natural numbers
    fun sumOfNumbers(n: Int): Int {
        // Base case: if n is 0, return 0
        return if (n == 0) {
            0
        } else {
            // Recursive case: add n to the result of sumOfNumbers(n - 1)
            n + sumOfNumbers(n - 1)
        }
    }
    
    fun main() {
        val number = 5
        val result = sumOfNumbers(number)
        println("The sum of first $number natural numbers is: $result")
    }

    Output:

    The sum of first 5 natural numbers is: 15

    Example 2:- Recursive Function to Calculate GCD

    // Recursive function to calculate the GCD of two numbers
    fun gcd(a: Int, b: Int): Int {
        return if (b == 0) {
            a  // Base case: when b becomes 0, return a
        } else {
            gcd(b, a % b)  // Recursive case: call gcd with b and a % b
        }
    }
    
    fun main() {
        val num1 = 48
        val num2 = 18
        val result = gcd(num1, num2)
        println("The GCD of $num1 and $num2 is: $result")
    }

    Output:

    The GCD of 48 and 18 is: 6
    Advantages of Recursion in Kotlin:

    1. Simplifies Complex Problems: Breaks problems into smaller sub-problems, making them easier to solve (e.g., tree traversal, factorial).
    2. Concise Code: Recursive functions are often shorter and more readable than iterative solutions.
    3. Modularity: Each recursive call solves a part of the problem, leading to a clear, step-by-step approach.
    4. Expressiveness: Ideal for algorithms like divide-and-conquer and backtracking.
    5. Tail Recursion Optimization: Kotlin optimizes tail-recursive functions, reducing the risk of stack overflow.

    Disadvantages of Recursion in Kotlin:

    1. Stack Overflow Risk: Deep recursion can cause stack overflow errors.
    2. Higher Memory Usage: Recursive calls add more stack frames, using more memory.
    3. Slower Performance: Recursive function calls can be slower due to call stack management.
    4. Complex Debugging: Tracking multiple recursive calls can make debugging harder.
    5. Limited by Call Stack Size: Deep recursion is limited by system stack depth.

    Tail Recursion

    In traditional recursion, the recursive call is made first, and the results are processed after the call returns. In tail recursion, the computation is done first, and the recursive call is made afterward, passing the result to the next call. Both methods produce the same result, but the key difference is how they manage their memory and function calls.

    In tail recursion, the recursive call must be the final action performed by the function. This allows the compiler to optimize the recursion by reusing the current function’s stack frame, thereby avoiding excessive memory usage and preventing StackOverflowError.

    Benefits of Tail Recursion:

    • Memory Optimization: Since there’s no need to store the current function call in the stack, the stack space is reused for the next recursive call.
    • Prevention of StackOverflowError: Tail-recursive functions avoid deep call stacks and do not run into stack overflow issues.

    Example 1: Finding the Factorial Using Tail Recursion

    // Tail-recursive function to calculate factorial
    tailrec fun factorial(num: Int, accumulator: Long = 1): Long {
        return if (num == 1)  // Base case
            accumulator
        else
            factorial(num - 1, accumulator * num)  // Tail recursion
    }
    
    fun main() {
        val number = 5
        val result = factorial(number)
        println("Factorial of $number is: $result")
    }

    Flowchart:

    Factorial of 5 is: 120

    Example 2: Finding the Sum of Array Elements Using Tail Recursion

    // Tail-recursive function to find sum of array elements
    tailrec fun sum(array: Array<Int>, index: Int, currentSum: Int = 0): Int {
        return if (index <= 0)  // Base case: index reaches 0
            currentSum
        else
            sum(array, index - 1, currentSum + array[index - 1])  // Tail recursion
    }
    
    fun main() {
        val array = arrayOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
        val totalSum = sum(array, array.size)
        println("The sum of array elements is: $totalSum")
    }

    Output:

    The sum of array elements is: 55

    Expressions and Anonymous Functions

    Lambdas are anonymous functions that can be treated as values, passed as arguments, or returned from functions. They are defined using curly braces {}.

    Syntax:

    val lambda_name : Data_type = { argument_List -> code_body }

    lambda expression in Kotlin is surrounded by curly braces {}. Inside the curly braces, the argument declarations go first, followed by the code body after the -> arrow. If the return type of the lambda is not Unit, the last expression in the lambda body is automatically considered as the return value.

    Example:

    val sum = { a: Int, b: Int -> a + b }

    Simplified Lambda Expression:

    Note: A lambda expression doesn’t always require a variable. It can be passed directly as an argument to a function.

    Kotlin Program Using Lambda Expressions:

    // Lambda with type annotations
    val sum1 = { a: Int, b: Int -> a + b }
    
    // Lambda without explicit type annotations
    val sum2: (Int, Int) -> Int = { a, b -> a + b }
    
    fun main(args: Array<String>) {
        val result1 = sum1(2, 3)
        val result2 = sum2(3, 4)
    
        println("The sum of two numbers is: $result1")
        println("The sum of two numbers is: $result2")
    
        // Directly printing the result of a lambda expression
        println(sum1(5, 7))
    }

    Output:

    The sum of two numbers is: 5
    The sum of two numbers is: 7
    12
    Type Inference in Lambdas

    Kotlin’s type inference allows the compiler to determine the type of a lambda expression. For example, in the following lambda expression, the compiler infers the types of parameters based on context:

    val sum = { a: Int, b: Int -> a + b }

    The compiler treats this lambda as a function of type (Int, Int) -> Int.

    If you want to return a string instead, you can use Kotlin’s built-in toString() function.

    Example:

    val sum1 = { a: Int, b: Int ->
        val num = a + b
        num.toString()  // Converts the result to String
    }
    
    fun main() {
        val result = sum1(2, 3)
        println("The sum of two numbers is: $result")
    }

    Output:

    The sum of two numbers is: 5

    Lambda Type Declaration and Examples:

    We can declare the types of input and output explicitly in lambdas. The pattern is (InputType) -> OutputType.

    Syntax:

    val lambda1: (Int) -> Int = { a -> a * a }
    val lambda2: (String, String) -> String = { a, b -> a + b }
    val lambda3: (Int) -> Unit = { println(it) }  // No return value

    Example:

    val lambda4: String.(Int) -> String = { this + it }
    
    fun main() {
        val result = "Hello".lambda4(42)
        println(result)
    }

    Output:

    Hello42
    Implicit Parameter (it)

    When a lambda has a single parameter, Kotlin provides an implicit parameter called it, which can be used to reference that parameter.

    Shorthand form using it:

    val numbers = arrayOf(1, -2, 3, -4, 5)
    
    fun main() {
        println(numbers.filter { it > 0 })  // Uses implicit parameter 'it'
    }

    Output:

    [1, 3, 5]

    Longhand form:

    val numbers = arrayOf(1, -2, 3, -4, 5)
    
    fun main() {
        println(numbers.filter { item -> item > 0 })  // Explicit parameter
    }

    Output:

    [1, 3, 5]
    Returning Values from Lambda Expressions

    A lambda expression returns the final value of the last expression. The return value can be of any type, such as an IntString, or Boolean.

    Kotlin Program Returning String from Lambda:

    val find = fun(num: Int): String {
        return when {
            num % 2 == 0 && num < 0 -> "Number is even and negative"
            num % 2 == 0 && num > 0 -> "Number is even and positive"
            num % 2 != 0 && num < 0 -> "Number is odd and negative"
            else -> "Number is odd and positive"
        }
    }
    
    fun main() {
        val result = find(112)
        println(result)
    }

    Output:

    Number is even and positive
    Anonymous Functions

    An anonymous function is like a regular function, except it doesn’t have a name. It can be used as an expression or a block.

    Example 1: Anonymous Function as an Expression:

    val multiply = fun(a: Int, b: Int): Int = a * b
    val anonymous1 = fun(x: Int, y: Int): Int = x + y
    val anonymous2 = fun(a: Int, b: Int): Int {
        val mul = a * b
        return mul
    }
    
    fun main() {
        val sum = anonymous1(3, 5)
        val mul = anonymous2(3, 5)
        println("The sum of two numbers is: $sum")
        println("The multiply of two numbers is: $mul")
    }

    Output:

    The sum of two numbers is: 8
    The multiply of two numbers is: 15

    Inline Functions

    In Kotlin, higher-order functions and lambda expressions are stored as objects, which can introduce memory overhead due to memory allocation for function objects and virtual calls. This overhead can be minimized by using the inline keyword. By marking a function as inline, we instruct the compiler to avoid allocating memory for the function and to copy the function’s code directly to the call site, thus improving performance.

    Example:

    fun higherfunc(str: String, mycall: (String) -> Unit) {
        mycall(str)  // Calls print() by passing str
    }
    
    // Main function
    fun main(args: Array<String>) {
        print("HelloKotlin: ")
        higherfunc("A Computer Science portal for Kotlin", ::print)
    }
    Bytecode Explanation:

    Like Java, Kotlin is platform-independent, and code is compiled to bytecode. To view bytecode, go to Tools -> Kotlin -> Show Kotlin Bytecode and decompile it.

    In the bytecode, the main thing to notice is:

    mycall.invoke(str)

    Here, mycall invokes the print function by passing str as a parameter, which adds an extra function call and memory overhead. If multiple such function objects are created, it can lead to increased method counts and a higher memory footprint.

    How Inline Helps:

    inline fun higherfunc(str: String, mycall: (String) -> Unit) {
        mycall(str)  // Calls print() by passing str
    }
    
    // Main function
    fun main(args: Array<String>) {
        print("GeeksforGeeks: ")
        higherfunc("A Computer Science portal for Geeks", ::print)
    }
    Non-Local Control Flow with Inline Functions

    In Kotlin, normally, you cannot return from a lambda expression directly. However, using the inline keyword allows you to return from the lambda and also exit the calling function where the inline function was invoked.

    Example with Return in Lambda:

    var lambda = {
        println("Lambda expression")
        return  // Normally, returning from a lambda like this causes a compile-time error
    }
    
    fun main(args: Array<String>) {
        lambda()
    }

    Output:

    Error: 'return' is not allowed here

    Using Return in Lambda Inside an Inline Function:

    fun main(args: Array<String>) {
        println("Main function starts")
        inlinedFunc({
            println("Lambda expression 1")
            return  // Inline functions allow this
        }, {
            println("Lambda expression 2")
        })
        println("Main function ends")
    }
    
    inline fun inlinedFunc(lmbd1: () -> Unit, lmbd2: () -> Unit) {
        lmbd1()
        lmbd2()
    }

    Output:

    Main function starts
    Lambda expression 1
    crossinline Keyword

    By default, using return in an inline function exits both the lambda and the enclosing function. To prevent this behavior, you can mark the lambda with the crossinline keyword, which prevents return statements within the lambda.

    Example with crossinline:

    fun main(args: Array<String>) {
        println("Main function starts")
        inlinedfunc({
            println("Lambda expression 1")
            return  // This will cause a compile-time error
        }, {
            println("Lambda expression 2")
        })
        println("Main function ends")
    }
    
    inline fun inlinedfunc(crossinline lmbd1: () -> Unit, lmbd2: () -> Unit) {
        lmbd1()
        lmbd2()
    }

    Output:

    Error: 'return' is not allowed here
    noinline Keyword

    If you only want some lambdas passed to an inline function to be inlined, you can use the noinline modifier. This tells the compiler not to inline certain lambdas.

    Example with noinline:

    fun main(args: Array<String>) {
        println("Main function starts")
        inlinedFunc({
            println("Lambda expression 1")
            return
        }, {
            println("Lambda expression 2")
            return  // This causes a compile-time error
        })
        println("Main function ends")
    }
    
    inline fun inlinedFunc(lmbd1: () -> Unit, noinline lmbd2: () -> Unit) {
        lmbd1()
        lmbd2()
    }

    Output:

    Error: 'return' is not allowed here
    Reified Type Parameters

    Sometimes, you may need to access the type of a parameter passed during a function call. By using the reified modifier, you can retrieve the type of the parameter at runtime.

    Example with reified:

    fun main(args: Array<String>) {
        genericFunc<String>()
    }
    
    inline fun <reified T> genericFunc() {
        println(T::class)
    }

    Output:

    class kotlin.String
    Inline Properties

    Inline properties work similarly to inline functions. The inline modifier can be used on property accessors (getter and setter) to copy the accessor methods to the calling location.

    Example with Inline Properties:

    fun main(args: Array<String>) {
        print(flag)
    }
    
    fun foo(i: Int): Int {
        return i
    }
    
    inline var flag: Boolean
        get() = foo(10) == 10
        set(value) { /* Do nothing */ }

    Output:

    true

    Higher-Order Functions

    Kotlin has excellent support for functional programming. Functions in Kotlin can be stored in variables, passed as arguments, and returned from other functions, making it easy to work with higher-order functions.

    Higher-Order Functions

    higher-order function is a function that takes another function as a parameter or returns a function as its result. Instead of passing primitive data types (e.g., IntStringArray), we pass anonymous functions or lambda expressions to other functions for more flexible and reusable code.

    Passing Lambda Expressions to Higher-Order Functions

    Kotlin allows us to pass lambda expressions as parameters to higher-order functions. These lambda expressions can either return Unit or any other type such as Int or String.

    Example 1: Lambda Expression Returning Unit

    // Lambda expression
    val lambda = { println("HelloKotlin") }
    
    // Higher-order function accepting a lambda
    fun higherfunc(lmbd: () -> Unit) {
        lmbd()  // Invokes the lambda
    }
    
    fun main(args: Array<String>) {
        higherfunc(lambda)  // Passing the lambda as a parameter
    }

    Output:

    HelloKotlin
    Kotlin and Functional Programming: Higher-Order Functions

    Kotlin has excellent support for functional programming. Functions in Kotlin can be stored in variables, passed as arguments, and returned from other functions, making it easy to work with higher-order functions.

    Higher-Order Functions

    higher-order function is a function that takes another function as a parameter or returns a function as its result. Instead of passing primitive data types (e.g., IntStringArray), we pass anonymous functions or lambda expressions to other functions for more flexible and reusable code.

    Passing Lambda Expressions to Higher-Order Functions

    Kotlin allows us to pass lambda expressions as parameters to higher-order functions. These lambda expressions can either return Unit or any other type such as Int or String.

    Example 1: Lambda Expression Returning Unit

    // Lambda expression
    val lambda = { println("HelloKotlin
    ") }
    
    // Higher-order function accepting a lambda
    fun higherfunc(lmbd: () -> Unit) {
        lmbd()  // Invokes the lambda
    }
    
    fun main(args: Array<String>) {
        higherfunc(lambda)  // Passing the lambda as a parameter
    }

    Output:

    HelloKotlin

    Explanation:

    • The lambda expression prints a string.
    • The higher-order function higherfunc accepts this lambda and invokes it inside the function body.

    Example 2: Lambda Expression Returning an Integer

    // Lambda expression returning an integer
    val lambda = { a: Int, b: Int -> a + b }
    
    // Higher-order function accepting the lambda
    fun higherfunc(lmbd: (Int, Int) -> Int) {
        val result = lmbd(2, 4)  // Invokes the lambda by passing parameters
        println("The sum of two numbers is: $result")
    }
    
    fun main(args: Array<String>) {
        higherfunc(lambda)  // Passing the lambda as a parameter
    }

    Output:

    The sum of two numbers is: 6
    Kotlin and Functional Programming: Higher-Order Functions

    Kotlin has excellent support for functional programming. Functions in Kotlin can be stored in variables, passed as arguments, and returned from other functions, making it easy to work with higher-order functions.

    Higher-Order Functions

    higher-order function is a function that takes another function as a parameter or returns a function as its result. Instead of passing primitive data types (e.g., IntStringArray), we pass anonymous functions or lambda expressions to other functions for more flexible and reusable code.

    Passing Lambda Expressions to Higher-Order Functions

    Kotlin allows us to pass lambda expressions as parameters to higher-order functions. These lambda expressions can either return Unit or any other type such as Int or String.

    Example 1: Lambda Expression Returning Unit

    // Regular function definition
    fun printMe(s: String) {
        println(s)
    }
    
    // Higher-order function accepting another function as a parameter
    fun higherfunc(str: String, myfunc: (String) -> Unit) {
        myfunc(str)  // Invokes the function passed as a parameter
    }
    
    fun main(args: Array<String>) {
        higherfunc("HelloKotlin", ::printMe)
    }

    Output:

    HelloKotlin
    Returning a Function from a Higher-Order Function

    Higher-order functions can also return a function. When returning a function, we need to specify the return type as a function signature.

    Example: Returning a Function

    // Function declaration
    fun mul(a: Int, b: Int): Int {
        return a * b
    }
    
    // Higher-order function returning another function
    fun higherfunc(): (Int, Int) -> Int {
        return ::mul
    }
    
    fun main(args: Array<String>) {
        val multiply = higherfunc()  // Storing the returned function in a variable
        val result = multiply(2, 4)  // Invoking the returned function
        println("The multiplication of two numbers is: $result")
    }

    Output:

    The multiplication of two numbers is: 8
  • Array & String

    Arrays

    In Kotlin, arrays are used to store multiple elements of the same type in a single variable. Arrays are a fundamental data structure that allows efficient storage and retrieval of data. Kotlin provides various methods and functions to operate on arrays, making them versatile and powerful.

    Creating Arrays:

    1. Using arrayOf(): This method creates an array of specified elements.

    Syntax:

    val intArray = arrayOf(1, 2, 3, 4)
    val stringArray = arrayOf("one", "two", "three")

    Example

    fun main() {
        val numbers = arrayOf(1, 2, 3, 4, 5)
    
        // Print all elements
        numbers.forEach { println(it) }
    
        // Modify and print the array
        numbers[2] = 10
        println(numbers.joinToString())
    }

    Output

    1
    2
    3
    4
    5
    1, 2, 10, 4, 5

    2. Using Array Constructor :The constructor takes the size of the array and a lambda function to initialize the elements.

    Syntax

    val num = Array(3, {i-> i*1})

    Example

    fun main()
    {
    	val arrayname = Array(5, { i -> i * 1 })
    	for (i in 0..arrayname.size-1)
    	{
    		println(arrayname[i])
    	}
    }

    Output

    0
    1
    2
    3
    4

    3. Typed Arrays: Kotlin provides specialized classes for primitive arrays to avoid the overhead of boxing.

    Syntax:

    val num = Array(3) { i -> i * 1 }

    Example

    fun main() {
        // Creating an IntArray of size 5, initialized with index * 2
        val intArray = IntArray(5) { it * 2 }
    
        // Creating a DoubleArray of size 3, initialized with index + 0.5
        val doubleArray = DoubleArray(3) { it + 0.5 }
    
        // Creating a BooleanArray with predefined values
        val booleanArray = booleanArrayOf(true, false, true)
    
        // Printing the contents of each typed array
        println("IntArray: ${intArray.joinToString()}")
        println("DoubleArray: ${doubleArray.joinToString()}")
        println("BooleanArray: ${booleanArray.joinToString()}")
    }

    Output

    1 2 3 4 5
    10 20 30 40 50
    Accessing Array Elements

    1. Using indexing : In Kotlin, array elements can be accessed using indexing. This involves specifying the position of the element within the array using its index. Array indices start at 0, so the first element is accessed with index 0, the second with index 1, and so on. Indexing is a straightforward way to retrieve or modify individual elements of an array.

    Syntax:

    val element = array[index]

    Example

    fun main() {
        // Define an array of integers
        val numbers = arrayOf(10, 20, 30, 40, 50)
    
        // Access elements using indexing
        val firstElement = numbers[0]   // Access the first element
        val secondElement = numbers[1]  // Access the second element
        val lastElement = numbers[4]    // Access the last element
    
        // Print the accessed elements
        println("First element: $firstElement")
        println("Second element: $secondElement")
        println("Last element: $lastElement")
    }

    Output:

    First element: 10
    Second element: 20
    Last element: 50

    2. Modifying Elements: Modifying elements in an array means changing the value of an element at a specific index. In an array, you can update any element by accessing it through its index and assigning a new value to that position. The array’s length and structure remain the same, but the value at the chosen index is replaced.

    Syntax:

    array[index] = new_value

    Example

    fun main() {
        // Define an array
        val arr = arrayOf(5, 10, 15, 20, 25)
    
        // Modify the element at index 1 (second element)
        arr[1] = 12
    
        // Modify the last element using a negative-like index (-1 is the last element)
        arr[arr.size - 1] = 30
    
        // Print the updated array
        println("Updated array: ${arr.joinToString(", ")}")
    }

    Output:

    Updated array: 5, 12, 15, 20, 30
    Array Methods:

    1. Size: In Kotlin, the size property returns the number of elements in an array. This is useful when you want to know how many items are stored in the array. It is not a method but a property that gives you the length of the array.

    Syntax:

    array.size

    Example

    fun main() {
        // Define an array
        val arr = arrayOf(1, 2, 3, 4, 5)
    
        // Get the size of the array
        val arraySize = arr.size
    
        // Print the size
        println("The size of the array is: $arraySize")
    }

    Output:

    The size of the array is: 5

    2. Iterating Over an Array: Iterating over an array refers to accessing each element of the array one by one in sequence. It allows you to perform operations on each element. In Kotlin, this can be done using a for loop or the forEach function, both of which are commonly used to traverse an array.

    Syntax:

    for (element in array) {
        // Perform an operation with each element
    }

    Example

    fun main() {
        // Define an array
        val arr = arrayOf(10, 20, 30, 40, 50)
    
        // Iterate over the array
        for (element in arr) {
            println(element)
        }
    }

    Output:

    10
    20
    30
    40
    50

    3. Using Higher-Order Functions: Higher-order functions in Kotlin are functions that take other functions as parameters or return functions. These are commonly used to perform operations on collections or arrays. Functions like mapfilterforEach, etc., are examples of higher-order functions that allow functional-style processing of array elements.

    Syntax:

    array.functionName { element ->
        // Perform an operation with each element
    }

    Example

    fun main() {
        // Define an array
        val arr = arrayOf(1, 2, 3, 4, 5)
    
        // Use higher-order function map to create a new array with doubled values
        val doubledArray = arr.map { it * 2 }
    
        // Print the new array
        println(doubledArray)
    }

    Output:

    [2, 4, 6, 8, 10]

    Kotlin Strings

    Strings in Kotlin are sequences of characters enclosed within double quotes. Strings are immutable, meaning that once a string is created, its value cannot be altered. Kotlin strings come with a rich set of methods for performing operations like concatenation, comparison, searching, and more.

    Creating Strings:

    1. Literal: Strings can be created by directly assigning a sequence of characters to a variable.

    Syntax:

    val variableName = "Your string here"

    Example

    fun main() {
        // Creating a string literal
        val greeting = "Hello, Kotlin!"
    
        // Print the string
        println(greeting)
    }

    Output:

    Hello, Kotlin!

    2. Using String Constructor: In Kotlin, you can create strings using the String constructor, which allows you to create a string from an array of characters. This approach is useful when you have a character array and want to transform it into a string. The String constructor takes a CharArray (array of characters) as a parameter and returns a string.

    Syntax:

    val stringVariable = String(charArray)

    Example

    fun main() {
        // Create a character array
        val charArray = charArrayOf('K', 'o', 't', 'l', 'i', 'n')
    
        // Use the String constructor to create a string from the character array
        val str = String(charArray)
    
        // Print the string
        println(str)
    }

    Output:

    Kotlin
    String Templates:

    1. Variable Interpolation: You can embed variables directly within strings using the ${} syntax.

    Syntax:

    val variable = "value"
    val result = "String with $variable inside"

    Example

    fun main() {
        // Declare a variable
        val name = "Kotlin"
    
        // Use string interpolation to include the variable in the string
        val greeting = "Hello, $name!"
    
        // Print the interpolated string
        println(greeting)
    }

    Output:

    Hello, Kotlin!

    2. Expression Interpolation: In Kotlin, you can embed expressions inside strings using string templates. This allows you to include the result of any expression within a string, which will be evaluated and concatenated as part of the string. Expressions are enclosed in curly braces ${}.

    Syntax:

    val result = "String with ${expression} inside"

    Example

    fun main() {
        // Declare variables
        val x = 10
        val y = 20
    
        // Use string interpolation to include an expression in the string
        val result = "The sum of $x and $y is ${x + y}"
    
        // Print the result
        println(result)
    }

    Output:

    The sum of 10 and 20 is 30
    String Methods:

    1. Length: In Kotlin, you can get the length of a string using the length property. This property returns the number of characters present in the string.

    Syntax:

    val length = stringVariable.length

    Example

    fun main() {
        // Define a string
        val text = "Kotlin Programming"
    
        // Get the length of the string
        val length = text.length
    
        // Print the length
        println("The length of the string is: $length")
    }

    Output:

    The length of the string is: 18

    2. Accessing Characters: In Kotlin, characters in a string can be accessed using their index. Since strings are zero-indexed, the index starts at 0 for the first character. You can use square brackets [] to access individual characters at a specified index.

    Syntax:

    val character = stringVariable[index]

    Example

    fun main() {
        // Define a string
        val text = "Kotlin"
    
        // Access characters by index
        val firstChar = text[0]    // First character
        val thirdChar = text[2]    // Third character
        val lastChar = text[text.length - 1]  // Last character
    
        // Print the accessed characters
        println("First character: $firstChar")
        println("Third character: $thirdChar")
        println("Last character: $lastChar")
    }

    Output:

    First character: K
    Third character: t
    Last character: n

    3. Substrings: In Kotlin, extract a part of a string using the substring method. This method allows you to specify a range of indices and returns a new string that contains the characters from the starting index up to, but not including, the ending index.

    Example

    val substring = stringVariable.substring(startIndex)

    Example

    fun main() {
        // Define a string
        val text = "Kotlin Programming"
    
        // Extract substrings
        val firstPart = text.substring(0, 6)    // Extracts "Kotlin"
        val secondPart = text.substring(7)      // Extracts "Programming"
    
        // Print the substrings
        println("First part: $firstPart")
        println("Second part: $secondPart")
    }

    Output:

    First part: Kotlin
    Second part: Programming

    4. String Comparison: Kotlin provides two types of equality checks for strings:

    • == for structural equality (checks if the values are the same).
    • === for referential equality (checks if the references point to the same object).

    Syntax:

    Structural Equality: string1 == string2
    Referential Equality: string1 === string2

    Example:

    fun main() {
        // Define strings
        val str1 = "Kotlin"
        val str2 = "Kotlin"
        val str3 = str1
        val str4 = String("Kotlin".toCharArray()) // Creates a new String object with the same content
    
        // Structural equality
        println("str1 == str2: ${str1 == str2}") // True, because the content is the same
        println("str1 == str4: ${str1 == str4}") // True, because the content is the same
    
        // Referential equality
        println("str1 === str2: ${str1 === str2}") // True, because both refer to the same interned object
        println("str1 === str3: ${str1 === str3}") // True, because str3 is assigned from str1
        println("str1 === str4: ${str1 === str4}") // False, because str4 is a new object, even though it has the same content
    }

    Output:

    str1 == str2: true
    str1 == str4: true
    str1 === str2: true
    str1 === str3: true
    str1 === str4: false

    5. String Manipulation: Kotlin provides several common methods for manipulating strings. These include methods like toUpperCasetoLowerCasetrimsplit, and more. These methods are used to perform various operations on strings, such as changing their case, trimming whitespace, or splitting them into substrings.

    Syntax of toUpperCase():

    toUpperCase()
    
    Syntax:
    val upperCaseString = originalString.toUpperCase()

    Syntax of toLowerCase():

    val lowerCaseString = originalString.toLowerCase()

    Syntax of trim():

    val trimmedString = originalString.trim()

    Syntax of split():

    val splitString = originalString.split(" ")

    Syntax of substring():

    val substring = originalString.substring(startIndex, endIndex)

    Syntax of replace():

    val replacedString = originalString.replace("oldValue", "newValue")

    Syntax of startsWithm k():

    val startsWithPrefix = originalString.startsWith("prefix")

    Syntax of toLowerCase():

    val endsWithSuffix = originalString.endsWith("suffix")

    Example

    fun main() {
        // Define a string
        val originalString = "   Kotlin String Manipulation   "
    
        // Convert the string to uppercase
        val upperCaseString = originalString.toUpperCase()
    
        // Convert the string to lowercase
        val lowerCaseString = originalString.toLowerCase()
    
        // Trim leading and trailing whitespace
        val trimmedString = originalString.trim()
    
        // Split the string into substrings
        val splitString = trimmedString.split(" ")
    
        // Print the results
        println("Original String: '$originalString'")
        println("Uppercase: '$upperCaseString'")
        println("Lowercase: '$lowerCaseString'")
        println("Trimmed: '$trimmedString'")
        println("Split: ${splitString.joinToString(", ")}")
    }

    Output:

    fun main() {
        // Define a string
        val originalString = "   Kotlin String Manipulation   "
    
        // Convert the string to uppercase
        val upperCaseString = originalString.toUpperCase()
    
        // Convert the string to lowercase
        val lowerCaseString = originalString.toLowerCase()
    
        // Trim leading and trailing whitespace
        val trimmedString = originalString.trim()
    
        // Split the string into substrings
        val splitString = trimmedString.split(" ")
    
        // Print the results
        println("Original String: '$originalString'")
        println("Uppercase: '$upperCaseString'")
        println("Lowercase: '$lowerCaseString'")
        println("Trimmed: '$trimmedString'")
        println("Split: ${splitString.joinToString(", ")}")
    }
  • Control Statements

    If- Expression

    Decision Making in programming is similar to decision-making in real life. In programming too, a certain block of code needs to be executed when some condition is fulfilled. A programming language uses control statements to control the flow of execution of a program based on certain conditions. If the condition is true then it enters into the conditional block and executes the instructions. 
    There are different types of if-else expressions in Kotlin: 

    • if expression
    • if-else expression
    • if-else-if ladder expression
    • nested if expression

    If- Expression : Kotlin’s if can be used both as a statement and as an expression. When used as an expression, it returns a value.

    Syntax:

    if(condition) {
    
           // code to run if condition is true
    }

    Example:

    fun main(args: Array<String>) {
    	var a = 3
    	if(a > 0){
    		print("Yes,number is positive")
    	}
    }

    Output:

    Yes, number is positive

    if-else-if ladder Expression: Kotlin’s if-else-if ladder can be used both as a statement and as an expression. When used as an expression, it returns a value.

    Syntax:

    if(Firstcondition) {
        // code to run if condition is true
    }
    else if(Secondcondition) {
        // code to run if condition is true
    }
    else{
    }

    Example:

    import java.util.Scanner
    
    fun main(args: Array<String>) {
    
    	// create an object for scanner class
    	val reader = Scanner(System.`in`)
    	print("Enter any number: ")
    
    	// read the next Integer value
    	var num = reader.nextInt()
    	var result = if ( num > 0){
    		"$num is positive number"
    	}
    	else if( num < 0){
    		"$num is negative number"
    	}
    	else{
    		"$num is equal to zero"
    	}
    	println(result)
    
    }

    Output:

    Enter any number: 12
    12 is positive number
    
    Enter any number: -11
    -11 is negative number
    
    Enter any number: 0
    0 is zero

    If-Else Expression: Kotlin’s if-else can be used both as a statement and as an expression. When used as an expression, it returns a value.

    Syntax:

    if(condition) {
            // code to run if condition is true
    }
    else {
           // code to run if condition is false
    }

    Example:

    if(condition) {
            // code to run if condition is true
    }
    else {
           // code to run if condition is false
    }

    Output:

    fun main(args: Array<String>) {
        var a = 5
        var b = 10
        if(a > b){
            print("Number 5 is larger than 10")
        }
        else{
            println("Number 10 is larger than 5")
        }
    }

    nested if expression: Kotlin’s if-else-if ladder can be used both as a statement and as an expression. When used as an expression, it returns a value.

    Syntax:

    if(condition1){
                // code 1
          if(condition2){
                      // code2
          }
    }

    Example:

    import java.util.Scanner
    
    fun main(args: Array<String>) {
    
    	// create an object for scanner class
    	val reader = Scanner(System.`in`)
    	print("Enter three numbers: ")
    
    	var num1 = reader.nextInt()
    	var num2 = reader.nextInt()
    	var num3 = reader.nextInt()
    
    	var max = if ( num1 > num2) {
    		if (num1 > num3) {
    			"$num1 is the largest number"
    		}
    		else {
    			"$num3 is the largest number"
    		}
    	}
    	else if( num2 > num3){
    		"$num2 is the largest number"
    	}
    	else{
    		"$num3 is the largest number"
    	}
    	println(max)
    
    }

    Output:

    Enter three numbers: 123 231 321
    321 is the largest number

    While Loop

    The while loop executes a block of code as long as the condition is true.

    Syntax:

    fun main(args: Array<String>) {
    	var number = 1
    
    	while(number <= 10) {
    		println(number)
    		number++;
    	}
    }

    Example:

    var count = 5
    
    while (count > 0) {
        println("Count: $count")
        count--
    }

    Output:

    Count: 5
    Count: 4
    Count: 3
    Count: 2
    Count: 1

    Do-While Loop

    The do-while loop executes the block of code at least once before checking the condition.

    Syntax:

    while(condition) {
               // code to run
    }

    Example:

    var count = 5
    
    do {
        println("Count: $count")
        count--
    } while (count > 0)

    Output:

    Count: 5
    Count: 4
    Count: 3
    Count: 2
    Count: 1

    For Loop

    Kotlin’s for loop is used to iterate over ranges, arrays, or other iterable objects.

    Syntax:

    for(item in collection) {
           // code to execute
    }

    Example:

    // Iterating over a range
    for (i in 1..5) {
        println(i)
    }
    
    // Iterating over an array
    val items = arrayOf("apple", "banana", "cherry")
    for (item in items) {
        println(item)
    }
    
    // Iterating with an index
    for ((index, value) in items.withIndex()) {
        println("Item at $index is $value")
    }

    Output:

    1
    2
    3
    4
    5
    apple
    banana
    cherry
    Item at 0 is apple
    Item at 1 is banana
    Item at 2 is cherry

    When Expression

    The when expression in Kotlin is used as a replacement for the switch statement and can be used both as a statement and as an expression.

    Syntax:

    fun main (args : Array<String>) {
    	print("Enter the name of heavenly body: ")
    	var name= readLine()!!.toString()
    	when(name) {
    		"Sun" -> print("Sun is a Star")
    		"Moon" -> print("Moon is a Satellite")
    		"Earth" -> print("Earth is a planet")
    		else -> print("I don't know anything about it")
    	}
    }

    Example:

    val x = 3
    
    when (x) {
        1 -> println("x is 1")
        2 -> println("x is 2")
        3, 4 -> println("x is 3 or 4")
        in 5..10 -> println("x is in the range 5 to 10")
        !in 1..10 -> println("x is outside the range 1 to 10")
        else -> println("x is none of the above")
    }
    
    // When as an expression
    val result = when (x) {
        1 -> "One"
        2 -> "Two"
        else -> "Unknown"
    }
    println(result)

    Output:

    x is 3 or 4
    Unknown

    Unlabelled Break

    The break statement terminates the nearest enclosing loop.

    Syntax:

    while(test expression) {
           // code to run
                if(break condition) {
                  break
                }
          // another code to run
    }

    Example:

    fun main(args: Array<String>) {
    	var sum = 0
    	var i = 1
    	while(i <= Int.MAX_VALUE) {
    		sum += i
    		i++
    		if(i == 11) {
    			break
    		}
    	}
    	print("The sum of integers from 1 to 10: $sum")
    }

    Output:

    The sum of integers from 1 to 10: 55

    labelled Break

    The continue statement skips the current iteration of the nearest enclosing loop. With labels, you can control which loop to continue.

    Syntax:

    outer@ while(firstcondition) {
          // code
          inner@ while(secondcondition) {
                //code
                if(condition for continue) {
                   continue@outer
                }
          }
    }

    Example:

    outer@ for (i in 1..5) {
        for (j in 1..5) {
            if (i == 3 && j == 2) {
                println("Breaking outer loop at i=$i, j=$j")
                break@outer  // Breaks the outer loop
            }
            println("i=$i, j=$j")
        }
    }

    Output:

    i=1, j=1
    i=1, j=2
    i=1, j=3
    i=1, j=4
    i=1, j=5
    i=2, j=1
    i=2, j=2
    i=2, j=3
    i=2, j=4
    i=2, j=5
    i=3, j=1
    Breaking outer loop at i=3, j=2
  • Basic Concepts

    Kotlin Data Types

    In Kotlin, data types are the foundation of variable storage. Each variable must be assigned a specific data type, which determines the kind of data it can hold and the operations that can be performed on it.

    1. Int (Integer): Represents a 32-bit signed integer. Suitable for whole numbers.

    These data types contain integer values.

    Data TypeBitsMin ValueMax Value
    byte8 bits-128127
    short16 bits-3276832767
    int32 bits-21474836482147483647
    long64 bits-9223372036854775808 9223372036854775807

    Example:

    fun main(args: Array<String>) {
        var myInt = 35
        var myLong = 23L // suffix L for long integer
    
        println("My integer: $myInt")
        println("My long integer: $myLong")
    
        println("Smallest byte value: ${Byte.MIN_VALUE}")
        println("Largest byte value: ${Byte.MAX_VALUE}")
    
        println("Smallest short value: ${Short.MIN_VALUE}")
        println("Largest short value: ${Short.MAX_VALUE}")
    
        println("Smallest integer value: ${Int.MIN_VALUE}")
        println("Largest integer value: ${Int.MAX_VALUE}")
    
        println("Smallest long integer value: ${Long.MIN_VALUE}")
        println("Largest long integer value: ${Long.MAX_VALUE}")
    }

    Output:

    My integer: 42
    My long integer: 123456789
    
    Smallest byte value: -128
    Largest byte value: 127
    
    Smallest short value: -32768
    Largest short value: 32767
    
    Smallest integer value: -2147483648
    Largest integer value: 2147483647
    
    Smallest long integer value: -9223372036854775808
    Largest long integer value: 9223372036854775807
    
    Smallest float value: 1.4E-45
    Largest float value: 3.4028235E38
    
    Smallest double value: 4.9E-324
    Largest double value: 1.7976931348623157E308

    2. Boolean Data Type : The Boolean data type represents one bit of information, with two possible values: true or false.

    Data TypeBitsValue Range
    Boolean1true, false

    Example Program:

    fun main(args: Array<String>) {
        if (true is Boolean) {
            println("Yes, true is a boolean value")
        }
    }

    Output:

    Yes, true is a boolean value

    3. Character Data Type : Represents characters such as letters, digits, and symbols.

    Data TypeBitsMin ValueMax Value
    Char16‘\u0000’‘\uFFFF’

    Example Program:

    fun main(args: Array<String>) {
        var alphabet: Char = 'C'
        println("C is a character: ${alphabet is Char}")
    }

    Output:

    C is a character: true

    Variables

    Variables in Kotlin store data values, which can be accessed and modified throughout the program. In Kotlin, every variable should be declared before it’s used. Without declaring a variable, an attempt to use the variable gives a syntax error. Declaration of the variable type also decides the kind of data you are allowed to store in the memory location. In case of local variables, the type of variable can be inferred from the initialized value. Kotlin enforces the declaration of variables with explicit types, providing clarity and safety.

    1. Immutable (val): Once assigned, the value of the variable cannot be changed. It is similar to a constant.

    var rollNumber = 55
    var studentName = "Praveen"
    println(rollNumber)
    println(studentName)

    In the example above, the variable rollNumber holds the value 55, and its type is inferred as an Int based on the literal value. Similarly, studentName is recognized as a String. In Kotlin, variables are declared using two primary keywords:

    • val for immutable variables
    • var for mutable variables

    2. Immutable Variables (val): Variables declared with val are read-only, meaning their value cannot be changed once assigned.

    val personName = "Gaurav"
    personName = "Praveen" // This will cause a compile-time error

    Attempting to reassign an immutable variable will result in an error. Although they cannot be reassigned, val variables are not considered constants because they can be initialized with other variables. The value of a val variable doesn’t need to be known at compile-time and can change during each function call if declared inside a construct that’s called repeatedly.

    var birthDate = "02/12/1993"
    val newBirthDate = birthDate
    println(newBirthDate)

    3. Mutable Variables (var) : Variables declared with var can have their values changed after initialization.

    var age = 25
    age = 26 // This compiles successfully
    println("My updated age is $age")

    Output:

    My updated age is 26

    Kotlin Operators

    In Kotlin, operators are special symbols that carry out operations on operands. For instance, + and  are operators that perform addition and subtraction, respectively. Similar to Java, Kotlin provides various kinds of operators:

    • Arithmetic operators
    • Relational operators
    • Assignment operators
    • Unary operators
    • Logical operators
    • Bitwise operators

    Arithmetic Operators : Arithmetic operators are used to perform basic mathematical operations. Here’s a breakdown:

    OperatorMeaningExpressionTranslates to
    +Additiona + ba.plus(b)
    -Subtractiona - ba.minus(b)
    *Multiplicationa * ba.times(b)
    /Divisiona / ba.div(b)
    %Modulusa % ba.rem(b)

    Example:

    fun main(args: Array<String>) {
        var a = 20
        var b = 4
        println("a + b = " + (a + b))
        println("a - b = " + (a - b))
        println("a * b = " + (a.times(b)))
        println("a / b = " + (a / b))
        println("a % b = " + (a.rem(b)))
    }

    Output:

    a + b = 24
    a - b = 16
    a * b = 80
    a / b = 5
    a % b = 0

    Relational Operators : These operators compare two values:

    OperatorMeaningExpressionTranslates to
    >Greater thana > ba.compareTo(b) > 0
    <Less thana < ba.compareTo(b) < 0
    >=Greater than or equal toa >= ba.compareTo(b) >= 0
    <=Less than or equal toa <= ba.compareTo(b) <= 0
    ==Equal toa == ba?.equals(b) ?: (b === null)
    !=Not equal toa != b!(a?.equals(b) ?: (b === null))

    Example:

    fun main(args: Array<String>) {
        var c = 30
        var d = 40
        println("c > d = " + (c > d))
        println("c < d = " + (c.compareTo(d) < 0))
        println("c >= d = " + (c >= d))
        println("c <= d = " + (c.compareTo(d) <= 0))
        println("c == d = " + (c == d))
        println("c != d = " + (!(c?.equals(d) ?: (d === null))))
    }

    Output:

    c > d = false
    c < d = true
    c >= d = false
    c <= d = true
    c == d = false
    c != d = true

    Assignment Operators : These operators are used to assign values to variables and also modify the current values.

    OperatorExpressionTranslates to
    +=a = a + ba.plusAssign(b)
    -=a = a - ba.minusAssign(b)
    *=a = a * ba.timesAssign(b)
    /=a = a / ba.divAssign(b)
    %=a = a % ba.remAssign(b)

    Example:

    fun main(args: Array<String>) {
        var a = 10
        var b = 5
        a += b
        println(a)
        a -= b
        println(a)
        a *= b
        println(a)
        a /= b
        println(a)
        a %= b
        println(a)
    }

    Output:

    15
    10
    50
    10
    0

    Unary Operators : Unary operators are applied to a single operand to modify its value.

    OperatorExpressionTranslates to
    ++++a or a++a.inc()
    ----a or a--a.dec()

    Example:

    fun main(args: Array<String>) {
        var e = 10
        println("Print then increment: " + e++)
        println("Increment then print: " + ++e)
        println("Print then decrement: " + e--)
        println("Decrement then print: " + --e)
    }

    Output:

    Print then increment: 10
    Increment then print: 12
    Print then decrement: 12
    Decrement then print: 10

    Logical Operators : Logical operators work with boolean values.

    OperatorMeaningExpression
    &&True if both are true(a > b) && (a > c)
    ` `
    !Negates the expressiona.not()

    Example:

    fun main(args: Array<String>) {
        var x = 100
        var y = 25
        var z = 10
        var result = false
        if (x > y && x > z) println(x)
        if (x < y || x > z) println(y)
        if (result.not()) println("Logical operators")
    }

    Output:

    100
    25
    Logical operators

    Bitwise Operators: Bitwise operators work directly on the bits of binary numbers.

    OperatorMeaningExpression
    shlSigned shift lefta.shl(b)
    shrSigned shift righta.shr(b)
    ushrUnsigned shift righta.ushr(b)
    andBitwise ANDa.and(b)
    orBitwise ORa.or(b)
    xorBitwise XORa.xor(b)
    invBitwise Inversea.inv()

    Example:

    fun main(args: Array<String>) {
        println("5 shifted left by 1: " + 5.shl(1))
        println("10 shifted right by 2: " + 10.shr(2))
        println("12 unsigned shifted right by 2: " + 12.ushr(2))
        println("36 AND 22: " + 36.and(22))
        println("36 OR 22: " + 36.or(22))
        println("36 XOR 22: " + 36.xor(22))
        println("Bitwise inverse of 14: " + 14.inv())
    }

    Output:

    5 shifted left by 1: 10
    10 shifted right by 2: 2
    12 unsigned shifted right by 2: 3
    36 AND 22: 4
    36 OR 22: 54
    36 XOR 22: 50
    Bitwise inverse of 14: -15

    Kotlin Standard Input/Output

    In Kotlin, standard input and output operations are used to transfer byte streams from input devices (like the keyboard) to the system’s memory and from memory to output devices (such as the monitor). This guide covers how to take input and display output using Kotlin.

    Kotlin Output

    In Kotlin, output is displayed using the print() and println() functions. Unlike Java, where we use System.out.println(), Kotlin directly provides print() and println().

    Here is an example of basic output:

    fun main(args: Array<String>) {
        print("Hello, World! ")
        println("Welcome to Kotlin.")
    }

    Output:

    Hello, World! Welcome to Kotlin.
    Difference Between print() and println()
    • print() displays the message inside the double quotes, but the cursor stays on the same line.
    • println() also displays the message but moves the cursor to the next line after printing.

    Example:

    fun main(args: Array<String>) {
        println("Kotlin Programming")
        println("Language Overview")
    
        print("Kotlin - ")
        print("Language")
    }

    Output:

    Kotlin Programming
    Language Overview
    Kotlin - Language
    Printing Literals and Variables

    You can print literals, variables, and even the result of function calls directly using string interpolation.

    Example:

    fun sum(a: Int, b: Int): Int {
        return a + b
    }
    
    fun main(args: Array<String>) {
        var x = 10
        var y = 20
        var z = 50L
        var score = 88.5
    
        println("Sum of $x and $y is: ${sum(x, y)}")
        println("Long value is: $z")
        println("Score: $score")
    }

    Output:

    Sum of 10 and 20 is: 30
    Long value is: 50
    Score: 88.5
    Kotlin Input

    Kotlin provides several ways to take input from the user.

    1. Using readLine(): The readLine() function reads input from the user as a string. It’s commonly used for string input but can be converted to other types like integers or doubles using conversion functions.

    Example:

    fun main(args: Array<String>) {
        print("Enter some text: ")
        var input = readLine()
        print("You entered: $input")
    }

    Output:

    Enter some text: Kotlin Input
    You entered: Kotlin Input

    2. Using the Scanner Class : For more advanced input handling, such as accepting numbers, we can use the Scanner class from Java. You need to import java.util.Scanner before using it.

    Example:

    import java.util.Scanner
    
    fun main(args: Array<String>) {
        val scanner = Scanner(System.`in`)
    
        print("Enter an integer: ")
        val intValue = scanner.nextInt()
        println("You entered: $intValue")
    
        print("Enter a float value: ")
        val floatValue = scanner.nextFloat()
        println("You entered: $floatValue")
    
        print("Enter a boolean: ")
        val boolValue = scanner.nextBoolean()
        println("You entered: $boolValue")
    }

    Output:

    Enter an integer: 10
    You entered: 10
    Enter a float value: 25.5
    You entered: 25.5
    Enter a boolean: true
    You entered: true

    3. Taking Input Without Using the Scanner Class : You can also take input without importing the Scanner class by using readLine() and converting the string to other data types.

    Example:

    fun main(args: Array<String>) {
        print("Enter an integer: ")
        val inputString = readLine()!!
        val intValue: Int = inputString.toInt()
        println("You entered: $intValue")
    
        print("Enter a double value: ")
        val doubleString = readLine()!!
        val doubleValue: Double = doubleString.toDouble()
        println("You entered: $doubleValue")
    }

    Output:

    Enter an integer: 42
    You entered: 42
    Enter a double value: 99.99
    You entered: 99.99

    Kotlin Type Conversion

    Type conversion, also known as type casting, refers to changing the data type of a variable into another type. In Java, implicit type conversion is allowed, meaning smaller data types can be automatically converted into larger ones. For instance, an integer value can be assigned to a long data type without any issues.

    Java Example of Implicit Type Conversion:

    public class TypeCastingExample {
        public static void main(String args[]) {
            byte p = 12;
            System.out.println("byte value: " + p);
    
            // Implicit type conversion
            long q = p;  // Integer value can be assigned to long
        }
    }
    Kotlin Example of Explicit Type Conversion:

    However, in Kotlin, implicit type conversion is not allowed. You cannot directly assign an integer value to a long variable.

    var myNumber = 100
    var myLongNumber: Long = myNumber  // This will cause a compiler error
    // Type mismatch: inferred type is Int but Long was expected

    In Kotlin, explicit type conversion can be achieved using helper functions.

    var myNumber = 100
    var myLongNumber: Long = myNumber.toLong()  // This will compile successfully
    Kotlin Type Conversion Helper Functions:

    Kotlin provides several helper functions to convert one data type to another:

    • toByte()
    • toShort()
    • toInt()
    • toLong()
    • toFloat()
    • toDouble()
    • toChar()

    Note: There is no helper function to convert directly to a Boolean type.

    Conversion from Larger to Smaller Data Types:

    var myLongNumber = 10L
    var myNumber2: Int = myLongNumber.toInt()

    Example:

    fun main(args: Array<String>) {
        println("259 to byte: " + (259.toByte()))
        println("50000 to short: " + (50000.toShort()))
        println("21474847499 to Int: " + (21474847499.toInt()))
        println("10L to Int: " + (10L.toInt()))
        println("22.54 to Int: " + (22.54.toInt()))
        println("22 to float: " + (22.toFloat()))
        println("65 to char: " + (65.toChar()))
        // Char to Number is deprecated in Kotlin
        println("A to Int: " + ('A'.toInt()))
    }

    Output:

    259 to byte: 3
    50000 to short: -15536
    21474847499 to Int: 11019
    10L to Int: 10
    22.54 to Int: 22
    22 to float: 22.0
    65 to char: A
    A to Int: 65

    Kotlin Expression, Statement and Block

    An expression in Kotlin consists of variables, operators, method calls, and other elements that produce a value. Expressions are building blocks of a program, often created to compute new values or assign them to variables. It’s worth noting that expressions can also contain other expressions.

    A few things to note:

    • A variable declaration is not an expression (e.g., var a = 100).
    • Assigning a value is not an expression (e.g., b = 15).
    • A class declaration is not an expression (e.g., class XYZ { ... }).

    In Kotlin, every function returns at least a Unit type, meaning every function is treated as an expression.

    Here is an example of Kotlin expressions:

    fun sumOf(a: Int, b: Int): Int {
        return a + b
    }
    
    fun main(args: Array<String>) {
        val a = 10
        val b = 5
        val sum = sumOf(a, b)
        val mul = a * b
        println(sum)
        println(mul)
    }

    Output:

    15
    50
    Kotlin if Expression

    Unlike Java, where if is a statement, in Kotlin, if is an expression. It evaluates a condition and returns a value based on the result. This is why Kotlin doesn’t have a ternary operator like (a > b) ? a : b, as the if expression serves this purpose.

    if (condition) conditionMet else conditionNotMet

    Example: Here’s an example to find the maximum of two values:

    if (condition) conditionMet else conditionNotMet

    Output:

    if (condition) conditionMet else conditionNotMet
    Kotlin Statements

    A statement is a syntactic unit in programming that expresses an action to be carried out. Programs are made up of one or more statements. While Java requires a semicolon at the end of every statement, Kotlin makes this optional.

    Examples of statements:

    • Declaring a variable: val marks = 90
    • Assigning a value: var sum = 10 + 20

    In the example var sum = 10 + 20, the part 10 + 20 is an expression, while var sum = 10 + 20 is a statement.

    Multiple Statements: Multiple statements can be written on a single line.

    fun main(args: Array<String>) {
        val sum: Int
        sum = 100
        println(sum)                       // single statement
        println("Hello"); println("World")  // multiple statements
    }

    Output:

    100
    Hello
    World
    Kotlin Block

    A block in Kotlin is a section of code enclosed in curly braces { ... }. A block can consist of one or more statements, often with variable declarations. Blocks can also be nested. Each function in Kotlin has its own block, and the main function also contains a block.

    Here’s an example of a block with a nested block:

    fun main(args: Array<String>) {              // start of outer block
         val array = intArrayOf(2, 4, 6, 8)
         for (element in array) {                // start of inner block
            println(element)
         }                                       // end of inner block
    }                                            // end of outer block

    Output:

    2
    4
    6
    8
  • Overview

    Introduction

    Kotlin is a statically typed, general-purpose programming language created by JetBrains, renowned for developing top-notch IDEs like IntelliJ IDEA, PhpStorm, and AppCode. First introduced in 2011, Kotlin is a new language for the JVM (Java Virtual Machine) that offers an object-oriented programming paradigm. It is often regarded as a “better language” than Java while maintaining full interoperability with existing Java code.

    In 2017, Google endorsed Kotlin as one of the official languages for Android development.

    Example of Kotlin:

    fun main() {
        println("Hello World")
    }
    Key Features of Kotlin:

    1. Statically Typed: In Kotlin, the type of every variable and expression is determined at compile time. Although it is a statically typed language, it doesn’t require you to explicitly define the type for every variable.
    2. Data Classes: Kotlin includes Data Classes that automatically generate boilerplate code such as equals, hashCode, toString, and getters/setters. For example:

    Java Code:

    class Book {
        private String title;
        private Author author;
    
        public String getTitle() {
            return title;
        }
    
        public void setTitle(String title) {
            this.title = title;
        }
    
        public Author getAuthor() {
            return author;
        }
    
        public void setAuthor(Author author) {
            this.author = author;
        }
    }

    Output:

    data class Book(var title: String, var author: Author)
    • Conciseness: Kotlin significantly reduces the amount of code required compared to other object-oriented programming languages.
    • Safety: Kotlin helps prevent the notorious NullPointerExceptions by incorporating nullability into its type system. By default, every variable in Kotlin is non-null.
    val s: String = "Hello World" // Non-null
    // The following line will produce a compile-time error:
    // s = null

    To assign a null value, a variable must be declared as nullable:

    var nullableStr: String? = null // Compiles successfully
    • The length() function is also disabled for nullable strings.
    • Interoperability with Java: Since Kotlin runs on the JVM, it is fully interoperable with Java, allowing seamless access to Java code from Kotlin and vice versa.
    • Functional and Object-Oriented Capabilities: Kotlin offers a rich set of features, including higher-order functions, lambda expressions, operator overloading, and lazy evaluation. A higher-order function is one that takes a function as a parameter or returns a function.Example of a Higher-Order Function:
    fun myFun(company: String, product: String, fn: (String, String) -> String): Unit {
        val result = fn(company, product)
        println(result)
    }
    
    fun main() {
        val fn: (String, String) -> String = { org, portal -> "$org develops $portal" }
        myFun("JetBrains", "Kotlin", fn)
    }

    Smart Casts: Kotlin automatically typecasts immutable values and ensures safe casting.

    fun main() {
        var string: String? = "BYE"
        // This line will produce a compile-time error:
        // print(string.length)
    }

    With smart casting:

    fun main() {
        var string: String? = "BYE"
        if (string != null) { // Smart cast
            print(string.length)
        }
    }

    1. Compilation Time: Kotlin has a high performance and fast compilation time.
    2. Tool-Friendly: Kotlin has excellent support in various IDEs. You can run Kotlin programs using any Java IDE such as IntelliJ IDEA, Eclipse, and Android Studio, as well as from the command line.

    Advantages of Kotlin:

    • Easy to Learn: Kotlin’s syntax is similar to Java, making it easy for Java developers to pick up quickly.
    • Multi-Platform Support: Kotlin can be used across all Java IDEs, enabling program writing and execution on any machine that supports the JVM.
    • Increased Safety: Kotlin provides enhanced safety features compared to Java.
    • Java Framework Compatibility: You can use existing Java frameworks and libraries in your Kotlin projects without needing to rewrite them in Java.
    • Open Source: The Kotlin programming language, along with its compiler, libraries, and tools, is entirely free and open source, available on GitHub.

    Applications of Kotlin:

    • Kotlin is used for building Android applications.
    • It can compile to JavaScript, making it suitable for front-end development.
    • Kotlin is also well-suited for web development and server-side programming.

    Basic Kotlin Example

    “Hello, World!” is often the first basic program written when learning any new programming language. Let’s go through how to write this introductory program in Kotlin.

    The “Hello, World!” Program in Kotlin:

    Start by opening your preferred text editor, such as Notepad or Notepad++, and create a file named firstapp.kt with the following code:

    // Kotlin Hello World Program
    fun main(args: Array<String>) {
        println("Hello, World!")
    }

    Compiling the Program:

    To compile the Kotlin program using the command-line compiler, use the following command:

    $ kotlinc firstapp.kt

    Running the Program:

    Once the program is compiled, run it using this command to see the output:

    $ kotlin firstapp.kt

    Expected Output:

    Hello, World!

    Note: You can also run this program using IntelliJ IDEA by following the steps in a relevant environment setup guide.

    Understanding the “Hello, World!” Program:

    • Line 1: The first line is a comment, which is ignored by the compiler. Comments are added to the program to make the code easier to read and understand for developers and users.

    Kotlin supports two types of comments:

    1. Single-line comment:

    // This is a single-line comment

    2. Multi-line comment:

    /*
       This is a
       multi-line comment
    */

    Line 2: The second line defines the main function, which is the entry point of every Kotlin program

    fun main(args: Array<String>) {
        // Function body
    }

    The main function is where the program starts executing. In Kotlin, all functions begin with the fun keyword, followed by the function name (in this case, main), a list of parameters (here, an array of strings args), and the function body enclosed in curly braces { ... }. The Unit type in Kotlin, which is analogous to void in Java, indicates that the function does not return a value.

    • Line 3: The third line is a statement that prints “Hello, World!” to the console.
    println("Hello, World!")
  • Kotlin Tutorial Roadmap

    Overview

    • Kotlin Programming Language (Introduction)
    • Advantages of Kotlin
    • Applications of Kotlin
    • Basic Kotlin Example

    Basic Concepts

    • Data Types
    • Variables
    • Operators
    • Standard Input/Output
    • Type Conversion
    • Expression, Statement and Block

    Control Statements

    • if-else expression
    • while loop
    • do-while loop
    • when expression
    • Unlabelled break
    • labelled continue

    Array & String

    • Kotlin Array
    • Kotlin String

    Functions

    • Default and Named argument
    • Recursion
    • Tail Recursion
    • Lambdas
    • Expressions and Anonymous Functions
    • Inline Functions
    • Higher-Order Functions

    Collections

    • ArrayList
    • listOf()
    • HashMap

    Class and Objects

    • Classes and Object
    • Nested Class and Inner Class
    • Setters and Getters
    • Class Properties and Custom Accessors
    • Constructor
    • Modifiers
    • Inheritance
    • Interfaces
    • Sealed Classes
    • Enum Classes
    • Extension
    • Functions
    • Generics

    Null Safety

    • Type Checking and Smart Casting
    • Explicit type casting

    Regex & Ranges

    • Regular Expression
    • Ranges

    Miscellaneous

    • Kotlin Annotations
    • Overview
    • Kotlin Reflection
    • Operator
    • Overloading
    • Destructuring
    • Declarations in Kotlin
    • Equality evaluation
    • Comparator
    • Triple
    • Pair
    • apply vs with