Contents

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

A 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: A 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 WebDeveloper, iOSDeveloper, 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 WebDeveloper, iOSDeveloper, 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