Lesson 9 - Companion objects in Kotlin
In the previous lesson, Arena with a mage in Kotlin (inheritance and polymorphism), we put our knowledge of inheritance and polymorphism to the test. In today's tutorial, we're going to go over static class members in Kotlin. Until now, we only used data, states, being carried by an instance. Properties, which we've defined, belonged to an instance and were unique to each instance. OOP, however, allows us to declare properties and methods on a class itself. We call these members static, sometimes called class members, who are independent of an instance.
WARNING! Today's lesson will show you that static members are approaches that actually violate the object-oriented principles. OOP includes them only for special cases and in general everything can be written without static members. We always have to think carefully before adding static members. Generally, I would recommend for you not to use static members ever, unless you're absolutely sure what you're doing. Like global variables, static members are something that enable you to write bad code and violate good practices. Today, we'll go over this topic just to make you understand some static methods and classes in Kotlin, not to write your own. Use this knowledge wisely, there will be less evil in the world.
Static (class) properties
We can declare various elements as static, let's start with properties. As I mentioned in the introduction, static elements belong to a class, not to an instance. So the data stored there can be read even if an instance of the class has not been declared. Basically, we could say that static properties are shared among all class instances, but even that wouldn't be accurate since they aren't actually related to instances.
Let's create a new project, name it something like Statics
, and
create a simple User
class:
class User(private val name: String, private val password: String) { private var loggedIn = false fun logIn(enteredPassword: String) { if (enteredPassword == password) loggedIn = true else loggedIn = false } }
The class is simple, it represents a user in a system. Each user instance has
its own name, password, and carries information about whether the user is logged
in. In order for the user to log-in, we call the logIn()
method
which takes a password as a parameter. The method verifies whether it's the
right password. If the person behind the keyboard is really that user, it logs
him/her in. It returns true
/false
depending on whether
the login was successful. In reality, the password would be also hashed, but we
won't do any hashing this time around.
When a new user registers, the system tells him/her what the minimum length
of their password must be. This number should be stored somewhere.
During user registration, we still wouldn't have its instance.
The object has not been created and would only be created after the form has
been completely filled and submitted. Therefore, we can't use a public
minimalPasswordLength
property in the User
class for
this purpose. Either way, we still need a minimum password length stored in the
User
class somewhere since it, naturally, belongs there. We'll make
this value a static property using the companion
object block:
class User(private val name: String, private val password: String) { private val loggedIn: Boolean init { loggedIn = false } // the rest of the implementation... companion object { val minimalPasswordLength = 6 } }
Now, let's switch to Main.kt
and print the property to the
console. We access this property directly on the class:
fun main(args: Array<String>) {
println(User.minimalPasswordLength)
}
As said before, Java can use Kotlin classes. If we wanted to use static
variables declared in Kotlin from Java, we'd first have to mark each of them
with the @JvmStatic
annotation, i.e. write this text above it.
However, because we only write code for Kotlin, we won't use this feature. .
[hint]
We can see the property really belongs to the class. We can access it from different places in the code without having to create a user. On the contrary, we wouldn't be able to access it on the user instance:
// this code will result in error val u = User("Thomas White", "passwordissword"); println(u.minimalPasswordLength)
IntelliJ will report an error and the code won't be compiled.
Numbering the instances
Another practical use of static properties is to assign unique identification
numbers to each user. If we didn't know what static members were, we'd have to
check every user creation and increment a counter. Instead, we'll create a
private static property nextId
right on the User
class. The next user who registers will have their id stored there. The first
user's id
will be 1, the second 2, and so on. The User will get a
new attribute - id
, that will be set in the constructor depending
on what the nextId
value is. Let's try it:
class User(private val name: String, private val password: String) { private var loggedIn: Boolean val id: Int init { loggedIn = false id = nextId nextId++ } // the rest of the implementation... companion object { private var nextId = 1 val minimalPasswordLength = 6 } }
The class stores the next instance's id by itself. We assign this id to a new instance in the constructor and increase it by 1, which prepares it for the next instance. Not only properties can be static, this approach could be applied to a variety of class members.
Static methods
Static methods are called on the class. All they usually are is
utilities that we need to use often and creating an instance
every time would be counterproductive. Kotlin does support so-called functions
which aren't called on an instance nor class (this is how we use the
println()
function), however, it's much more useful to gather
related functions in classes. That way, IntelliJ will autocomplete them for us
better and we'll be able to open the list of everything we can call on a
particular class by pressing Ctrl + Space.
Let's make another example, just to clarify. During user registration, we
need to know the minimum password length before we create its instance. Also, it
would be great if we could check the password before the program creates the
user. A full check, in this case, would include checking whether the length is
correct, making sure it doesn't contain accent characters, verifying that there
is at least one number in it, and so on. To do this, we'll create a
static validatePassword()
utility
method:
companion object { private var nextId = 1 val minimalPasswordLength = 6 fun validatePassword(password: String): Boolean { if (password.length >= minimalPasswordLength) return true else return false } }
Let's try to call the method on the User
class:
println(User.validatePassword("passwordissword"))
Viewer beware! The validatePassword()
method belongs to the class. Meaning that we can't access any instance
properties in it. Those properties don't exist in the class context,
rather in the context of an instance. It wouldn't make any sense to use the
user's name
in our method! You can try it out if you'd like to get
a visual confirmation of its impossibility.
Password validation can be achieved without knowledge of static members. We
could create a UserValidator
class and write methods in it
accordingly. Then, we'd have to create its instance to be able to call those
methods. It'd be a bit confusing because the "concept of a user" would be
unnecessarily split up into two classes. Now, thanks to static members, all of
the information is neatly organized in one place.
We didn't break encapsulation in the example with the static
minimalPasswordLength
property because we declared the property as
val
.
Finally, we'll test our static properties. The main()
method
will look like this:
val u = User("Thomas White", "passwordissword") println("First user's ID: ${u.id}") val v = User("Oli Pickle", "csfd1fg") println("Second user's ID: ${v.id}") println("Minimum password length: ${User.minimalPasswordLength}") println("""Validity of password "password" is ${User.validatePassword("password")}""")
Output screenshot:
First user's ID: 1 Second user's ID: 2 Minimum password length: 6 Validity of password "password": false
Private constructor
If a class contains only utility methods or it
doesn't make sense to create an instance of it, for example we'll never
run two consoles at a time, we call it static class/object. Kotlin lets us to
mark the class as static by using the object
keyword. This object
is then not possible to instantiate (we can't create an
instance of such class).
Let's create a test object A
which we won't be able to
instantiate. We'll replace class
by the object
keyword:
object A {
// Here we could write our methods and properties
}
Let's try to instantiate the A
object:
val a = A()
We'll get an error because instantiation is prohibited. All properties of the object are static and therefore it doesn't make sense to create an instance of it; it wouldn't contain anything.
Kotlin also supports the static constructor which is called when the class is registered. We create it the same way as a class constructor, only in object.
object A { init { // Here would be our static construction code } // Here we could write our methods and properties }
If we wanted to have such constructor in a regular class and initialize static variables in it, we can do so:
class Class { companion object { init { // Here would be our static construction code } // Here we could write our methods and properties } }
Static registry
Let's create a simple static class. We could create a class containing only utility methods and properties. However, I've decided I'll have us create a static registry. Which is basically how you share important data between classes without making an instance.
Let's say we're making a more robust application, e.g. a digital journal. The application would be multilingual, its features would include adding bookmarks, choosing folders for storing files, color schemes and even running at system startup. It would have adjustable settings, e.g. picking the first day of the week (Sunday/Monday), which would be accessed from various parts in the code. Without knowledge of static members, we'd have to pass the settings instance to all objects (calendar, tasks, notes...) through the constructor.
One workaround would be to use an object
to store these
settings. It'd be accessible everywhere in the program, without even having to
create an instance. It'd contain all the necessary settings that would be used
by other objects. Our object
would end up looking something like
this:
object Settings { val language = "ENG" val colorScheme = "red" val runAtStartUp = true }
Now we'll put our class to use even though we don't have a full journal
application. Let's make a Calendar
class and verify that we can
truly access Settings
there. We'll add a method to it that returns
all of the settings:
class Calendar { fun getSettings(): String { return "Language: ${Settings.language}\n" + "Color scheme: ${Settings.colorScheme}\n" + "Run at start-up: ${Settings.runAtStartUp}" } }
Last of all, we'll print it all to the console:
val calendar = Calendar() println(calendar.getSettings())
The output:
Language: ENG Color scheme: red Run at start-up: true
We see that calendar instance has no problem accessing all program settings.
Again, be careful, this code can be improperly used to share unencapsulated data. We must only use it in specific situations. Most data exchanges should happen using parameterized instance constructors, not through static members.
Static members appear very often in design patterns, we've already mentioned them in our lessons. We'll get in further detail in the approaches that bring object-oriented programming to perfection later on. For now, this will do. I don't want to overwhelm you In the next lesson, Properties in Kotlin, we'll look at what properties are in Kotlin.