A Kotlin Class Is Mostly Its Header
Most of a Kotlin program lives in its classes, and most of a Kotlin class lives in one line: the header. That header doubles as the constructor and declares your properties, which is why a class that would be thirty lines of Java often fits in one. The rest is a handful of conventions around setup, access, and inheritance.
This is part one of two. Here we cover the everyday class — how you declare one, get values into it, run setup logic, control access, and extend it. Part two covers the specialized kinds: data, enum, sealed, object, and friends.
The smallest possible class
This is a complete, valid class:
class Person
No body, no braces, nothing. You create an instance by calling it like a function — note that Kotlin has no new keyword:
val p = Person()
Add a body when you have something to put in it:
class Person {
fun greet() {
println("Hello")
}
}
Properties: the heart of a class
A property is Kotlin’s version of a field plus its getter and setter, declared in one line:
class Person {
var name: String = "Unknown"
val species: String = "Homo sapiens"
}
var is read-write, val is read-only. You access both with a dot:
val p = Person()
p.name = "Ada" // fine — name is a var
println(p.species) // "Homo sapiens"
// p.species = "..." // compile error — species is a val
Under the hood Kotlin generates a getter (and, for var, a setter). You rarely write them by hand, but they’re there — which matters later when you want to customize them.
The primary constructor
Most classes need values handed in at creation time. The primary constructor goes right in the class header:
class Person(name: String, age: Int)
But there’s a crucial detail. The line above declares constructor parameters, not properties — name and age are not accessible after construction. Add val or var to promote them to properties:
class Person(val name: String, var age: Int)
Now name and age are real properties:
val p = Person("Ada", 36)
println(p.name) // "Ada"
p.age = 37 // var, so this works
This one-line “declare and assign properties from the constructor” is something Java only recently approached with records. In Kotlin it’s the default way to write a class.
init blocks: code that runs at construction
The primary constructor can’t contain statements — so where does setup logic go? An init block:
class Person(val name: String) {
init {
require(name.isNotBlank()) { "name must not be blank" }
}
}
init runs when the instance is created. You can have several; they execute top to bottom, interleaved with property initializers:
class Person(val name: String) {
val initials = name.take(1) // runs first
init {
println("Creating $name") // runs second
}
}
A parameter without val/var is still usable here — it just isn’t kept as a property:
class Circle(radius: Double) {
val area = Math.PI * radius * radius // radius used, then discarded
}
radius exists only long enough to compute area. After construction, there’s no radius property on the object.
Default and named arguments
Before reaching for more constructors, remember that Kotlin parameters can have defaults:
class Server(
val host: String = "localhost",
val port: Int = 8080,
val useTls: Boolean = false,
)
Combined with named arguments, this replaces the pile of overloaded constructors you’d write in Java:
Server() // all defaults
Server(port = 9090) // override just one
Server(host = "example.com", useTls = true)
One declaration, every sensible combination, and the call sites read clearly.
Secondary constructors
Occasionally you need an alternative way to build an object that does more than defaults allow. That’s a secondary constructor, and it must delegate to the primary one with this(...):
class Person(val name: String) {
var age: Int = 0
constructor(name: String, age: Int) : this(name) {
this.age = age
}
}
In practice, default arguments make secondary constructors rare. Reach for them mainly when interoperating with frameworks that expect specific constructor shapes.
Encapsulation: visibility and custom accessors
Properties are public by default, but you can tighten that — including making a property publicly readable but only privately writable:
class BankAccount(initial: Int) {
var balance: Int = initial
private set
fun deposit(amount: Int) {
balance += amount // allowed inside the class
}
}
Outside code can read balance but not assign to it. You can also define a property entirely in terms of a getter — a computed property that holds no backing field:
class BankAccount(var balance: Int) {
val isOverdrawn: Boolean
get() = balance < 0
}
isOverdrawn is recomputed every time it’s read. No storage, always consistent with balance.
Inheritance: classes are final by default
Here’s a deliberate difference from Java. In Kotlin, a class cannot be subclassed unless you say so. You opt in with open:
open class Animal(val name: String) {
open fun speak(): String = "..."
}
class Dog(name: String) : Animal(name) {
override fun speak(): String = "Woof"
}
Three things to notice:
- The base class is
open; without it,Dog : Animalwon’t compile. - The method you intend to override is also
open, and the override is marked withoverride(not optional — the compiler enforces it). - The subclass calls the base constructor right in its header:
: Animal(name).
This “final by default” stance encodes a long-standing piece of OO advice — design for inheritance explicitly, or prohibit it — into the language itself.
Abstract classes
When a base class shouldn’t be instantiated on its own and leaves some behavior unspecified, make it abstract:
abstract class Shape {
abstract fun area(): Double // no body — subclasses must provide one
fun describe(): String = "Area is ${area()}" // concrete, shared
}
class Square(val side: Double) : Shape() {
override fun area(): Double = side * side
}
Abstract members have no implementation and are implicitly open, so you don’t repeat open. An abstract class can freely mix abstract members with concrete ones — useful when subclasses share most behavior but differ in a few key spots.
Final thoughts
Everything above is the plain class: a header that doubles as a constructor, properties that bundle storage with access, init for setup, and explicit, opt-in inheritance. It’s a small set of rules, but they compose into most of the classes you’ll ever write.
What makes Kotlin’s type system feel rich is the next layer — the specialized class kinds that bake whole behaviors in for you: structural equality with data, fixed sets with enum, closed hierarchies with sealed, singletons with object, and more. That’s part two.