Table of Contents
- Classes and Objects: The Building Blocks
- Constructors: Initializing Objects
- Properties: State of Objects
- Inheritance: Reusing and Extending Code
- Interfaces: Defining Contracts
- Abstract Classes: Partial Implementation
- Data Classes: For Data-Holding Objects
- Sealed Classes: Restricting Inheritance
- Enums: Enumerated Types
- Objects and Singletons: Single Instances
- OOP Best Practices in Kotlin
- Conclusion
- References
1. Classes and Objects: The Building Blocks
In OOP, a class is a blueprint for creating objects. It defines the properties (data) and methods (behavior) that objects of that class will have. An object is an instance of a class— a concrete entity created from the blueprint.
Kotlin Class Syntax
Kotlin classes are declared with the class keyword. Unlike Java, Kotlin classes are concise and require minimal boilerplate:
// A simple class representing a Person
class Person {
// Properties (state)
var name: String = "John Doe"
var age: Int = 30
// Method (behavior)
fun greet() {
println("Hello, my name is $name and I'm $age years old.")
}
}
Creating Objects
To create an object (instance) of a class, use the class name followed by parentheses (no new keyword, unlike Java):
fun main() {
val person = Person() // Create an instance of Person
person.greet() // Output: Hello, my name is John Doe and I'm 30 years old.
}
2. Constructors: Initializing Objects
Constructors initialize objects by setting initial values for properties. Kotlin supports two types of constructors: primary and secondary.
Primary Constructor
The primary constructor is part of the class declaration and is defined after the class name. It can include parameters to initialize properties directly:
// Primary constructor with parameters
class Person(val name: String, var age: Int) {
// Initializer block: Executes when an object is created
init {
println("Person initialized with name: $name, age: $age")
}
fun greet() {
println("Hello, I'm $name!")
}
}
// Usage
fun main() {
val alice = Person("Alice", 25) // Output: Person initialized with name: Alice, age: 25
alice.greet() // Output: Hello, I'm Alice!
}
- Parameters: Marked
val(immutable) orvar(mutable) become class properties. - Initializer Blocks: Use
initto run code during initialization (e.g., validation).
Secondary Constructors
Secondary constructors handle additional initialization logic and are declared with the constructor keyword. They must delegate to the primary constructor using this:
class Person(val name: String, var age: Int) {
// Secondary constructor: Takes only a name (defaults age to 18)
constructor(name: String) : this(name, 18) {
println("Secondary constructor called for $name (default age)")
}
init {
println("Primary constructor initialized $name")
}
}
// Usage
fun main() {
val bob = Person("Bob") // Output: Primary constructor initialized Bob; Secondary constructor called for Bob (default age)
println(bob.age) // Output: 18
}
3. Properties: State of Objects
Properties store the state of an object. Kotlin properties are more powerful than Java fields—they automatically generate getters and setters, and support custom logic.
Val vs Var
val: Immutable (read-only). Kotlin generates a getter but no setter.var: Mutable (read-write). Kotlin generates both a getter and a setter.
class Person(val name: String, var age: Int) // name is val (immutable), age is var (mutable)
fun main() {
val carol = Person("Carol", 30)
println(carol.name) // Getter called (auto-generated)
carol.age = 31 // Setter called (auto-generated)
}
Custom Getters and Setters
Override default getters/setters to add logic (e.g., validation, computed values):
class Rectangle(private val width: Int, private val height: Int) {
// Custom getter: Computed property (no backing field)
val area: Int
get() = width * height // Calculated on each access
// Custom setter with validation
var sideLength: Int = 0
set(value) {
if (value > 0) {
field = value // "field" refers to the backing field
} else {
throw IllegalArgumentException("Side length must be positive")
}
}
}
fun main() {
val rect = Rectangle(5, 10)
println(rect.area) // Output: 50 (getter called)
rect.sideLength = 10 // Setter called (valid)
rect.sideLength = -5 // Throws IllegalArgumentException
}
- Backing Field: Use the
fieldkeyword to refer to the property’s underlying storage (avoids infinite recursion in custom setters).
4. Inheritance: Reusing and Extending Code
Inheritance allows a class (subclass) to reuse code from another class (superclass). Kotlin enforces safe inheritance with the open and override keywords.
Key Rules:
- By default, classes are
final(cannot be inherited). Useopento allow inheritance. - Override methods/properties with
override.
Example: Inheritance
// Superclass (open to allow inheritance)
open class Animal(val name: String) {
open fun makeSound() {
println("$name makes a sound")
}
}
// Subclass (inherits from Animal)
class Dog(name: String) : Animal(name) {
override fun makeSound() { // Override superclass method
super.makeSound() // Call superclass implementation
println("$name barks")
}
}
fun main() {
val dog = Dog("Buddy")
dog.makeSound()
// Output:
// Buddy makes a sound
// Buddy barks
}
5. Interfaces: Defining Contracts
Interfaces define a contract of methods and properties that a class must implement. Unlike classes, interfaces cannot hold state (except for constants) and can have default method implementations.
Interface Syntax
// Interface with abstract and default methods
interface Drivable {
val maxSpeed: Int // Abstract property (must be implemented by classes)
fun drive() // Abstract method (must be implemented)
fun honk() { // Default method (optional to override)
println("Honking!")
}
}
// Class implementing the interface
class Car(override val maxSpeed: Int) : Drivable {
override fun drive() {
println("Driving at $maxSpeed km/h")
}
}
fun main() {
val myCar = Car(120)
myCar.drive() // Output: Driving at 120 km/h
myCar.honk() // Output: Honking! (default implementation)
}
Resolving Interface Conflicts
If a class implements multiple interfaces with conflicting methods, use super<Interface>.method() to specify which implementation to use:
interface A { fun foo() = "A" }
interface B { fun foo() = "B" }
class C : A, B {
override fun foo(): String {
return super<A>.foo() + super<B>.foo() // Resolve conflict
}
}
fun main() {
println(C().foo()) // Output: AB
}
6. Abstract Classes: Partial Implementation
Abstract classes are blueprints for other classes. They can contain abstract methods (no implementation) and concrete methods (with implementation), but cannot be instantiated directly.
Key Differences from Interfaces:
- State: Abstract classes can have mutable state (properties with backing fields); interfaces cannot.
- Inheritance: A class can inherit from only one abstract class (single inheritance), but implement multiple interfaces.
// Abstract class
abstract class Shape {
abstract val area: Double // Abstract property (no implementation)
abstract fun draw() // Abstract method (no implementation)
fun printArea() { // Concrete method
println("Area: $area")
}
}
// Subclass implementing Shape
class Circle(val radius: Double) : Shape() {
override val area: Double
get() = Math.PI * radius * radius
override fun draw() {
println("Drawing a circle with radius $radius")
}
}
fun main() {
val circle = Circle(5.0)
circle.draw() // Output: Drawing a circle with radius 5.0
circle.printArea() // Output: Area: 78.53981633974483
}
7. Data Classes: For Data-Holding Objects
Data classes are designed to hold data (e.g., DTOs, model objects). Kotlin automatically generates utility functions like equals(), hashCode(), toString(), copy(), and componentN() (for destructuring).
Requirements for Data Classes:
- Primary constructor must have at least one parameter.
- All primary constructor parameters must be
valorvar. - Cannot be
abstract,open,sealed, orinner.
// Data class
data class User(val id: Int, val name: String, var email: String)
fun main() {
val user1 = User(1, "Alice", "[email protected]")
val user2 = User(1, "Alice", "[email protected]")
// Auto-generated toString()
println(user1) // Output: User(id=1, name=Alice, [email protected])
// Auto-generated equals()
println(user1 == user2) // Output: true (compares property values)
// Auto-generated copy() (creates a new instance with modified values)
val updatedUser = user1.copy(email = "[email protected]")
println(updatedUser.email) // Output: [email protected]
// Destructuring (componentN() functions)
val (id, name, email) = user1
println("ID: $id, Name: $name") // Output: ID: 1, Name: Alice
}
8. Sealed Classes: Restricting Inheritance
Sealed classes restrict inheritance—all subclasses must be declared in the same file as the sealed class. This ensures a fixed set of subclasses, making them ideal for state management (e.g., UI states like Loading, Success, Error).
Syntax and Usage:
// Sealed class (restricts inheritance)
sealed class Result<out T> {
data class Success<out T>(val data: T) : Result<T>()
data class Error(val message: String) : Result<Nothing>()
object Loading : Result<Nothing>()
}
// Function returning a Result
fun fetchData(): Result<String> {
return Result.Success("Data loaded!") // or Result.Error("Failed"), Result.Loading
}
fun main() {
val result = fetchData()
// Exhaustive when expression (no "else" needed—Kotlin knows all subclasses)
when (result) {
is Result.Success -> println("Success: ${result.data}")
is Result.Error -> println("Error: ${result.message}")
Result.Loading -> println("Loading...")
}
}
Why Sealed Classes? They enable exhaustive when expressions, ensuring all possible states are handled (no runtime errors from missing cases).
9. Enums: Enumerated Types
Enums (enumerated types) represent a fixed set of constants. Each enum constant is an instance of the enum class and can have properties and methods.
Example:
// Enum class with properties and methods
enum class Direction(
val dx: Int, // Change in x-coordinate
val dy: Int // Change in y-coordinate
) {
NORTH(0, 1),
SOUTH(0, -1),
EAST(1, 0),
WEST(-1, 0); // Semicolon required if enum has methods
// Method to get opposite direction
fun opposite(): Direction {
return when (this) {
NORTH -> SOUTH
SOUTH -> NORTH
EAST -> WEST
WEST -> EAST
}
}
}
fun main() {
val dir = Direction.NORTH
println("North dx: ${dir.dx}, dy: ${dir.dy}") // Output: North dx: 0, dy: 1
println("Opposite of North: ${dir.opposite()}") // Output: Opposite of North: SOUTH
}
10. Objects and Singletons: Single Instances
Kotlin simplifies singleton creation with object declarations—they create a single instance of a class automatically.
Object Declarations (Singletons)
// Singleton object (only one instance exists)
object Logger {
fun log(message: String) {
println("[LOG] $message")
}
}
fun main() {
Logger.log("App started") // Output: [LOG] App started
Logger.log("Data saved") // Output: [LOG] Data saved
}
Companion Objects
Companion objects are singletons tied to a class, used for “static” members (like factory methods or constants):
class User(val id: Int, val name: String) {
// Companion object: Members are scoped to the class
companion object Factory {
fun createGuest(): User { // Factory method
return User(0, "Guest")
}
const val MAX_AGE = 120 // Constant
}
}
fun main() {
val guest = User.createGuest() // Call companion method
println(guest.name) // Output: Guest
println(User.MAX_AGE) // Output: 120
}
11. OOP Best Practices in Kotlin
-
Prefer Immutability: Use
valfor properties unless mutability is required. This avoids side effects and makes code thread-safe. -
Use Data Classes for Data: For classes holding data (e.g., models), use
data classto auto-generate utility functions. -
Sealed Classes for State: Use sealed classes to model fixed states (e.g., UI states) and enable exhaustive
whenexpressions. -
Interfaces Over Abstract Classes: Prefer interfaces for defining contracts—they support multiple inheritance and avoid tight coupling.
-
Limit Mutable State: Keep mutable properties private and expose them via controlled setters (e.g., with validation).
12. Conclusion
Kotlin’s OOP capabilities combine conciseness, safety, and expressiveness, making it a joy to model real-world problems. From data classes that eliminate boilerplate to sealed classes that enforce state safety, Kotlin empowers developers to write clean, maintainable code. By leveraging features like inheritance, interfaces, and singletons, you can build scalable applications with robust object-oriented designs.
13. References
- Kotlin Official Documentation: Classes and Objects
- Kotlin Official Documentation: Data Classes
- Kotlin Official Documentation: Sealed Classes
- Kotlin Official Documentation: Enums
- Kotlin in Action (Book by Dmitry Jemerov & Svetlana Isakova)