Kotlin Classes and Objects: Complete OOP Guide

Kotlin provides a powerful and concise object-oriented programming model. This guide covers classes, objects, inheritance, interfaces, and all the OOP features you need to build robust applications.
Class Declaration
Basic Class
class Greeter {
fun greet() {
println("Hello!")
}
}
// With constructor parameters
class Person(val name: String, var age: Int) {
fun introduce() {
println("I'm $name, $age years old")
}
}
// Empty body can be omitted
class Empty
Visibility and Annotations on Constructor
class Customer public @Inject constructor(name: String) {
// ...
}
Initializer Blocks
class Customer(name: String) {
val customerKey: String
init {
customerKey = name.uppercase()
println("Customer initialized: $customerKey")
}
}
Constructors
Primary Constructor
class Person(val name: String, var age: Int)
// Private property in constructor
class ParameterizedClass<A>(private val value: A) {
fun getValue(): A = value
}
Secondary Constructors
class Person(val name: String) {
var children: MutableList<Person> = mutableListOf()
constructor(name: String, parent: Person) : this(name) {
parent.children.add(this)
}
}
// Multiple secondary constructors
class MyView : View {
constructor(ctx: Context) : super(ctx)
constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)
}
Private Constructor
class Singleton private constructor() {
companion object {
val instance = Singleton()
}
}
Inheritance
Open Classes
Classes are final by default. Use open to allow inheritance:
open class Animal(val name: String) {
open fun makeSound() {
println("Some sound")
}
}
class Dog(name: String) : Animal(name) {
override fun makeSound() {
println("Woof!")
}
}
Abstract Classes
abstract class Shape(val sides: List<Double>) {
val perimeter: Double get() = sides.sum()
abstract fun calculateArea(): Double
}
class Rectangle(
val height: Double,
val width: Double
) : Shape(listOf(height, width, height, width)) {
override fun calculateArea() = height * width
}
Preventing Further Override
open class Base {
open fun v() {}
}
class Derived : Base() {
final override fun v() {} // Cannot be overridden
}
Properties
Property Override
Properties can be overridden. val can be overridden with var (adding setter):
open class Foo {
open val x: Int get() = 1
}
class Bar : Foo() {
override var x: Int = 0 // val -> var is allowed
}
Custom Getters and Setters
class Rectangle(val width: Int, val height: Int) {
val area: Int
get() = width * height
var counter: Int = 0
set(value) {
if (value >= 0) field = value
}
var setterVisibility: String = "visible"
private set // Private setter
}
Late Initialization
For non-null properties initialized after construction:
class MyTest {
lateinit var subject: TestSubject
@Before
fun setup() {
subject = TestSubject()
}
@Test
fun test() {
subject.method() // Direct access, no null check
}
fun checkInitialized(): Boolean = ::subject.isInitialized
}
Nested and Inner Classes
Nested Class (Static)
class Outer {
private val bar: Int = 1
class Nested {
fun foo() = 2 // Cannot access bar
}
}
val demo = Outer.Nested().foo() // 2
Inner Class
class Outer {
private val bar: Int = 1
inner class Inner {
fun foo() = bar // Can access outer class members
}
}
val demo = Outer().Inner().foo() // 1
Qualified this and super
class A {
inner class B {
fun Int.foo() {
val a = this@A // A's this
val b = this@B // B's this
val c = this // Int's this (receiver)
}
}
}
class Bar : Foo() {
inner class Baz {
fun g() {
super@Bar.f() // Calls Foo's f()
}
}
}
Interfaces
Basic Interface
interface Clickable {
fun click()
fun showOff() = println("I'm clickable!") // Default implementation
}
class Button : Clickable {
override fun click() = println("Button clicked")
}
Interface with Properties
interface Named {
val name: String
}
class Person(override val name: String) : Named
Multiple Interface Inheritance
interface A {
fun foo() { println("A") }
}
interface B {
fun foo() { println("B") }
}
class C : A, B {
override fun foo() {
super<A>.foo()
super<B>.foo()
}
}
Data Classes
Automatically generate equals(), hashCode(), toString(), copy(), and componentN():
data class User(val name: String, val age: Int)
val john = User("John", 30)
val olderJohn = john.copy(age = 31)
// Destructuring
val (name, age) = john
// In loops
for ((key, value) in map) {
println("$key -> $value")
}
Returning Multiple Values
data class Result(val value: Int, val status: Status)
fun compute(): Result {
return Result(42, Status.SUCCESS)
}
val (value, status) = compute()
Sealed Classes
Restricted class hierarchies for exhaustive when:
sealed class Expr
data class Const(val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()
object NotANumber : Expr()
fun eval(expr: Expr): Double = when(expr) {
is Const -> expr.number
is Sum -> eval(expr.e1) + eval(expr.e2)
NotANumber -> Double.NaN
// No else needed - all cases covered
}
Enum Classes
enum class Direction {
NORTH, SOUTH, WEST, EAST
}
// With properties
enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF)
}
// With methods
enum class ProtocolState {
WAITING {
override fun signal() = TALKING
},
TALKING {
override fun signal() = WAITING
};
abstract fun signal(): ProtocolState
}
// Generic enum access
inline fun <reified T : Enum<T>> printAllValues() {
print(enumValues<T>().joinToString { it.name })
}
printAllValues<Color>() // RED, GREEN, BLUE
Object Declarations and Expressions
Object Declaration (Singleton)
object DataManager {
val data = mutableListOf<String>()
fun addItem(item: String) {
data.add(item)
}
}
// Usage
DataManager.addItem("item")
Companion Objects
Static-like members in classes:
class MyClass {
companion object Factory {
fun create(): MyClass = MyClass()
}
}
val instance = MyClass.create()
// Companion can implement interfaces
interface Factory<T> {
fun create(): T
}
class MyClass {
companion object : Factory<MyClass> {
override fun create(): MyClass = MyClass()
}
}
Object Expressions (Anonymous Objects)
window.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) { /* ... */ }
override fun mouseEntered(e: MouseEvent) { /* ... */ }
})
// Simple object for local use
fun getPoint() = object {
var x: Int = 0
var y: Int = 0
}
Visibility Modifiers
| Modifier | Class Member | Top-level |
|---|---|---|
public (default) |
Visible everywhere | Visible everywhere |
private |
Visible in class | Visible in file |
protected |
Visible in class and subclasses | N/A |
internal |
Visible in same module | Visible in same module |
open class Base {
private val a = 1
protected open val b = 2
internal val c = 3
val d = 4 // public by default
protected open fun e() {}
}
Traits Pattern with Extension Functions
interface Logger
fun Logger.log(text: String) {
Log.d(this.javaClass.simpleName, text)
}
class MyService : Logger {
fun doSomething() {
log("Doing something") // Uses extension
}
}
Inline Classes (Value Classes)
Wrap a value without runtime overhead:
@JvmInline
value class Password(private val s: String)
fun login(password: Password) { /* ... */ }
// At runtime, Password is just a String
login(Password("secret"))
Best Practices
- Prefer composition over inheritance: Use delegation
- Make classes final by default: Only open when needed
- Use data classes for DTOs: Automatic equals/hashCode
- Leverage sealed classes: For restricted hierarchies
- Use companion objects wisely: For factory methods and constants
- Prefer properties over getters: More Kotlin idiomatic
Conclusion
Kotlin’s OOP features combine the power of traditional object-oriented programming with modern conveniences like data classes, sealed classes, and object declarations. Understanding these concepts allows you to write clean, safe, and expressive code that’s easy to maintain and extend.
Comments