Matteo Di Pirro I am an enthusiastic young software engineer who specialized in the theory of programming languages and type safety. I enjoy learning and experimenting with new technologies and languages, looking for effective ways to employ them.

How to create static methods and classes in Kotlin

7 min read 2149

Kotlin Logo Over Colorful Swirl

One of the key differences between the object-oriented model in Kotlin and Java is the definition of static methods and classes. Generally speaking, a static class doesn’t have to be instantiated in order to be used. Similarly, to invoke a static method, we just have to know its name, not to instantiate an object of the class where such a method is defined.

In Java, classes may easily declare static fields or methods. However, the definition of a static class is a bit more complex. Kotlin, on the other hand, greatly simplifies the job with native constructs such as package-level functions, objects, and companion objects.

In this article, we are first going to look at the legacy Java way of declaring static classes and methods. Secondly, we’ll see how the same thing can be achieved, with much less effort, in Kotlin. Then, we’ll compare the benefits of Kotlin’s way with respect to code reusability.

Table of contents:

Static classes and methods in Java

In object-oriented programming (OOP), static methods and fields are useful to model common values or operations that do not need to be tied to an instance of a class. For example, the Math class contains several static fields and methods for common constants (such as pi) and mathematical operations (such as max and min):

public final class Math {
    public static final double E = 2.7182818284590452354;
    public static final double PI = 3.14159265358979323846;

    public static int max(int a, int b) { ... }
    public static int min(int a, int b) { ... }
}

Static classes, on the other hand, are more tricky to define. In Java, only nested classes (that is, classes defined within another class) can be declared static. They do not need a reference to the outer class (the class they’re declared within). Hence, while we may not instantiate non-static nested classes without an instance of the outer class, static classes are independent.

Furthermore, the class loader loads the code of static classes when they are first used and not when the enclosing class gets loaded. This allows us to reduce the memory footprint of our application.

For example, we may want to use static classes to implement a thread-safe singleton class without paying the price of synchronizing the getInstance method:

public class Singleton {
    private Singleton(){ }       

    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }

    // …
}

In the example above, using a static class to hold a reference to the singleton instance gives us some nice properties:

  • the class-loader only loads SingletonHolder the first time it’s accessed (via Singleton::getInstance());
  • when a Java class is loaded, it is guaranteed that all of its static properties are initialized. Hence, SingletonHolder::INSTANCE gets instantiated immediately before the first usage;
  • SingletonHolder::INSTANCE can be declared final even though it’s lazily initialized;
  • the class-loader itself is thread-safe, which makes the first two properties thread-safe

Besides performance reasons, static classes are often used to improve the readability and the maintainability of the code, as we can use them to move components closer to where they are used.



Static classes in Kotlin

As Java, Kotlin allows us to define nested classes in our code. In Kotlin, however, a nested class is static by default. That is, by default nested classes in Kotlin do not hold a reference to the enclosing class:

class Car(val model: String) {
    class Engine(val fuel: String)
}

fun main() {
    val engine = Car.Engine("Gasoline")
    val car = Car("SomeModel")

    println(engine.fuel)
    println(car.model)
}

In the example above, we defined a nested class Engine inside a class Car. As we can see, we can instantiate Car and Engine separately. In particular, we create an instance of Engine without providing a reference to an object of Car. The only clue that Engine is defined inside Car is its qualified name, Car.Engine. The example above prints Gasoline and SomeModel.

Static classes in Kotlin can access the properties of the companion object of the enclosing class. We’ll see more about companion objects below.

If we want to define a nonstatic nested class in Kotlin we have to declare it as inner:

class Car(val model: String) {
    inner class Engine(val fuel: String) {
                val forModel = [email protected]
    }
}

fun main() {
    val engine = Car("SomeModel").Engine("Gasoline")
    println("${engine.forModel} - ${engine.fuel}")
}

Now that Engine is marked as inner, we have to create an instance of Car and use it to instantiate Engine. From within Engine, we can reference the outer object using [email protected]. The example prints SomeModel - Gasoline.

Similar to Java, nested classes can be declared also in the scope of a method of the enclosing class. In this case, the new class would be a local type.

The main benefit of Kotlin’s approach is that it limits the risks of memory leaks, by default. In Java, it is easier to overlook the fact that a given nested class holds a reference to the enclosing class. In Kotlin, on the other hand, such a reference does not exist by default.


More great articles from LogRocket:


Whether to use an inner class or a static one largely depends on the way we’re modeling our domain. Surely, static classes allow for greater code reusability, as we do not need to instantiate the enclosing class, while letting us define (possibly) dependent components close to one another.

Static methods in Kotlin

Kotlin greatly simplifies how we can define static methods or variables. In particular, it does so using (companion) objects and package-level functions.

Package-level functions

Kotlin is not exclusively an object-oriented language because it also supports the functional programming paradigm: this is where package-level functions come from. As the name suggests, they are functions (or members) that do not belong to a given class but are instead defined within a package. Often, they are utility functions that are independent of any other class.

For example, we can use them to implement handy functions to initialize a class. Assume we have a class named Point to model a point in the Cartesian plane:

package com.logrocket.blog
class Point(val x: Int, val y: Int) {
    override fun toString(): String = "Point($x, $y)"
}

Then, in a different package, we might define the following functions:

// In file factory.kt
package com.logrocket.blog.utils

import com.logrocket.blog.Point

val centerPoint = Point(x = 0, y = 0)

fun onXAxis(x: Int) = Point(x, y = 0)

fun onYAxis(y: Int) = Point(x = 0, y)

We can then use the functions and values above just by importing them:

package com.logrocket.blog

import com.logrocket.blog.utils.centerPoint
import com.logrocket.blog.utils.onXAxis


fun main() {
    val point = onXAxis(5)
    println(centerPoint)
    println(point)
}

The main function above prints the strings Point(0, 0) and Point(5, 0).

Note how we defined two package-level functions in the com.logrocket.blog.utils package, onXAxis and onYAxis. We also defined a package-level value, centerPoint. Both the functions and the value can be accessed without any references to any enclosing classes, as we’d have done in Java: we just have to import them.

Package-level functions and values are syntactic sugar for static fields and methods in Java. What the Kotlin compiler does is generate a Java class named after the Kotlin file, with static methods and fields in it. For example, the functions in com.logrocket.blog.utils.factory.kt will be compiled into a class named com.logrocket.blog.utils.FactoryKt (where the name of the class is built using the name of the file and Kt, in PascalCase):

package com.logrocket.blog.utils

// Generated class
class FactoryKt {
    public static Point centerPoint = new Point(0, 0);
    
    public static Point onXAxis(int x) {
            return new Point(x, 0);
    }

    public static Point onYAxis(int y) {
            return new Point(0, y);
    }
}

If we wanted to change the name of the generated Java class, we could use the @JvmName annotation. For example, if we place the annotation @file:JvmName("PointFactory") at the beginning of factory.kt, the generated class will be named PointFactoryKt instead of FactoryKt. Such an annotation must appear before the package directive.

Lastly, if we want more utility functions to be compiled into the same generated Java class, or if we already have a file named pointfactory.kt, we can use the @JvmMultifileClass annotation. This way, the compiler will generate a Java façade class with the specified name and all the declarations from all the Kotlin files with the same JvmName.

Objects

By declaring an object in Kotlin we define a singleton, that is, a class with only one instance. Such an instance is created lazily, the first time it’s used, in a thread-safe manner.

For example, we could define the following object to group the functions and values we defined above:

object PointFactory {
    val center = Point(x = 0, y = 0)
    fun onXAxis(x: Int) = Point(x, y = 0)
    fun onYAxis(y: Int) = Point(x = 0, y)
}

Then, differently than before, we have to specify the name of the object to access its functions. In other words, an object defines a scope:

val point = PointFactory.onYAxis(5)

As there’s only one instance of each Kotlin object, the qualified name of the object is enough to access its members. This is similar to but slightly different than a Java class consisting of static methods or variables only. In the latter case, we could instantiate that Java class as many times as we wanted (assuming the constructor is not private). In that case, the static variables would be the same for each different instance of the class. With Kotlin objects, on the other hand, we only have one instance.

Companion objects

In the example above, PointFactory is pretty tied to the Point class, as it contains several methods to instantiate a point. For cases like this, we can make it a companion object, making this tight coupling more explicit:

class Point(val x: Int, val y: Int) {
    companion object {
                val center = Point(x = 0, y = 0)
                fun onXAxis(x: Int) = Point(x, y = 0)
                fun onYAxis(y: Int) = Point(x = 0, y)
    }

    override fun toString(): String = "Point($x, $y)"
}

With companion objects, we can declare methods and tie them to a given class, rather than to its instances. As for “normal” objects, companion objects are singletons. Hence, we can reference the functions by specifying the name of the class:

val point = Point.onYAxis(5)

A comparison

Kotlin provides us with three different solutions to define static methods or fields.

Package-level functions and values are the most idiomatic way. Often there’s no need to scope utility methods inside a class. In such cases, package-level members are a fine choice allowing for greater reusability of the code. As a matter of fact, most of the standard library is implemented using them.

However, objects and companion objects do have some pros. For example, they allow for a better scoping of methods and fields. One of the main cons of package-level members is that they pollute the auto-completion suggestions available in most IDEs, making it more difficult to pick the right function. The scope of an object solves this issue.

Conclusion

Strictly speaking, in a pure object-oriented programming mindset, everything is better defined inside of a class. However, as we saw above, often we need methods that are different to place in an existing class. This can happen, for example, with utility methods that operate on a class but do not represent the behavior of that class.

In languages like Java, the normality is to define Utils or Helper classes full of the methods that are different to scope in a certain class. This gets easily out of control and leads to classes with different responsibilities and heterogeneous methods that are very difficult to read, maintain, and re-use.

Kotlin, on the hand, is not just an object-oriented language. It supports other programming paradigms, such as the functional one. Hence, it does not take the object orientation as strictly as Java, allowing us, for example, to define functions that are not tied to any class.

On the one hand, this improves the reusability and the maintainability of the code. Furthermore, we can use the package structure and the visibility keywords to choose which portion of our codebase can use a given function or object. Better still, with companion objects, we can define utility code as close as possible to the class it operates on. On the other hand, we ought to pay attention to the freedom and flexibility of Kotlin’s approach. For example, nothing prevents us from defining a mutable package-level variable, essentially a global one, which can be very harmful.

As is common with modern programming languages, we have a number of ways to model the same thing and achieve the same result. Hence, it is always a matter of experience and sensibility to figure out what the right construct is and to use it appropriately.

: Full visibility into your web and mobile apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page web and mobile apps.

.
Matteo Di Pirro I am an enthusiastic young software engineer who specialized in the theory of programming languages and type safety. I enjoy learning and experimenting with new technologies and languages, looking for effective ways to employ them.

Leave a Reply