Classes in Kotlin: Constructors and Properties, Inheritance, Abstract, Open and data class

Video thumbnail

Classes in Kotlin, as we will see in the next two posts, are simplified to write the greatest amount of functionality with the least amount of code. We already saw the use of functions in Kotlin as a key piece for reusing code; now let's move on to the next block: classes.

The syntax is simple or, in the worst-case scenario, similar to other languages like Java (remembering it follows the same object-oriented paradigm). It features the same concepts of constructors, inheritance, interfaces, and abstract classes as Java (although with a substantial difference in how properties are handled).

Classes are the way we define real-world objects in our code. For example, if our application sells cars, we would create a Car class with specific attributes (make, model, etc.) to classify and organize them.

Empty Classes

To define a class in Kotlin in its minimal expression, we have:

class Vacia

As we can see, in Kotlin, to create a public class (default when no other class type is defined) there is no need to define braces or anything of the sort, since it has no properties or other methods; we could also define the previous class with braces:

class Vacia {}

Although these are optional depending on whether we define properties and methods within the class; but at a minimum, we must define a class that we can call empty, meaning it does not have any type of properties or methods in it, and the braces.

Instantiating Classes:

To instantiate the previous class, it would be as follows:

val vacia = Vacia ()

The () serve to indicate the constructor method, which for this example is the default for the Vacia class; Also note that we do not use the reserved word from other programming languages like Java new, which is used in Java to indicate the creation of a new object.

Although a class with nothing inside or nothing defined is not of much use to us; for that, we have the properties that we will see next.

Properties of Classes in Kotlin

Classes serve us to reference real-world objects, and through properties, we can define the different characteristics we are interested in manipulating that make up that object; for example, to define a Persona class in Kotlin with some properties (yes, properties and not attributes) we can do the following:

class Persona {
  var nombre: String = ""
  var apellido: String = ""
  var edad: Int = 0
}

Properties instead of variables

Properties in Kotlin are something innovative that will make things much easier for us when building our classes; since they replace the getters and setters that we have to use in other languages like Java.

With this, we achieve an enormous simplification in the amount of code we have to create in our object-oriented classes or those that define an entity or something similar; for example, a person, we do not have to define gets and sets for nombre, apellido, etc., but simply declare the property as if it were a variable.

Now we are going to create an instance of a class again, in this case of the class named Persona, and we are going to access the properties we defined earlier:

val persona = Persona()
persona.nombre = "Andrés"
persona.apellido = "Cruz"

Accessing each of them is just as easy:

println(persona.nombre) println(persona.apellido)

Properties, but not variables, in Kotlin classes

A very important point is what we were saying before: there is a change of concept between Kotlin and Java. While in Java the properties nombre and apellido for this example would be fields, in Kotlin they are properties. This means that while in Java it is considered bad programming practice to access fields directly as we did before, it is not so in Kotlin.

In Kotlin, the GET and SET methods are inferred by the compiler, which does not happen in Java, and this is due to the difference in concepts we pointed out earlier; you can get more information in Kotlin Getters and Setters and it is important that you know this paradigm shift and know how to take advantage of it.

Overriding the get and set methods

Of course, we can define our own get and set methods that override those that are default or inferred by the Kotlin compiler:

class Persona {
var nombre: String = ""
    get() = field
    set(value) {
      field = value
    }
var apellido: String = ""
var edad: Int = 0
}

In Kotlin, it is not necessary to explicitly define the Get and Set methods, as the language generates them automatically. However, you can customize them if you need additional logic (such as converting text to uppercase when assigning it) using the special field called field.

Class Constructors

Primary Constructor

Like every object-oriented programming language, Kotlin has constructor methods in classes that serve to initialize class values, just like in Java; but unlike the latter, Kotlin incorporates a very interesting way in which we save a few lines of code; it changes the methodology and does not define the primary constructor as a method inside the function but embedded within the class:

class Persona(nombre: String, apellido: String, edad: Int) {
  var nombre: String = nombre
  var apellido: String = apellido
  var edad: Int = edad
}

Kotlin defines the primary constructor as part of the class in its header, where the parameters are optional.

Constructors in the Signature

Unlike other languages, Kotlin allows defining the primary constructor directly in the class signature. This allows receiving parameters and assigning properties compactly:

class Persona(val nombre: String, var edad: Int)

With this, Kotlin automatically makes the equivalence between the parameters in the constructor and the properties defined in the class.

Properties and Null Safety

Classes contain properties (nombre, apellido, edad). When defining them, it is vital to remember Kotlin's null safety. We must prevent a property from being null; therefore, it is recommended to assign default initial values.

  • Val (Constant): Used for values that do not change (like a name).
  • Var (Variable): Used for values that can mutate (like age).

For optimization and best practices, try to make everything a val unless it is strictly necessary to change it.

To create an instance initializing from the constructor we have:

var persona = Persona("Andrés", "Cruz", 27)    

init Constructor

There is also the init block, which acts as the body of the primary constructor to perform initializations or validations. If you need variants, you can create secondary constructors using the keyword constructor.

class Persona(nombre: String, apellido: String, edad: Int) {
  var nombre: String = ""
  var apellido: String = ""
  var edad: Int = 0
  init {
	this.nombre = nombre
	this.apellido = apellido
	this.edad = edad
  }
}

This last code would be the "Java way" in which we specify which constructor parameters initialize the class properties; internally, this is what Kotlin does with the init{} structure.

Or we could simplify it even further:

class Persona(nombre: String, apellido: String, edad: Int) {
    var nombre: String = nombre
    var apellido: String = apellido
    var edad: Int = edad
}

And with this, we eliminate the init structure from our code.

We can also place the word constructor after indicating the class name:

class Persona constructor (nombre: String, apellido: String, edad: Int) { 
   var nombre: String = nombre 
   var apellido: String = apellido 
   var edad: Int = edad 
}

All these examples of primary constructors are equivalent, but if we are not going to perform any validation, we can leave it using the default constructor:

class Persona(nombre: String, apellido: String, edad: Int) { 
   var nombre: String = "" 
   var apellido: String = "" 
   var edad: Int = 0 
}

Regardless of the scheme you prefer, to create an instance of the Persona class using the constructor, we can do the following:

var persona = Persona("Andrés"," Cruz",27)

Multiple constructors (secondary constructors)

Having as a primary constructor the one that defines (nombre: String, apellido: String, edad: Int), we also define a secondary constructor, which are defined outside the class header but inside the class body, such as for this example constructor (nombre: String, apellido: String):

class Persona constructor (nombre: String, apellido: String, edad: Int) {
  var nombre: String = ""
  var apellido: String = ""
  var edad: Int = 0
  constructor (nombre: String, apellido: String) : this(nombre, apellido, 0)
}

In this way, you can define as many secondary constructors as you wish.

Or what is the same:

class Persona (nombre: String, apellido: String, edad: Int) {
  var nombre: String = nombre
  var apellido: String = apellido
  var edad: Int = edad
  
  constructor (nombre: String, apellido: String) : this(nombre, apellido, 0)
}

As we can see, there are multiple combinations according to our preferred scheme; in the next post, we will see the types of classes in Kotlin, inheritance, interfaces, and abstract classes in Kotlin.

Open Classes (Open class) and Inheritance in Kotlin

The first thing we will cover here is how to create a class in Kotlin and how those classes are declared; classes in Kotlin are by default declared as final, which means we cannot inherit from a class we create as in this example:

class Padre 
class Hija : Padre()

Because the Kotlin compiler would give us an error; classes in Kotlin inherit by default from a superclass called Any; therefore, the class called Padre inherits by default from the Any class:

class Padre // implicitly inherits from the Any class

If we want to inherit from a class we define, we must declare it as an "open class"; this annotation has the opposite behavior to finals in Java classes and therefore allows us to perform inheritance, using the annotation as follows:

open class Padre 
class Hija : Padre()

This is an important difference from Java, where we can inherit from a class without needing to incorporate a reserved word specifying that we want to allow inheritance from it; this is a very important principle in Kotlin to help us keep our classes organized.

By adding open to our classes, we are indicating that we want to design that class so it can be inherited, which in the end facilitates reading the code of an application built with Kotlin, whether it is an Android application or for another platform; it is also important to note that we cannot inherit from multiple classes or have multiple inheritance.

Open Classes (open)

By default, all classes in Kotlin are final (cannot be inherited). To allow inheritance, we must mark the parent class with the word open.

Inheritance and Polymorphism

For a child class to inherit from a parent, we use the colon :

For example, a Forma class can have a method calcularArea().

Classes like Circulo or Rectangulo will inherit from Forma and use the word override to provide their own implementation of the area.

open class Forma {
    open fun calcularArea(): Double = 0.0
}
class Circulo(val radio: Double) : Forma() {
    override fun calcularArea(): Double {
        return Math.PI * radio * radio
    }
}

Example of Classes and Inheritance

This would be the essential part here for inheriting from classes; otherwise, we can do the same as we do in other programming languages; for example, the following scheme is valid for declaring a series of classes to specify geometric shapes, which is what we will see next:

open class Forma(val nombre: String) {
   open fun area() = 0.0
}
class Circulo(nombre: String, val radius: Double): Forma(nombre)

In the previous example, we defined a class called Forma that would have the primitives to specify geometric shapes, which in our case is just a name for the geometric shape, and a function that we override to specify the area of the geometric shape.

Then we create a Circulo class and its constructor, where in the case of the circle we must add an extra parameter which is its radius:

class Circulo(name: String, val radius: Double)

Then, as we want our Circulo class to inherit the properties of Forma, we specify the inheritance by indicating the name of the class to inherit after the colon, where we reference the constructor of the Forma class (this would be like using the reserved word super in Java):

class Circulo(name: String, val radius: Double): Forma(nombre)

Now we can override the area method for our circle:

To also override any property or method of the inherited class, we must specify the reserved word open on that property or method.

open class Forma(val nombre: String) {
  open fun area() = 0.0
}
class Circulo(nombre: String, val radio: Double): Forma(nombre) {
  override fun area() = Math.PI * Math.pow(radio, 2.0)
}
fun main(args: Array<String>) {
  val circulo = Circulo("Circulo", 4.0)
  println(circulo.nombre)
  println(circulo.radio)
  println(circulo.area())
}

Abstract Classes in Kotlin

Abstract classes practically follow the same principle as in other object-oriented programming languages like Java; abstract classes are those that have no implementation, and the reserved word abstract is used in classes for this purpose.

As you may recall, in this case, it is not necessary to specify a return value for our area method as we did before, since it will be overridden in our child class:

abstract class Forma(val nombre: String) {
   abstract fun area(): Double
   fun printName(){
       println("el nombre es: ${nombre}")
   }
}
class Circulo(nombre: String, val radio: Double): Forma(nombre) {
   override fun area() = Math.PI * Math.pow(radio, 2.0)
}
fun main(args: Array<String>) {
   val circulo = Circulo("Circulo", 4.0)
   println(circulo.nombre)
   println(circulo.radio)
   println(circulo.area())
   println(circulo.printName())
}

We can use the reserved word in methods (fun) to specify that the class defining them must override those methods, or we can implement some within the abstract class so they can be used in the class that defines that abstract class.

As in any other language, we must override any method or property that contains the reserved word abstract in the child class.

Interfaces in Kotlin

As the final object-oriented programming concept we will cover in this entry, interfaces are declared with the reserved word interface; you should know that all methods are public and can be overridden; we can also define methods with a body or content, which is a great advantage since we don't have to repeat code in other classes and can define it as a base in the interface.

A primary characteristic of interfaces is that they do not implement methods or constructors, which is a distinction from the inheritance or abstract classes we saw earlier.

Interfaces do not encapsulate data; they only define which methods must be implemented in the classes that define them.

Therefore, our example would look as follows:

interface Forma {
   fun area(): Double
   fun printName();
}
class Circulo(val nombre: String, val radio: Double): Forma {
   override fun area() = Math.PI * Math.pow(radio, 2.0)
   override fun printName(){}
}
fun main(args: Array<String>) {
   val circulo = Circulo("Circulo", 4.0)
   println(circulo.nombre)
   println(circulo.radio)
   println(circulo.area())
}

We can also define a default implementation in our interface:

interface Forma {
   fun area(): Double = 0.0
   fun printName();
}

Copying Instances

When assigning one object to another, both point to the same memory. To have an independent instance, a copy must be performed:

class Persona(var nombre: String)
fun main() {
    val persona1 = Persona("Juan")
    val persona2 = persona1 // Copia de referencia
    persona2.nombre = "Carlos"
    println(persona1.nombre) // Resultado: "Carlos" (¡El original cambió!)
}

The most efficient and common way to copy in Kotlin is using a data class. These classes automatically include the copy() method, which creates a new instance with the same values.

class Usuario(val id: Int, val email: String)
fun main() {
   val user1 = Usuario(1, "ana@correo.com")
   val user2 = user1.copy() // Es un objeto totalmente nuevo en memoria
   println(user1 == user2) // true (tienen el mismo contenido)
   println(user1 === user2) // false (son diferentes objetos físicamente)
}

The most powerful thing about copy() is that you can change only some properties while keeping the rest the same. This is ideal for following the principle of immutability.

val original = Usuario(id = 1, email = "pedro@correo.com")
val actualizado = original.copy(email = "pedro_nuevo@correo.com")
println(original.email)    // "pedro@correo.com"
println(actualizado.email) // "pedro_nuevo@correo.com"

Why is copying important?

In modern application development (especially with Jetpack Compose or state architectures), immutability is key.

Instead of modifying an existing object, we "copy" the previous state, apply the change, and generate a new state.

This avoids hard-to-track bugs where data changes "under the hood" without warning.

Data classes in Kotlin to store and process data easily

Video thumbnail

Let's explore another very interesting structure: the Data Class. As its name suggests, it is a class designed specifically for working with data.

We are going to discover another very interesting structure that was also in the classes section: data classes.
Its name already says quite a bit, but it is still somewhat abstract, so let's get to know them in detail.

The "problem" with traditional classes

First, what is the problem (in quotes) that we have with normal classes?

Usually, classes are often used just as a data store, like a repository. For example, when we want to create a user model, a list of posts, or any similar structure, we simply store data and don't use additional logic.

The problem is that a traditional class ends up being quite large and complex, with all the features we saw previously (constructors, open, final, inheritance, etc.), things we often don't need when we just want to handle data.

That's why the concept of the data class appears, which is a class designed specifically for working with data, simple as that.
In fact, Kotlin is one of the few languages that incorporates this concept natively.

What advantages do Data Classes have?

When using data classes:

  • You forget about things like open, final, and other unnecessary configurations.
  • They are ideal when we want to handle lists of data (users, products, posts, etc.).
  • They are more efficient than a normal class.
  • Comparisons are much simpler.

If you want a class to be inheritable, you must mark it as open.

With data classes, we don't have to worry about any of that.

In summary:

  1. Efficiency: They are lighter than a normal class by having less overhead.
  2. Simple comparisons: They greatly facilitate the comparison of objects, something that in traditional classes is a headache.

The Memory Reference Problem

To understand why they are useful, we must remember how normal classes work. When you create an object, you are not assigning the value directly, but a memory position (a reference).

class Contact(val id: Int, var email: String)
fun main() {
    val contact = Contact(1, "mary@gmail.com")
    var contact2 = contact
    println(contact == contact2) // false           
}

If you have contact1 and create contact2 by equating it to the first, both point to the same site, the same position in memory. If you change the email in one, it will automatically change in the other because they share the same instance.

fun main() {
    val contact = Contact(1, "mary@gmail.com")
    var contact2 = contact
    // Prints the value of the property: email
    println(contact.email)           
    // mary@gmail.com
    // Updates the value of the property: email
    contact.email = "jane@gmail.com"
    
    // Prints the new value of the property: email
    println(contact2.email)           
    // jane@gmail.com
}

In contrast, with primitive types (like numbers), assignment does create a copy of the value. If you compare two instances of a normal class with the same data, the result will be false because they are in different memory positions, unless you manually implement methods like equals or toString.

var n = 5
var n2 = n
println(n2 = n)

The problem of comparing objects

When we work with real data and want to make comparisons between objects, things get complicated:

  • Two objects can have exactly the same data…
  • …but point to different memory positions.
  • And therefore, the comparison results in false.

This forces us to implement methods like equals, hashCode, and other additional mechanisms just to be able to compare data.

And that is exactly where data classes come in.

Data Class in action

A data class is defined almost the same as a normal class:

  • Name in uppercase.
  • Constructor.
  • Properties.

The difference lies in what Kotlin automatically generates for us.

data class User(val name: String, val id: Int)
val user = User("Alex", 1)
val user2 = User("Andres", 1)
println(user == user2) // false, compares data            

For example, when printing a normal class, what we see is usually a strange memory reference:

class User(val name: String, val id: Int)
val user = User("Alex", 1)
val user2 = User("Alex", 1)
println(user) // FileKt$main$User@3d494fbf    
println(user == user2) // false, compares memory position and not data           

With a data class, however, Kotlin prints the object's values directly, which is already a huge advantage.

println(user) //User(name=Alex, id=1)

Simple and clear comparisons

Another great advantage is that comparisons are very simple.

If we create two instances of a data class with the same values and compare them, the result will be true, because the data is compared, not the memory position.

data class User(val name: String, val id: Int)
val user = User("Alex", 1)
val user2 = User("Alex", 1)
println(user == user2) // true, compares data          

If we change one of the values, the comparison becomes false.

data class User(val name: String, val id: Int)
val user = User("Alex", 1)
val user2 = User("Andres", 1)
println(user == user2) // false, compares data     

Simple and clear.

Copying objects with copy()

Instead of directly modifying an existing instance (which is not always best practice), data classes allow us to use the copy() method.

This is very useful when:

  • We want to create a new entity from another one.
  • We only need to change one or two values.
  • We want to keep instances separate.

For example, we can copy a user and change only the name, keeping the rest of the data intact.

Each copy will have its own memory position, avoiding unexpected errors.

val user = User("Alex", 1)
// Creates an exact copy of user
println(user.copy())       
// User(name=Alex, id=1)
// Creates a copy of user with name: "Max"
println(user.copy("Max"))  
// User(name=Max, id=1)
// Creates a copy of user with id: 3
println(user.copy(id = 3)) 
// User(name=Alex, id=3)

Data Class Destructuring

Another very powerful feature is destructuring.

We can obtain the values of an object in several ways:

Accessing each property directly (obj.name, obj.price, etc.).

Using destructuring by order:

data class Product(val nombre: String, val precio: Double)
fun main() {
	val myProduct = Product("Laptop", 1200.0)
	val (nombre, precio) = myProduct //("Laptop", 1200.0)
}

Using the componentN() methods (component1(), component2(), etc.).

This is especially useful when we work with generic objects or when we don't care about the property names, but rather their position.

data class Product(val nombre: String, val precio: Double)
fun main() {
	val myProduct = Product("Laptop", 1200.0)
	println(myProduct.component1() // "Laptop"
}

Pair and Triple data classes in Kotlin to store pairs of values

Continuing with the Kotlin tutorials, today we will see the Pair data classes in Kotlin, which are simply a generic representation (any type of data or classes) of two values (pairs); they are a kind of tuple but with some variations.

Pair data classes are a structure that allows you to save two values.

Pair values are excellent when we want to save couples of data; and just like that, it is a structure that serves us only for that; the constructor method or rather the basic structure of a pair value in Kotlin looks like the following:

Pair(first: A, second: B)

The first value corresponds to the first value and the second, as you might imagine.

In practice, using pair values in Kotlin is as easy as doing the following:

var pair = Pair("Kotlin Pair",2)

As you can see, we are not forced to have pair values of the same type, they are independent and can take any value we need.

Pair reserved word or to to create pair values

We can also create pair values using the reserved variable to in the following way:

var pair = "Kotlin Pair" to 2

This in Android is very useful as we can use Pair to save a couple of values, for example a user and password or any pair of values that have some relationship.

How to access Pair values in Kotlin?

Now that it is clear what the Pair structure in Kotlin is for, the next thing we are interested in knowing is how to access each of these values embedded within a Pair data class; for this, the reserved words first and second are used respectively in the following way; having our variable called pair defined in any of the previous ways:

println(pair.first) // Kotlin 
println(pair.second) //Pair2

And we get as output:

Kotlin Pair2

We can also use the component1() and component2() methods respectively to obtain the same output as above:

println(pair.component1()) // Kotlin 
println(pair.component2()) //Pair2

For each of the println respectively.

Decomposition of Pair values in Kotlin

We can also separate or decompose the values of the pairs into individual and independent variables in the following way; just as we did before with the data classes in the previous entry:

val (user, password) = Pair("user", "password") println(user) // user println(password) // password

And it is obtained:

user password

Methods of pair values

Pairs in Kotlin have a couple of methods that we can use to employ the entire structure 100%; the first of them is toString which prints a string with the two values:

var pair = Pair("Kotlin Pair",2)
fun main(args: Array<String>) {
    println(pair.toString()) // prints (Kotlin Pair, 2)
}

And we also have the toList() method which returns a list with both pair values:

var pair = Pair("Kotlin Pair",2)
fun main(args: Array<String>) {
    println(pair.toList()) // prints [Kotlin Pair, 2]
}

Inspiration for using pair values

With pair values we can do many things, there are many structures where it is perfect to use a Kotlin Pair; for example the location of a couple of points on a 2D plane:

data class Point<T>(var x: T, var y: T)

Which can be serializable so you can place any value, such as integers or floats; we can use it as a pair as we did previously for a user and password pair, we can also use it to configure a URL and a single parameter:

var url = Pair("https://x.com/","ACY291190")

Now, learn about another type of class, such as the classes listed in Kotlin.

I agree to receive announcements of interest about this Blog.

In this entry we will see how to handle classes in Kotlin, main constructor, secondary constructors, properties, set and get methods, creating class instances, empty classes, data class, Pair y Triple.

| 👤 Andrés Cruz

🇪🇸 En español