Kotlin Collections and OOP: List, Set, Map, Classes, Inheritance & Polymorphism
1. Collections in Kotlin
What are collections in Kotlin?
Collections are data structures to store and manipulate multiple items. Kotlin provides List, Set, and Map, with immutable and mutable variants.
Types:
- List: Ordered, allows duplicates (
List,MutableList). - Set: Unordered, no duplicates (
Set,MutableSet). - Map: Key-value pairs, unique keys (
Map,MutableMap).
Immutable vs. Mutable:
- Immutable:
listOf,setOf,mapOf(read-only). - Mutable:
mutableListOf,mutableSetOf,mutableMapOf(modifiable).
Key Features: Null safety (e.g., List<Int?> for nullable elements), rich standard library functions like map, filter, groupBy.
Use Case: Storing and processing data (e.g., user lists, configuration maps).
Example:
// Collections example
fun main() {
// Immutable List
val numbers: List<Int> = listOf(1, 2, 3, 2)
println("List: $numbers")
println("Filtered (even): ${numbers.filter { it % 2 == 0 }}")
// Mutable Set
val uniqueNames: MutableSet<String> = mutableSetOf("Krishna", "Ram", "Krishna")
uniqueNames.add("Kristal")
println("Set: $uniqueNames")
// Mutable Map
val salaries: MutableMap<String, Int> = mutableMapOf("Krishna" to 50000, "Ram" to 60000)
salaries["Kristal"] = 70000
println("Map: $salaries")
println("Names with salary > 55000: ${salaries.filterValues { it > 55000 }}")
}
List: [1, 2, 3, 2]
Filtered (even): [2, 2]
Set: [Krishna, Ram, Kristal]
Map: {Krishna=50000, Ram=60000, Kristal=70000}
Names with salary > 55000: {Ram=60000, Kristal=70000}
Note: listOf creates an immutable list; filter extracts even numbers. mutableSetOf ensures unique elements; add modifies the set. mutableMapOf stores key-value pairs; filterValues filters by value.
2. Classes and Objects
What are classes and objects in Kotlin?
Class: A blueprint for objects, defining properties and functions. Syntax: class ClassName { ... }.
Object: An instance of a class, created using the class constructor. Creation: val obj = ClassName().
Use Case: Modeling entities like employees, vehicles, or configurations.
Example:
// Classes and objects example
class Employee(val name: String, val salary: Int) {
fun displayInfo(): String {
return "Name: $name, Salary: $$salary"
}
}
fun main() {
val emp1 = Employee("Krishna", 60000)
val emp2 = Employee("Ram", 55000)
println(emp1.displayInfo())
println(emp2.displayInfo())
}
Name: Krishna, Salary: $60000
Name: Ram, Salary: $55000
Note: Employee class has properties name and salary, initialized in the primary constructor. displayInfo returns a formatted string. Objects emp1 and emp2 are instances with unique data.
3. init Blocks
What are init blocks in Kotlin?
init blocks are executed during object initialization, after the primary constructor. They are used for setup or validation logic.
Syntax: init { ... }.
Use Case: Initializing complex properties or enforcing constraints.
Example:
// init block example
class Car(val brand: String, val model: String) {
var fullName: String
init {
fullName = "$brand $model"
println("Initialized: $fullName")
}
fun carInfo(): String = "Car: $fullName"
}
fun main() {
val car1 = Car("Toyota", "Camry")
val car2 = Car("Honda", "Civic")
println(car1.carInfo())
println(car2.carInfo())
}
Initialized: Toyota Camry
Initialized: Honda Civic
Car: Toyota Camry
Car: Honda Civic
Note: init block sets fullName based on constructor parameters. Runs automatically during object creation.
4. Instance vs. Class (Companion) Variables
What are instance and class (companion) variables in Kotlin?
Instance Variables: Unique to each object, defined in the class body or constructor (e.g., val name in an Employee object).
Class (Companion) Variables: Shared across all instances, defined in a companion object. Syntax: companion object { val/var name }.
Use Case: Instance variables for object-specific data; companion variables for shared data.
Example:
// Instance vs. companion variables
class Employee(val name: String, val salary: Int) {
companion object {
const val COMPANY_NAME = "TechCorp"
}
fun info(): String = "$name at $COMPANY_NAME, Salary: $$salary"
}
fun main() {
val emp1 = Employee("Krishna", 60000)
val emp2 = Employee("Ram", 55000)
println(emp1.info())
println(emp2.info())
println("Company: ${Employee.COMPANY_NAME}")
}
Krishna at TechCorp, Salary: $60000
Ram at TechCorp, Salary: $55000
Company: TechCorp
Note: name and salary are instance variables, unique per object. COMPANY_NAME is a companion variable, shared across all instances. const val ensures compile-time constant for efficiency.
5. Inheritance and Method Overriding
What are inheritance and method overriding in Kotlin?
Inheritance: A class (subclass) inherits properties and functions from another class (superclass). Syntax: open class Base { ... } and class Derived : Base() { ... }. Classes are final by default; open allows inheritance.
Method Overriding: Subclass provides a specific implementation of a superclass function. Syntax: override fun in subclass; superclass function must be open.
Use Case: Extending behavior (e.g., specialized employee types).
Example:
// Inheritance and method overriding
open class Person(val name: String) {
open fun describe(): String = "Person: $name"
}
class Employee(name: String, val salary: Int) : Person(name) {
override fun describe(): String = "Employee: $name, Salary: $$salary"
}
fun main() {
val person = Person("Kristal")
val employee = Employee("Krishna", 60000)
println(person.describe())
println(employee.describe())
}
Person: Kristal
Employee: Krishna, Salary: $60000
Note: Person is open to allow inheritance; describe is open for overriding. Employee inherits name and overrides describe for specific behavior. super.describe() can call the superclass method if needed.
6. Polymorphism and Encapsulation
What are polymorphism and encapsulation in Kotlin?
Polymorphism: Objects of different classes can be treated as instances of a common superclass, with methods behaving differently. Types: Compile-time (function overloading) and runtime (method overriding).
Encapsulation: Restricting access to properties and functions using visibility modifiers (private, protected, internal, public (default)). Private properties use private set or backing fields for controlled access.
Use Case: Polymorphism for flexible interfaces; encapsulation for data protection.
Example:
// Polymorphism and encapsulation
open class Employee(val name: String, private var salary: Int) {
open fun describe(): String = "Employee: $name"
fun getSalary(): Int = salary
}
class Manager(name: String, salary: Int) : Employee(name, salary) {
override fun describe(): String = "Manager: $name"
}
fun main() {
val employees: List<Employee> = listOf(Employee("Krishna", 60000), Manager("Ram", 80000))
// Polymorphism
employees.forEach { println(it.describe()) }
// Encapsulation
val emp = Employee("Kristal", 55000)
println("Salary: ${emp.getSalary()}")
// emp.salary = 1000 // Error: private property
}
Employee: Krishna
Manager: Ram
Salary: 55000
Note: Polymorphism: describe varies by class (Employee vs. Manager). Encapsulation: salary is private, accessed via getSalary. List<Employee> holds both types, demonstrating runtime polymorphism.
7. Class and Static (Companion) Methods
What are class and static (companion) methods in Kotlin?
Class Methods: Functions in a companion object, accessed via the class name. Syntax: companion object { fun method() }. Equivalent to Java's static methods but more flexible.
Static Behavior: Use @JvmStatic for true static methods in JVM interop.
Use Case: Utility functions or factory methods not tied to instances.
Example:
// Companion methods example
class Employee(val name: String) {
companion object {
const val COMPANY_NAME = "TechCorp"
fun createFromString(data: String): Employee {
return Employee(data.split(",")[0])
}
@JvmStatic
fun isValidName(name: String): Boolean = name.length >= 2
}
}
fun main() {
val emp = Employee.createFromString("Krishna,60000")
println("Employee: ${emp.name}, Company: ${Employee.COMPANY_NAME}")
println("Is 'A' valid? ${Employee.isValidName("A")}")
println("Is 'Ram' valid? ${Employee.isValidName("Ram")}")
}
Employee: Krishna, Company: TechCorp
Is 'A' valid? false
Is 'Ram' valid? true
Note: createFromString is a factory method in the companion object. isValidName mimics static behavior with @JvmStatic. Companion object members are accessed via Employee.
8. Special Methods (e.g., toString, componentN)
What are special methods in Kotlin?
Special methods customize object behavior, often in data classes or via operator overloading. Examples include toString() for string representation and componentN() for destructuring declarations.
Special Methods: toString(), componentN(), equals, hashCode, copy (in data classes).
Use Case: Enhancing object usability in printing, destructuring, or comparisons.
Example:
// Special methods example
data class Team(val name: String, val members: Int) {
override fun toString(): String = "Team(name=$name, members=$members)"
}
fun main() {
val team = Team("Coders", 3)
// toString
println(team)
// Destructuring (componentN)
val (name, members) = team
println("Name: $name, Members: $members")
}
Team(name=Coders, members=3)
Name: Coders, Members: 3
Note: data class auto-generates toString, componentN, equals, etc. override fun toString() customizes the output format. Destructuring uses component1, component2 automatically.
9. Comprehensive Example Combining Collections and OOP
Example:
// Comprehensive collections and OOP example
open class Person(val name: String) {
companion object {
const val GROUP = "Community"
fun createFromString(data: String): Person = Person(data)
}
open fun describe(): String = "Person: $name"
}
class Employee(name: String, private var salary: Int) : Person(name) {
init {
if (salary <= 0) throw IllegalArgumentException("Salary must be positive")
}
override fun describe(): String = "Employee: $name, Salary: $$salary"
fun getSalary(): Int = salary
companion object {
fun isValidSalary(salary: Int): Boolean = salary > 0
}
}
fun main() {
// Collections
val employees: MutableList<Employee> = mutableListOf(
Employee("Krishna", 60000),
Employee("Ram", 55000)
)
employees.add(Employee("Kristal", 70000))
val salaryMap: Map<String, Int> = employees.associate { it.name to it.getSalary() }
val highEarners: Set<String> = employees.filter { it.getSalary() > 60000 }.map { it.name }.toSet()
// Polymorphism
val people: List<Person> = listOf(Person("Dave"), employees[0], employees[1])
println("Descriptions:")
people.forEach { println(it.describe()) }
// Collections output
println("\nSalary Map: $salaryMap")
println("High Earners: $highEarners")
// Companion methods
println("\nIs 0 valid salary? ${Employee.isValidSalary(0)}")
println("Group: ${Person.GROUP}")
// Special methods (data class)
data class Team(val name: String, val size: Int)
val team = Team("Devs", employees.size)
val (teamName, teamSize) = team
println("\nTeam: $team")
println("Team Name: $teamName, Size: $teamSize")
}
Descriptions:
Person: Dave
Employee: Krishna, Salary: $60000
Employee: Ram, Salary: $55000
Salary Map: {Krishna=60000, Ram=55000, Kristal=70000}
High Earners: [Kristal]
Is 0 valid salary? false
Group: Community
Team: Team(name=Devs, size=3)
Team Name: Devs, Size: 3
Description: Collections: Uses MutableList for employees, Map for salaries, Set for high earners. Classes/Objects: Person and Employee with objects created. init Block: Validates salary in Employee. Instance/Companion Variables: name (instance), GROUP (companion). Inheritance/Overriding: Employee inherits Person, overrides describe. Polymorphism: List<Person> holds mixed types; describe varies. Encapsulation: salary is private, accessed via getSalary. Companion Methods: createFromString and isValidSalary. Special Methods: Team data class with toString and componentN.
10. Common Mistakes and Best Practices
Common Mistakes:
- Collections: Modifying immutable collections (e.g.,
listOf().add()), causing errors. Ignoring null safety, leading to NullPointerException. - Classes/Objects: Forgetting
val/varin constructor parameters, making them inaccessible. Not usingdata classwhentoString,equals, etc., are needed. - init Blocks: Placing complex logic in
init, reducing readability. Not validating inputs, causing runtime issues. - Instance/Companion Variables: Using instance variables for shared data, causing redundancy. Accessing companion variables via instances, confusing scope.
- Inheritance/Overriding: Forgetting
openon classes/functions, preventing inheritance/overriding. Not callingsuperwhen needed, missing parent initialization. - Polymorphism/Encapsulation: Ignoring polymorphism, duplicating code across classes. Exposing properties without
privateor getters, risking misuse. - Companion Methods: Overusing companion objects for instance-specific logic. Omitting
@JvmStaticfor JVM interop when needed. - Special Methods: Not overriding
toStringfor meaningful output. MisusingcomponentNin non-data classes, causing errors.
Best Practices:
- Collections: Use immutable collections (
listOf,setOf) unless mutation is needed. Leverage null safety with types likeList<Int?>. Use standard library functions (map,filter) for concise code. - Classes/Objects: Use
data classfor simple data holders with auto-generated methods. Define properties in the primary constructor for conciseness. - init Blocks: Keep
initblocks focused on initialization/validation. Use for setup that depends on constructor parameters. - Instance/Companion Variables: Use companion objects for shared data; instance properties for unique data. Mark constants with
const valin companion objects for efficiency. - Inheritance/Overriding: Mark classes/functions as
openonly when inheritance is intended. Usesuperto extend, not replace, parent functionality. - Polymorphism/Encapsulation: Design classes for polymorphic use with shared interfaces. Use
privateorprotectedfor sensitive properties; provide getters. - Companion Methods: Use companion objects for factory methods or utilities. Add
@JvmStaticfor Java interop when needed. - Special Methods: Override
toStringfor readable output in all classes. Usedata classfor automaticcomponentN,equals, etc. - General: Follow Kotlin conventions: camelCase for functions/properties, PascalCase for classes. Use explicit type annotations for clarity in complex code. Handle nullability with
?,!!, or safe calls (?.). Test with edge cases (e.g., empty collections, invalid inputs).