This is part 3 of the series Kotlin in Action Summary. Here is the link to part 2 which we talked about chapter 3 of the book with the title "Defining and Calling Functions". As mentioned before, it is highly recommended to read the book first and consider this series as a refresher. We do NOT cover all the materials in the book. Let's start with part 3, chapter 4 of the book: "Classes, Objects and Interfaces"!
Contents
- Interfaces
- Open, Final and Abstract Modifiers
- Visibility Modifiers
- Inner and Nested Classes
- Sealed Classes
- Constructors and Properties
- Universal Object Methods
- Data Classes
- Class Delegation
- The
object
Keyword
Interfaces
pages 68, 69, 70
Kotlin's interfaces are similar to that of Java 8. They can contain abstract methods and implementation of non-abstract methods(like Java 8 default methods), but they cannot have any state!
This is how we declare and use interfaces in Kotlin:
interface Clickable {
fun click()
fun showOff() = println("Clickable showOff")
}
interface Focusable {
fun setFocus(b: Boolean)
fun showOff() = println("Focusable showOff")
}
class Button: Clickable, Focusable {
override fun click() = println("button clicked")
override fun setFocus(b: Boolean) = println("set focus")
override fun showOff() {
super<Focusable>.showOff()
super<Clickable>.showOff()
}
}
Here are some points to consider:
- In Kotlin, we use colon
:
instead ofimplements
andextends
keywords -
override
is like the@Override
annotation in Java, but it is mandatory to use in Kotlin. - To provide implementation for a method in Kotlin, we do not need to use the
default
keyword. - If we implement two interfaces in a class that contain the same method signature, we must provide the implementation of that method in our class.
- To call the parent class's method, we need to use the
super
keyword in Kotlin. But to select a specific implementation we need to usesuper
alongside with theClassName
inside angle brackets:super<Clickable>.showOff()
Open, Final and Abstract Modifiers
pages 70, 71, 72
In contrast to java, Kotlin classes are final
by default. If we want to allow a class to be extended, we need to mark it with the open
modifier. All properties and methods of that class are also final
, so we have to use the open
modifier for every one of them that we want to be overridden.
open class RichButton : Clickable {
// 1
fun disable() {}
// 2
open fun animate() {}
// 3
override fun click() {}
}
- 1 ->
disable()
function is not open (methods are final by default), so it cannot be overridden - 2 ->
animate()
function is open, so it can be overridden - 3 ->
click()
function overrides an open method, so it is open itself and can be overridden
NOTE: If we override a member of a base class or interface, the overridden member is open by default. To prevent it from being overridden, we should mark it with final
modifier.
As in Java, Kotlin supports abstract classes which cannot be instantiated. Why do we use abstract classes? Sometimes we need a contract but do not want to provide implementation. We want the implementation to be provided by the subclasses. Abstract members of an abstract class are always open and do not require us to use open
keyword:
abstract class Animated {
// 1
abstract fun animate()
// 2
open fun stopAnimating() {}
// 3
fun animateTwice() {}
}
- 1 ->
animate()
function is marked withabstract
keyword, so it is open and do not need theopen
modifier - 2 ->
stopAnimating()
function is marked withopen
, so it can be overridden - 3 ->
animateTwice()
function is neitherabstract
noropen
, so it cannot be overridden by subclasses.
NOTE: A member in an interface is always open; we cannot declare it to as final. It's abstract if it has no body, but the keyword is not required.
Visibility Modifiers
pages 73, 74
Visibility modifiers help us to control access to declarations in our code base. Why should we do so? Because by enforcing encapsulation, we are sure that other classes do not depend on the implementation of our class, but rather on the abstraction. We can freely change the implementation and do not break anything in the client code!
Visibility modifiers in Kotlin are similar to those in Java. We have public
, private
, and protected
with some differences. One difference is that, a class with no modifier is public by default and Kotlin does not support package-private modifier.
Why Kotlin does not have package-private modifier? Because they have replaced it with something better: we can use internal
to declare a class to be visible inside a module.
What is a module? A module is a set of Kotlin files compiled together. It might be IntelliJ IDEA module, Gradle or Maven project.
What is the advantage? It provides real encapsulation for the implementation details of our module.
Another difference is that Kotlin allows the use of private
visibility for top-level declarations, including classes, functions and properties. These are visible only in the file where they are declared.
To list the difference in visibility between Java and Kotlin:
- Kotlin default visibility is
public
- Kotlin does NOT have package-private visibility
- Kotlin supports module private visibility with
internal
keyword. - Kotlin supports private visibility for top-level declarations meaning visible inside the file.
-
protected
modifier in Kotlin is just visible to the class itself or its subclasses, but in Java, it is also visible to classes in the same package! - Unlike Java, an outer class doesn't see
private
members of its inner (or nested) classes in Kotlin - Kotlin forbids us to reference less visible types from more visible members(e.g. referencing a
private
class from apublic
function) because it will expose the less visible type!
// 1
fun TalkativeButton.giveSpeech() {
// 2
yell()
// 3
whisper()
}
- 1 ->
- 2 ->
- 3 ->
Inner and Nested Classes
pages 75, 76,
As in Java, you can declare a class inside another class in Kotlin. Difference? Unlike Java, the Kotlin inner class does not have access to the outer class instance, unless we specify so.
A nested class in Kotlin is the same as a static
nested class in Java. To turn a nested class to an inner class (so the inner class has a reference to the outer class), we have to mark it with inner
modifier.
How do we reference an instance of an outer class inside the inner class? We use this@OuterClassName
syntax as in the following example:
class Outer {
inner class Inner {
fun getOuterReference(): Outer = this@Outer
}
}
In Java, we use OuterClassName.this
syntax to refer to the outer class instance.
Sealed Classes
pages 77, 78
On the first glance sealed
classes are similar to enums
. The difference is quite obvious — enums
are a group of related constants, sealed
classes are a group of related classes with different states. The other difference is that, enum
constant is available only in one instance, whereas sealed
classes can have multiple instances with different values.
We use sealed
classes when we want to impose restriction on the number of options and values to a super class. What would be the benefit? First, we don't have to provide an else
branch in when
expressions. Second, when we add another subclass to the super class, we get a compile time error if we don't cover that subclass in the when
expressions that we have written.
Let's see an example:
interface Result
class Success(val data: Any) : Result
class Error(val error: Exception) : Result
fun result(result: Result) : Any =
when (result) {
is Success -> result.data
is Error -> result.error
else -> throw java.lang.IllegalArgumentException("Unknown Expression")
}
In the sample above, there are some problems. First problem is that we always have to provide an else branch for every when expression we write for Result
interface. Also, if we decide to add another type to the subtypes of the Result
, say NetworkError
, then we will face an IllegalArgumentException
at runtime.
To resolve these issues, we use sealed classes. When we use sealed classes and handle all the subclasses in a when
expression, we don't have to provide the else branch. If we add another subclass and forget to handle that new subclass in a when
expression, we'll get a compile time error. Let's see the above example converted to a sealed class:
// 1
sealed class Result<out T> {
// 2
data class Success<out T>(val data: T) : Result<T>()
data class Error(val error: Exception) : Result<Nothing>()
}
fun <T> result(result: Result<T>) : Any? =
// 3
when (result) {
is Result.Success<T> -> result.data
is Result.Error -> result.error
}
- 1 ->
sealed
classes are open by default (because they are implicitlyabstract
), we don't have to provide theopen
modifier - 2 -> we are able to subclass
sealed
classes just inside it or in the same file it is declared in - 3 -> when we use a
sealed
class inside awhen
expression and we cover all possible cases, we don't have to provide theelse
branch
NOTE: Under the hood, the sealed
class has a private constructor which can be called only inside the class.
To list all the characteristics of sealed
classes:
- They are implicitly abstract
- Subclasses must be declared inside itself or in the same file as it is in
- They ease the pain of providing else branches when we cover all the instances
- If we add another instance and we forget to cover that instance inside a
when
expression, we will get a compile time error
Constructors and Properties
pages 79, 80, 81, 82, 83, 84, 85, 86
Primary Constructors
pages 79, 80, 81
You can read about Kotlin classes and constructors at Kotlin Official site.
Kotlin, just like Java, allows us to declare more than one constructor. However, Kotlin makes distinction between a primary constructor and secondary ones.
class User(val name: String)
The primary constructor is declared after the class name, outside the class body. We can use the constructor
keyword, or omit that if we don't have annotations or visibility modifiers. The primary constructor roles are:
- specifying constructor parameters
- defining properties that are initialized by constructor parameters
If we want to write the equivalent Java way of the code above, it would look like this:
// 1
class User constructor(
// 2
_name: String
) {
val name: String
// 3
init {
name = _name
}
}
In the code snippet above we have two new keywords:
- 1 ->
constructor
: begins the declaration of a constructor in Kotlin - 2 ->
init
: introduces an initializer block - 3 ->
underscore
: the underscore in constructor parameter is used to distinguish the parameter name from the property name. We could have usedthis.name = name
instead.
What is an initializer block?
An initializer block contains code that's executed when the class is created and intended to be used together with primary constructors.
Why do we need initializer blocks? Because the primary constructors are concise in Kotlin and cannot contain logic, we need initializer blocks.
NOTE: We can have more than one initializer block in a class. Initializer blocks are executed in the same order as they appear in the code.
In the code snippet above, there can be two modifications:
- we can omit the
constructor
keyword if there are not visibility modifiers or annotations on primary constructor - we can omit the initializer block and assign the property to to parameter directly.
class User(_name: String) {
// 1
val name = _name
}
- 1 -> This is called property initialization
Kotlin has an even more concise syntax for declaring properties and initializing them from the primary constructor. These properties can be declared as immutable (val
) or mutable (var
).
class User(val name: String)
As with functions, we can declare constructors with default values in Kotlin:
class User(val name: String = "John Doe")
NOTE: If all constructor parameters have default values, the compiler generates an additional constructor without parameters that uses the default values. It does so to make it easier to use Kotlin with libraries that instantiate classes without parameters.
If we extend another class, our primary constructor must initialize the super-class:
// 1
open class User(val name: String) {
}
// 2
class TwitterUser(username: String) : User(username) {
}
- 1 -> mark the super-class
open
to be able to extend it - 2 ->
TwitterUser
must call its super-class constructor
Just like Java, if we don't provide any constructors, a no-arg constructor is generated for the class. And if we extend this class, we still need to call its constructor:
// 1
open class User
// 2
class InstagramUser : User() {
}
- 1 -> We did not declare any constructor for the
User
, but the compiler generates a no-arg constructor for the class - 2 -> Although
User
does not have any declared constructor or parameters, we still need to call its generated constructor!
NOTE: Be aware of the difference between extending a class and implementing an interface in Kotlin. Because interfaces don't have any constructors, we don't put parentheses after their name when implementing them!
NOTE: To make sure that a class cannot be instantiated, we should make the primary constructor private
:
class User private constructor() {
}
Secondary Constructors
pages 81, 82, 83
Because Kotlin has default values for constructor parameters, secondary constructors are not used as widely as they are used in Java.
We declare secondary constructors (like primary constructors) with the constructor
keyword.
Let's take a look at an example from the Android world:
open class View {
constructor(ctx: Context) {
}
constructor(ctx: Context, attr: AttributeSet) {
}
}
class TextView : View {
// 1
constructor(ctx: Context) : super(ctx) {
}
constructor(ctx: Context, attr: AttributeSet) : super(ctx, attr) {
}
}
- 1 -> if we extend a super-class and declare secondary constructors, we need to call the super class constructor in every constructor that we declare by using the
super
keyword (the exception is when we call another constructor of our class)
Just as in Java, we can call another constructor of our class using the this
keyword:
class TextView : View {
val myStyle = // some code to instantiate myStyle
constructor(ctx: Context, attr: AttributeSet) : super(ctx, attr) {
}
// 1
constructor(ctx: Context) : this(ctx, myStyle) {
}
}
- 1 -> we use
this
keyword to call another constructor of our class
Properties in Interfaces
pages 83, 84
Kotlin interfaces can contain abstract properties.
interface User {
// 1
val name: String
}
- 1 -> The interface does not have a state because of this property because it does not specify whether it is a field or just a getter. It just means that classes that implement the interface need to provide a way to obtain the value of
name
. Pay attention that all classes are overriding thename
property.
// Primary constructor property
class PrivateUser(override val name: String) : User
// Custom getter -> it does not have a backing field
class SubscribingUser(val email: String) : User {
override val name: String
get() = email.substringBefore('@')
}
// Property initializer
class FacebookUser(val accountId: Int) : User {
override val name = getFacebookName(accountId)
private fun getFacebookName(accountId: Int): String {
//code to get the facebook name
}
}
Accessing Backing Fields
page 85
Let's see how we can access a backing field from the accessors. To do so, we use the field
identifier in the body of a setter.
class User7(val name: String) {
var address: String = "unspecified"
// 1
set(value) {
// 2
println(
"""
address changed for $name:
$field -> $value
""".trimIndent()
)
// 3
field = value
}
get() {
// 2
return "$name address: $field"
}
}
- 1 -> We define a setter by writing a
set
function under the property (the property must be mutable (var
)) - 2 -> We can access the property backing field by the
field
keyword. - 3 -> We change the value of the property
NOTE: If we provide custom accessors for a property and do not use the field
keyword, the compiler won't generate a backing field for that property (for getter in val
property and for both accessors in var
property).
class User(val name: String) {
// 1
var address: String = "unspecified"
set(value) {
println(
"address for $name: $value"
)
}
get() {
return "$name address: "
}
}
If we write such a code, it won't compile with the error Initializer is not allowed here because this property has no backing field. Why? Because we did not use the field
keyword in neither the setter nor the getter.
Changing Accessor Visibility
page 86
The accessor's visibility is the same as the property's visibility. We can change the accessor's visibility by putting a visibility modifier before the get
or set
keyword.
class User(val name: String) {
// 1
var id: Int = 0
// 2
private set(value) {
field = getAutoGeneratedId()
}
private fun getAutoGeneratedId(): Int {
// code to get an auto generated id
}
}
- 1 -> the property's visibility is public and can be accessed outside the class
- 2 -> we limit the setter's visibility to the class because we do not want other classes to set this property
NOTE: Accessors cannot be more visible than the property itself. Meaning that if the property is protected
, the accessors can be private
but cannot be public
!
Universal Object Methods
pages 87, 88, 89
We use some classes specifically to hold data. These classes usually need to override some methods like toString()
, equals()
, and hashCode()
. Let's see how we should implement them in Kotlin.
First, we declare a Person
class.
class Person(
val name: String,
val phoneNumber: String,
val address: String
)
toString()
toString()
method is used primarily for debugging and logging. It should be overridden in almost every class to make the debugging easier. If we do not override the toString()
method, when we print an object, the result looks like Person@23fc625e
class Person(
val name: String,
val phoneNumber: String,
val address: String
) {
override fun toString(): String {
return "Person(name=$name, phone number=$phoneNumber, address=$address"
}
}
equals()
equals()
method is used to check whether two objects' values are equal, not the references!
class Person(
val name: String,
val phoneNumber: String,
val address: String
) {
override fun toString(): String {
return "Person(name=$name, phone number=$phoneNumber, address=$address"
}
override fun equals(other: Any?): Boolean {
if (other == null || other !is Person)
return false
return name == other.name &&
phoneNumber == other.phoneNumber &&
address == other.address
}
}
hashCode()
The hashCode()
method should be always overridden together with the equals()
method! If two objects are equal, they must have the same hash code.
class Person(
val name: String,
val phoneNumber: String,
val address: String
) {
override fun toString(): String {
return "Person(name=$name, phone number=$phoneNumber, address=$address"
}
override fun equals(other: Any?): Boolean {
if (other == null || other !is Person)
return false
return name == other.name &&
phoneNumber == other.phoneNumber &&
address == other.address
}
override fun hashCode(): Int {
return name.hashCode() +
phoneNumber.hashCode() +
address.hashCode()
}
}
NOTE: Why the hashCode()
method should be always overridden together with the equals()
method? Because some clients (like HashSet
) first use the hashCode()
method to check for equality and if the hash codes are equal, then they check the equals()
method!
Data Classes
pages 89, 90, 91
Kotlin has made it easier for us to declare classes to hold data. We just mark that class with the data
modifier and the Kotlin compiler would generate the previously mentioned methods for us! The compiler will generate toString()
, equals()
, hashCode()
, (also copy()
, and componentN()
) functions from the properties declared in the primary constructor.
data class Person(
val name: String,
val phoneNumber: String,
val address: String
)
The Person
class above is somehow equal to the Person
class we declared before this (with toString()
, equals()
, hashCode()
methods overridden).
Data classes must fulfill the following requirements:
- The primary constructor needs to have at least one parameter.
- All primary constructor parameters need to be marked as val or var.
- Data classes cannot be abstract, open, sealed, or inner.
NOTE: As we mentioned earlier, the compiler just takes into account the properties in the primary constructor. In other words, we can declare properties in the class body to exclude it from generated implementations(like toString()
and hashCode()
).
copy()
Method
It's strongly recommended that we use read-only (val
) properties to make the instances of data classes immutable. This is mandatory if we want to use such instances as keys in a HashMap
or a similar container, because otherwise the container could get into an invalid state if the object used as a key is modified after it was added to the container.
Class Delegation
Sometimes we need to change the behavior of a class, but we cannot extend that class (either our class has another parent class, or the class we want to extend is not open
). In this situation, we can use the decorator pattern:
- We implement the same
interface
that the class we want to extend is implementing - We declare a property of type of the class we want to extend
- We forward the requests that we don't want to change to that property
- If we want to change the behavior of a method, we will
override
that method in our class Let's see this pattern in practice:
class DelegatingCollection<T> : Collection<T> {
private val innerList = arrayListOf<T>()
override val size: Int get() = innerList.size
override fun isEmpty() = innerList.isEmpty()
override fun iterator() = innerList.iterator()
override fun containsAll(elements: Collection<T>) = innerList.containsAll(elements)
override fun contains(element: T) = innerList.contains(element)
}
Here, we have used the decorator pattern as we use it in Java. We forward every call to the innerList
variable of type ArrayList
. If we want to change the behavior of the method isEmpty()
, we can easily do so be defining our own implementation.
Fortunately, Kotlin has provided a first-class support for delegation. To use this feature:
- We extend the same interface as our class we want to extend
- We declare a property of the class we want to extend
- We use the
by
keyword after the interface implementation.
class DelegatingClass<T>(
val innerList: Collection<T> = ArrayList<T>()
) : Collection<T> by innerList
This is just the same as the implementation before with much less boilerplate code! We have used the by
keyword to delegate the requests to the object specified(innerList
here)!
The object
Keyword
pages 93, 94, 95, 96, 97, 98, 99, 100, 101
The object
keyword in Kotlin is used to define a class and create an instance of that class at the same time.
Object Declaration
pages 93, 94, 95
Singleton design pattern is used fairly common in object-oriented design systems. Kotlin provides object declaration which combines the class declaration and a declaration of a single instance of that class.
object DataProviderManager {
fun registerDataProvider(provider: DataProvider) {
// ...
}
val allDataProviders: Collection<DataProvider>
get() = // ...
}
Object declarations are created with object
keyword. An object declaration defines a class and a variable of that class in a single statement.
NOTE: If we pay attention to its name (object declaration), we understand its purpose. We declare an object, and to declare an object, we must create an instance of that object. With object declarations, we combine the two steps.
An object declaration is just like a normal class with one difference: it does not allow constructors to be declared in it. Objects of object declarations are created immediately at the point of definition, not by calling their constructors.
NOTE: Object declaration, like a variable declaration, is not an expression and cannot be used on the right-hand side of an assignment statement!
Object declarations allow us to call their methods and access their properties by using the class name and the . character:
val numberOfDataProviders = DataProviderManager.allDataProviders.size
DataProviderManager.registerDataProvider(/*...*/)
Object declarations can inherit from classes and interfaces. This is often useful when the framework requires us to implement an interface, but our implementation does not contain any state! The Comparator
interface is a great example for this. A Comparator
implementation receives two objects and returns an integer indicating which of the two objects is greater! Comparators never store any data, so we usually need a single Comparator
instance for a particular way of comparing objects. This is a perfect use for object declaration!
data class Person(val name: String, val age: Int)
object AgeComparator : Comparator<Person> {
override fun compare(p1: Person, p2: Person): Int {
return p1.age - p2.age
}
}
We can use singleton objects in any context where an ordinary object can be used.
We can also declare objects in a class. Such objects also have just a single instance; they don't have a separate instance per instance of the containing class. Take the Person
class as an example. Isn't it more logical to put different Comparator
implementations inside the class itself for further use?
data class Person(val name: String, val age: Int) {
object AgeComparator : Comparator<Person> {
override fun compare(p1: Person, p2: Person): Int {
return p1.age - p2.age
}
}
object NameComparator : Comparator<Person> {
override fun compare(p1: Person, p2: Person): Int {
return p1.name.compareTo(p2.name)
}
}
}
To refer to the instance of the singleton object in Java which is created by the object declaration, we should use the INSTANCE
static field:
AgeComparator.INSTANCE.compare(person1, person2);
Companion Objects
pages 96, 97, 98, 99, 100
Kotlin does not have the static
keyword which exists in Java. Most of the time, our needs for static
members and methods is satisfied with top-level member and functions in Kotlin. However, we cannot access private
members of the class. So, if we need to write a function that can be called without an instance of the class, but has access to the private members of the class, we can use the object declaration
inside that class.
NOTE: Please be aware that companion object
(just like static methods) cannot access instance members of the class. To do so, we must provide an instance of the class for the companion object:
class Person {
private lateinit var name: String
object Print {
fun printName1(person: Person) {
println(person.name)
}
fun printName2() {
val person = Person()
println(person.name)
}
}
}
If we mark the object declaration
with the keyword companion
, we can access the methods and properties inside that object declaration directly through the name of the containing class (without specifying the object name).
For example, to call the methods in the above example, we use this syntax:
Person.Print.printName1(Person())
Person.Print.printName2()
Now, if we omit Print
(the object declaration name) and put the companion
keyword before object
, we can use the methods like static methods in Java:
class Person {
private lateinit var name: String
companion object {
fun printName1(person: Person) {
println(person.name)
}
fun printName2() {
val person = Person()
println(person.name)
}
}
}
To use the methods above, we use this syntax:
Person.printName1(personInstance)
Person.printName2()
Note that we do not use any object name (Print
in previous example) to call the methods inside companion object
.
One of the most important use-cases of companion object
is to implement the factory pattern. Why? Because companion objects have access to the private members of the class, including the constructors.
class Person private constructor(val name: String, val role: Role){
companion object {
fun newManager(name: String) =
Person(name, Role.MANAGER)
fun newEmployee(name: String) =
Person(name, Role.EMPLOYEE)
}
enum class Role {
MANAGER, EMPLOYEE
}
}
Here, we have used factory methods to create instances of the Person
object. To create instances of Person
, we use the class name with the method from the companion object:
Person.newManager("Tom")
Factory methods are useful for several reasons:
- They can be named according to their purpose
- Can return subclasses depending on the method that we invoke
- We can also avoid creating new objects when it's not necessary.
The downside with using factory methods instead of constructors is that we cannot override them in subclasses!
Companion Objects Extension
We can define an extension function (just like normal extension functions) for companion objects. The only requirement is that the class should have at least one companion object. If it does not have any, we should define an empty one to extend it.
Suppose we want to add an extension function (say setupFakeManager()
) to the Person
class we declared earlier. Here is a sample code:
fun Person.Companion.setupFakeManager() : Person {
return Person.newManager("FakeManager")
}
Because we had a companion object in the Person
class, we just extended the companion object to have another function. Note the Companion
keyword after the class name. By using the Companion
keyword, we are indicating that we are extending the companion object!
Object Expressions
The third use-case of the object
keyword is to declare anonymous objects. Anonymous objects are here to replace the Java's anonymous inner classes. For instance, we can use them to setup click listeners in Android:
button.setOnClickListener(object : View.OnClickListener {
override fun onClick(view: View?) {
// Do some work here
}
})
As we see, the syntax is the same as the object declaration but we have dropped the object name!
NOTES: Object expressions in Kotlin:
- can implement multiple interfaces or no interfaces(Java can just extend one class or implement one interface).
- can access the variables in the function where they are created.
- are useful when we want to override multiple methods in our anonymous object. Otherwise, we are better to use lambda expressions.
- if we need the instance of it, we can assign the expression to a variable.
This is the end of part 3 which was the summary of chapter 4 (a long one:-) ). There were other parts in chapter 4 that I did not cover here because I felt they are not as important and they would have made the article too long. Anyhow, if you found this article useful, please like it and share it with other fellow developers. If you have any questions or suggestions, please feel free to comment. Thanks for your time.
Top comments (0)