Share
Explore

KOTLIN Coding Drills Lab Book

Pillars of Object Orientation:

Day 02 Learning Outcomes

megaphone

Introduction to Object Oriented Programming

Defining a class
Declaring class properties
The “this” keyword
Declaring class functions : methods or operations that the class performs
Constructors : special method that you run instantiate an object from a class
Creating an instance of a class : what you do when you run the constructor
Class → Recipe, set of assembly instructions
OBJECT → cake : the realized version o your design

class Person {
var name: String = ""
var age: Int = 0

// Method to print info with some color
fun printInfo() {
println("\u001B[34mName: $name, Age: $age\u001B[0m")
}

// Method to celebrate a birthday with a fun message
fun celebrateBirthday() {
this.age += 1
println("\u001B[33mHappy Birthday, ${this.name}! You are now ${this.age} years old.\u001B[0m")
}

// Method for a custom greeting
fun greet() {
println("\u001B[32mHello, $name! Welcome to our community!\u001B[0m")
}

// Method to display a brief life story
fun lifeStory() {
println("\u001B[35m$name's Life Story: \n$name was born $age years ago. Over the years, $name has experienced many adventures and learned a lot. Now, at the age of $age, $name is excited about the future!\u001B[0m")
}
}

fun main() {
val person1 = Person().apply {
name = "Alice"
age = 25
}

val person2 = Person().apply {
name = "Bob"
age = 30
}

person1.printInfo()
person1.celebrateBirthday()
person1.greet()
person1.lifeStory()

person2.printInfo()
person2.celebrateBirthday()
person2.greet()
person2.lifeStory()
}

megaphone

The `apply` function in Kotlin is a scope function that allows you to configure an object within a block of code and then return the object itself. It’s particularly useful for initializing or configuring an object without having to reference the object’s name multiple times.

### What `apply` is Doing
In your code, `apply` is being used to set the properties `name` and `age` of `Person` objects in a more concise way. Here’s a breakdown of how it works:
1. **Create a new `Person` object**. 2. **Call `apply` on the newly created object**. 3. **Inside the `apply` block, refer to the object using `this` implicitly** (so you don’t need to use the object’s name to set its properties). 4. **Return the object itself after the block is executed**.
### Example Code with Explanation
```kotlin class Person { var name: String = "" var age: Int = 0
fun printInfo() { println("Name: $name, Age: $age") }
fun celebrateBirthday() { this.age += 1 println("Happy Birthday, ${this.name}! You are now ${this.age} years old.") }
fun greet() { println("Hello, $name! Welcome to our community!") }
fun lifeStory() { println("$name's Life Story: \n$name was born $age years ago. Over the years, $name has experienced many adventures and learned a lot. Now, at the age of $age, $name is excited about the future!") } }
fun main() { val person1 = Person().apply { name = "Alice" age = 25 }
val person2 = Person().apply { name = "Bob" age = 30 }
person1.printInfo() person1.celebrateBirthday() person1.greet() person1.lifeStory()
person2.printInfo() person2.celebrateBirthday() person2.greet() person2.lifeStory() } ```
### What Happens in Detail
1. **Creating `person1`**: ```kotlin val person1 = Person().apply { name = "Alice" age = 25 } ``` - A new `Person` object is created. - `apply` is called on this object. - Inside the `apply` block, `name` is set to "Alice" and `age` is set to 25. - The `apply` function returns the `Person` object, now initialized with `name` and `age`.
2. **Creating `person2`**: ```kotlin val person2 = Person().apply { name = "Bob" age = 30 } ``` - Similar steps as above, but for a different `Person` object with `name` set to "Bob" and `age` set to 30.
3. **Calling Methods on `person1` and `person2`**: - `printInfo()`, `celebrateBirthday()`, `greet()`, and `lifeStory()` methods are called on both `person1` and `person2` to demonstrate their initialized state and behavior.
### Why Use `apply`
- **Readability**: It makes the code more readable by reducing the repetition of the object’s name. - **Convenience**: It allows you to perform multiple operations on the object in a single block of code.
### Example without `apply`
If you didn’t use `apply`, you would have to write more verbose code:
```kotlin val person1 = Person() person1.name = "Alice" person1.age = 25
val person2 = Person() person2.name = "Bob" person2.age = 30
person1.printInfo() person1.celebrateBirthday() person1.greet() person1.lifeStory()
person2.printInfo() person2.celebrateBirthday() person2.greet() person2.lifeStory() ```
Using `apply` simplifies this process, making the initialization and configuration more compact and readable.

Here are six progressive lab drills designed to teach you the various aspects of defining a Kotlin class.
Each drill builds on the previous one, gradually introducing new concepts and complexity.
info
These lab drills cover:
Basic class definition and properties.
Primary constructors.
Secondary constructors.
Custom methods.
Default values and initialization blocks.
Data classes and additional methods.
By following these steps, students will progressively understand how to define and work with classes in Kotlin, gaining a solid foundation in object-oriented programming principles.

Lab 1: Basic Class Definition

Objective: Introduce the basic structure of a Kotlin class, including properties and a simple method.
Instructions:
Define a class Person with two properties: name (String) and age (Int).
Create a method printInfo() to print the name and age.
Code:

class Person {
var name: String = ""
var age: Int = 0

fun printInfo() {
println("Name: $name, Age: $age")
}
}

fun main() {
val person = Person()
person.name = "John"
person.age = 25
person.printInfo()
}


Lab 2: Primary Constructor

Objective: Introduce the use of a primary constructor to initialize properties.
Instructions:
Modify the Person class to use a primary constructor for initializing name and age.
Update the printInfo() method accordingly.
Code:

class Person(val name: String, var age: Int) {

fun printInfo() {
println("Name: $name, Age: $age")
}
}

fun main() {
val person = Person("Alice", 30)
person.printInfo()
}


Lab 3: Secondary Constructor

Objective: Introduce the use of a secondary constructor for additional initialization logic.
Instructions:
Add a secondary constructor to the Person class that only takes name and sets age to a default value.
Demonstrate using both constructors.
Code:

class Person(val name: String, var age: Int) {

constructor(name: String) : this(name, 0)

fun printInfo() {
println("Name: $name, Age: $age")
}
}

fun main() {
val person1 = Person("Bob", 40)
person1.printInfo()

val person2 = Person("Charlie")
person2.printInfo()
}


megaphone

More play with secondary constructors

image.png
To execute specific code when the secondary constructor is called in Kotlin, you can include the additional logic within the secondary constructor block.
Let's modify thePerson class to demonstrate how this can be done.

Step-by-Step Explanation

Define the Primary Constructor: The primary constructor initializes the name and age properties.
Define the Secondary Constructor: The secondary constructor initializes the name property and includes additional logic specific to this constructor.
Call the Primary Constructor: The secondary constructor must call the primary constructor using the this keyword.

Modified Code Example

kotlin
Copy code
class Person(val name: String, var age: Int) {

// Primary constructor
constructor(name: String) : this(name, 0) {
println("Primary constructor called for $name with default age 0")
}

// Secondary constructor
constructor(name: String, vacayPreference: String) : this(name) {
println("Secondary constructor called for $name with vacation preference: $vacayPreference")
// Additional logic specific to the secondary constructor
if (vacayPreference.isNotEmpty()) {
println("$name prefers vacationing in $vacayPreference.")
}
}

fun printInfo() {
println("Name: $name, Age: $age")
}
}

fun main() {
val person1 = Person("Bob", 40)
person1.printInfo()

val person2 = Person("Charlie")
person2.printInfo()

val person3 = Person("Mike", "Hawaii")
person3.printInfo()
}

Explanation of the Code

Primary Constructor:
Initializes name and age.
Additional logic can be added here if needed (e.g., default values).
Secondary Constructor:
Calls the primary constructor using this(name).
Adds specific logic for the secondary constructor, such as handling the vacayPreference parameter.
Prints additional messages or performs other actions specific to this constructor.
Main Function:
Creates instances using different constructors to demonstrate both primary and secondary constructors.
Calls printInfo() to display the initialized values and any additional messages.

Output

Running this code will produce the following output:
sql
Copy code
Primary constructor called for Bob with default age 0
Name: Bob, Age: 40
Primary constructor called for Charlie with default age 0
Name: Charlie, Age: 0
Primary constructor called for Mike with default age 0
Secondary constructor called for Mike with vacation preference: Hawaii
Mike prefers vacationing in Hawaii.
Name: Mike, Age: 0

This output shows that the secondary constructor adds specific behavior for the vacayPreference parameter while still initializing the object through the primary constructor.

Lab 4: Custom Methods

Objective: Add custom methods to demonstrate how we can perform operations on classes.
Instructions:
Add a celebrateBirthday() method to the Person class to increase the age by 1 and print a birthday message.
Update the main() function to demonstrate this method.
Code:

class Person(val name: String, var age: Int) {

constructor(name: String) : this(name, 0)

fun printInfo() {
println("Name: $name, Age: $age")
}

fun celebrateBirthday() {
age += 1
println("Happy Birthday, $name! You are now $age years old.")
}
}

fun main() {
val person = Person("David", 35)
person.printInfo()
person.celebrateBirthday()
person.printInfo()
}



Lab 5: Default Values and Initialization Blocks

Objective: Use default values and initialization blocks for property (class data field) initialization and additional setup logic.
Instructions:
Add default values for the properties.
Use an init block to print a welcome message when a Person object is created.

megaphone

Let's delve into the role and significance of the `init` block in Kotlin.

### The Role of the `init` Block
The `init` block in Kotlin is used for initializing an instance of a class. It is a special block of code that gets executed immediately after the primary constructor. Here's a detailed look at its role and importance:
1. **Initialization Logic**: - The `init` block allows you to run additional setup or initialization logic after the primary constructor has run. - It is especially useful when you need to perform tasks that are common to all constructors.
2. **Multiple `init` Blocks**: - A Kotlin class can contain multiple `init` blocks. - These blocks are executed in the order they appear in the class, after the primary constructor.
3. **Access to Constructor Parameters**: - The `init` block can access the parameters of the primary constructor directly. - This allows for flexible initialization based on the constructor arguments.
4. **Complementing Property Initialization**: - While properties can have default values or be initialized directly, the `init` block is ideal for more complex initialization logic that cannot be handled by simple assignment.
5. **Consistency in Initialization**: - By using the `init` block, you ensure that certain code runs whenever an instance is created, maintaining consistency across different constructor usages.
### Example and Explanation
Consider the following example where we use an `init` block to print a welcome message whenever a `Person` object is created:
```kotlin class Person(val name: String, var age: Int) {
init { println("Welcome, $name!") }
fun printInfo() { println("Name: $name, Age: $age") } }
fun main() { val person1 = Person("Alice", 30) person1.printInfo()
val person2 = Person("Bob", 25) person2.printInfo() } ```
Explanation of the Example
1. **Class Definition**: - The `Person` class has a primary constructor with two parameters: `name` and `age`.
2. **`init` Block**: - The `init` block is defined right after the primary constructor. - It prints a welcome message that includes the person's name.
3. **Object Creation**: - When `Person("Alice", 30)` is called, the `init` block executes immediately after the primary constructor, printing "Welcome, Alice!". - The same happens for `Person("Bob", 25)`, printing "Welcome, Bob!".
### Use Cases for `init` Block
1. **Logging or Debugging**: - Printing logs or debug statements during object initialization. 2. **Validation**: - Checking and validating constructor parameters. - Throwing exceptions if the parameters are invalid. 3. **Complex Initialization**: - Performing complex calculations or setup tasks that cannot be done through simple property assignments. 4. **Resource Management**: - Initializing resources that are required for the object's lifecycle, such as database connections or network clients.

Combining `init` with Secondary Constructors
When using secondary constructors, the `init` block still runs after the primary constructor but before the secondary constructor completes:
```kotlin class Person(val name: String, var age: Int) {
init { println("Welcome, $name!") }
constructor(name: String) : this(name, 0) { println("$name is initialized with age 0.") }
fun printInfo() { println("Name: $name, Age: $age") } }
fun main() { val person = Person("Charlie") person.printInfo() } ```
In this example: - The `init` block runs and prints "Welcome, Charlie!". - Then, the secondary constructor runs, printing "Charlie is initialized with age 0.".
### Conclusion
The `init` block in Kotlin is a powerful feature for running initialization code.
It ensures that specific logic is executed every time an object is created, maintaining consistency and allowing for complex setup tasks that go beyond simple property assignments. Understanding and utilizing the `init` block effectively can lead to more robust and maintainable Kotlin applications.
Code:

class Person(val name: String = "Unknown", var age: Int = 0) {

init {
println("Welcome, $name!")
}

fun printInfo() {
println("Name: $name, Age: $age")
}

fun celebrateBirthday() {
age += 1
println("Happy Birthday, $name! You are now $age years old.")
}
}

fun main() {
val person = Person()
person.printInfo()

val anotherPerson = Person("Eva", 28)
anotherPerson.printInfo()
}


Lab 6: Data Classes and Additional Methods

Objective: Introduce data classes and their benefits, such as automatically generated methods (toString, equals, hashCode).
Instructions:
Convert the Person class into a data class.
Add a method to compare the ages of two Person objects and print who is older.
Code:
data class Person(val name: String = "Unknown", var age: Int = 0) {

init {
println("Welcome, $name!")
}

fun printInfo() {
println("Name: $name, Age: $age")
}

fun celebrateBirthday() {
age += 1
println("Happy Birthday, $name! You are now $age years old.")
}

fun compareAge(other: Person) {
when {
this.age > other.age -> println("$name is older than ${other.name}.")
this.age < other.age -> println("$name is younger than ${other.name}.")
else -> println("$name and ${other.name} are the same age.")
}
}
}

fun main() {
val person1 = Person("Frank", 25)
val person2 = Person("Grace", 30)

person1.printInfo()
person2.printInfo()

person1.celebrateBirthday()

person1.compareAge(person2)
person2.compareAge(person1)
}


megaphone

Polymorphism, Method Overloading and Method Overriding

megaphone

Lab exercise that illustrates and teaches about polymorphism, method overloading, and method overriding in Kotlin.

We'll use an interesting example involving a basic shape drawing application.


Method Overloading: same method name but different Method Signatures
Method Overriding: change the implemention of a Method in a Sub Class

Lab Exercise: Shape Drawing Application
Objectives: 1. Understand and implement polymorphism. 2. Learn and apply method overloading. 3. Learn and apply method overriding.
Step-by-Step Instructions
Step 1: Create a Base Class and Subclasses
1. **Define a base class `Shape` with a method `draw`.** 2. **Create subclasses `Circle`, `Rectangle`, and `Triangle` that inherit from `Shape` and override the `draw` method.**
```kotlin ​open class Shape { open fun draw() { println("Drawing a shape") } }
class Circle : Shape() { override fun draw() { println("Drawing a circle") } }
class Rectangle : Shape() { override fun draw() { println("Drawing a rectangle") } }
class Triangle : Shape() { override fun draw() { println("Drawing a triangle") } } ```
Step 2: Implement Method Overloading
1. **Add a `draw` method in the `Shape` class that accepts different parameters for customization.**
```kotlin open class Shape { open fun draw() { println("Drawing a shape") }
fun draw(color: String) { println("Drawing a shape in color $color") }
fun draw(color: String, size: Int) { println("Drawing a shape in color $color with size $size") } } ```

Step 3: Demonstrate Polymorphism
1. **Create a function `drawShapes` that accepts a list of `Shape` objects and calls their `draw` method.**
Want to print your doc?
This is not the way.
Try clicking the ⋯ next to your doc name or using a keyboard shortcut (
CtrlP
) instead.