Before I begin developing a chat server, I decided to study Kotlin. Since the Hero Tech Course also uses Kotlin, I thought it would be a good time to study and organize my understanding together.
To get a sense of Kotlin’s design philosophy, I looked through some books. Kotlin is a multi-paradigm language. It supports object-oriented and functional programming, and apparently two more paradigms as well. Since I come from a Java background, I’m more familiar with OOP and still getting used to Kotlin, so I’ll focus on Kotlin from an object-oriented programming (OOP) perspective.
Object State and Behavior
In OOP, the core concept is an object, and that object’s state and behavior.
Java
Let’s take a look at how state and behavior are expressed in Java.
public class Person {
private final String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void introduce() {
System.out.println("Hi, my name is " + name);
}
}
In the Person
class above, the name
field represents the object’s state. It’s declared private final
, so it can’t be directly accessed or changed from outside the class — only read. This means the value is initialized during construction and cannot be changed afterward.
The constructor Person(String name)
is used to initialize the name when the object is created. In other words, this.name = name
assigns the externally provided value to the object’s internal state.
Kotlin
Now let’s see how Kotlin expresses the same idea. Here’s the equivalent of the Java code above:
class Person(private val name: String) {
fun introduce() {
println("Hi, my name is $name")
}
}
In Kotlin, you don’t need to explicitly define a getName
method — when you declare a property, Kotlin automatically generates the getter/setter under the hood.
Declaring and Calling Functions: Java vs Kotlin
Here’s how you’d use the Person
class in Java:
public class Demo {
public static void main(String[] args) {
Person person = new Person("Bob");
System.out.println(person.getName());
}
}
You create an object with the new
keyword and call its method.
In Kotlin:
fun main() {
val person = Person("Bob")
println(person.name)
}
Even though we didn’t define a getName()
function, Kotlin allows direct property access. This feels more concise and expressive than Java.
But why does Kotlin do it this way? The benefit becomes clearer when we look at custom getters.
Imagine the Person
class has a birth year and we want to calculate age dynamically rather than storing and updating it manually.
class Person(
private val name: String,
private val birthYear: Int) {
fun introduce() {
println("Hi, my name is $name.")
}
fun getAge(): Int {
return LocalDate.now().year - birthYear
}
}
In Java, it would be common to store age or calculate it in the constructor. But that would “freeze” the state of the object at the time it’s created.
Using the Kotlin code above:
fun main() {
val person = Person("Bob", 1999)
println(person.name)
println(person.getAge())
}
At first glance, this seems fine — but is age really a method? Isn’t it part of the object’s state? Storing it as a variable might be misleading because age changes over time.
Instead, Kotlin lets you define custom getters to calculate such values dynamically while keeping the API intuitive.
class Person(
private val name: String,
private val birthYear: Int) {
val age: Int
get() = LocalDate.now().year - birthYear
fun introduce() {
println("Hi, my name is $name and I'm $age years old.")
}
}
Now in the main function:
fun main() {
val person = Person("Bob", 1999)
println(person.name)
println(person.age)
}
This way, the age still behaves like a property even though it’s dynamically calculated, which improves clarity and correctness.
Validating Constructor Arguments in Kotlin
In Java, constructors are often the place to validate inputs and prevent invalid objects from being created. But in Kotlin, at first glance it’s unclear where to place such validation.
Kotlin provides the init
block, along with validation functions like require
, check
, and assert
.
class Person(
val name: String,
val birthYear: Int
) {
init {
require(name.isNotBlank()) { "Name must not be blank" }
require(birthYear in 1900..LocalDate.now().year) { "Invalid birth year" }
}
val age: Int
get() = LocalDate.now().year - birthYear
}
require()
throwsIllegalArgumentException
check()
throwsIllegalStateException
assert()
throwsAssertionError
(enabled only with JVM-ea
flag)
Function | Purpose | Exception Thrown |
---|---|---|
require() | Validating input | IllegalArgumentException |
check() | Validating state | IllegalStateException |
assert() | Debug assertions | AssertionError |
Reusing and Extending Objects
In OOP, we often need to reuse and extend objects. This is essentially what inheritance is about. In both Java and Kotlin, only single inheritance is allowed — each class can only extend one superclass. In Java, Object
is the implicit superclass; in Kotlin, it’s Any
.
Inheritance in Java
public class Animal {
public void sound() {
System.out.println("Some generic animal sound");
}
}
public class Dog extends Animal {
@Override
public void sound() {
System.out.println("Bark!");
}
}
The Dog
class extends Animal
and overrides the sound()
method, demonstrating code reuse and behavioral extension.
Inheritance in Kotlin
In Kotlin, classes are final
by default, so you need to mark them as open
to allow inheritance.
open class Animal {
open fun sound() {
println("Some generic animal sound")
}
}
class Dog : Animal() {
override fun sound() {
println("Bark!")
}
}
Kotlin uses open
to declare overridable members and override
instead of Java’s @Override
annotation.
Abstraction of State and Behavior
Abstraction allows us to define what an object can do while hiding how it does it. This is often done using interfaces or abstract classes.
Abstraction in Java
public interface Animal {
void sound();
}
public class Dog implements Animal {
@Override
public void sound() {
System.out.println("Bark!");
}
}
public class Cat implements Animal {
@Override
public void sound() {
System.out.println("Meow!");
}
}
This structure lets us use the Animal
interface as a general type without knowing the exact implementation.
Abstraction in Kotlin
interface Animal {
fun sound()
}
class Dog : Animal {
override fun sound() {
println("Bark!")
}
}
class Cat : Animal {
override fun sound() {
println("Meow!")
}
}
Kotlin also supports default method implementations inside interfaces, similar to Java 8+.
interface Animal {
fun sound() {
println("Some generic sound")
}
}
So far, we’ve looked at Kotlin through an OOP lens. Since Kotlin preserves many concepts from other familiar languages, it’s relatively easy to learn — especially when you already understand the principles of object-oriented programming.