Kotlin is a modern programming language that compiles to Java bytecode. It is free and open source, and promises to make coding for Android even more fun.
In the previous article, you learned more about Kotlin properties such as late-initialization, extension, and inline properties. Not only that, you also learned about advanced classes such as data, enum, nested, and sealed classes in Kotlin.
In this post, you'll continue to learn about object-oriented programming in Kotlin by learning about abstract classes, interfaces, and inheritance. For a bonus, you'll also learn about type aliases.
1. Abstract Classes
Kotlin supports abstract classes—just like Java, these are classes which you never intend to create objects from. An abstract class is incomplete or useless without some concrete (non-abstract) subclasses, from which you can instantiate objects. A concrete subclass of an abstract class implements all the abstract methods and properties defined in the abstract class—otherwise that subclass is also an abstract class!
We create an abstract class with the abstract
modifier (similar to Java).
1 | abstractclassEmployee(valfirstName:String,vallastName:String){ |
2 | abstractfunearnings():Double |
3 | } |
Note that not all members have to be abstract. In other words, we can have a default method implementation in an abstract class.
1 | abstractclassEmployee(valfirstName:String,vallastName:String){ |
2 | // ... |
3 | |
4 | funfullName():String{ |
5 | returnlastName+" "+firstName; |
6 | } |
7 | } |
Here we created the non-abstract function fullName()
in an abstract class Employee
. Any function that you define in an abstract class is non-abstract by default. It becomes abstract when you specifically declare it as such.
Concrete classes (subclasses of the abstract class) can override a method in an abstract class in only two situations. You either need to declare the function abstract
if you aren't provding its implementation or you need to declare the function open
if you are providing an implementation.
We can also store state in abstract classes.
1 | abstractclassEmployee(valfirstName:String,vallastName:String){ |
2 | // ... |
3 | valpropFoo:String="bla bla" |
4 | } |
Even if the abstract class doesn't define any methods, we need to create a subclass before we can instantiate it, as in the example below.
1 | classProgrammer(firstName:String,lastName:String):Employee(firstName,lastName){ |
2 | |
3 | overridefunearnings():Double{ |
4 | // calculate earnings |
5 | } |
6 | } |
Our Programmer
class extends the Employee
abstract class. In Kotlin we use a single colon character (:
) instead of the Java extends
keyword to extend a class or implement an interface.
We can then create an object of type Programmer
and call methods on it — either in its own class or the superclass (base class).
1 | valprogrammer=Programmer("Chike","Mgbemena") |
2 | println(programmer.fullName())// "Mgbemena Chike" |
One thing that might surprise you is that we have the ability to override a val
(immutable) property with var
(mutable).
1 | openclassBaseA(openvalbaseProp:String){ |
2 | |
3 | } |
4 | |
5 | classDerivedA:BaseA(""){ |
6 | |
7 | privatevarderivedProp:String="" |
8 | |
9 | overridevarbaseProp:String |
10 | get()=derivedProp |
11 | set(value){ |
12 | derivedProp=value |
13 | } |
14 | } |
Make sure you use this functionality wisely! Be aware that we can't do the reverse—override a var
property with val
.
Earlier we saw how to override an abstract method of a class with a non-abstract method. You can also do the opposite and override a regular method with an abstract one inside an abstract class as shown below:
1 | openclassPerson(valfirstName:String,vallastName:String){ |
2 | openfunproperGreeting():String{ |
3 | return"Hi, $firstName $lastName." |
4 | } |
5 | } |
6 | |
7 | abstractclassEmployee(firstName:String,lastName:String,valcompany:String):Person(firstName,lastName){ |
8 | abstractoverridefunproperGreeting():String |
9 | } |
10 | |
11 | classProgrammer(firstName:String,lastName:String,company:String):Employee(firstName,lastName,company){ |
12 | overridefunproperGreeting():String{ |
13 | vargreeting="Hi programmer, $firstName $lastName from $company." |
14 | println(greeting) |
15 | |
16 | returngreeting |
17 | } |
18 | } |
19 | |
20 | valprogrammer=Programmer("Chike","Mgbemena","Envato") |
21 | programmer.properGreeting()// Hi programmer, Chike Mgbemena from Envato. |
Here, we created a class called Employee
which extends the Person
class. The properGreeting()
method in Person
is non-abstract but we override it to be abstract inside Employee
. As a result, any class that extends Employee
will have to provide its own implementation of properGreeting()
. This is what we did with the Programmer
class.
2. Interfaces
An interface is simply a collection of related methods that typically enable you to tell objects what to do and also how to do it by default. (Default methods in interfaces are a new feature added to Java 8.) In other words, an interface is a contract that implementing classes must abide by.
An interface is defined using the interface
keyword in Kotlin (similar to Java).
1 | classResult |
2 | classStudent |
3 | |
4 | interfaceStudentRepository{ |
5 | fungetById(id:Long):Student |
6 | fungetResultsById(id:Long):List<Result> |
7 | } |
In the code above, we've declared a StudentRepository
interface. This interface contains two abstract methods: getById()
and getResultsById()
. Note that including the abstract
keyword is redundant in an interface method because they are already abstract implicitly.
An interface is useless without one or more implementers—so let's create a class that will implement this interface.
1 | classStudentLocalDataSource:StudentRepository{ |
2 | overridefungetResults(id:Long):List<Result>{ |
3 | // do implementation |
4 | } |
5 | |
6 | overridefungetById(id:Long):Student{ |
7 | // do implementation |
8 | } |
9 | } |
Here we created a class StudentLocalDataSource
that implements the StudentRepository
interface.
We use the override
modifier to label the methods and properties we want to redefine from the interface or superclass—this is similar to the @Override
annotation in Java.
Note the following additional rules of interfaces in Kotlin:
- A class can implement as many interfaces as you want, but it can only extend a single class (similar to Java).
- The
override
modifier is compulsory in Kotlin—unlike in Java. - Along with methods, we can also declare properties in a Kotlin interface.
- A Kotlin interface method can have a default implementation (similar to Java 8).
Let's see an example of an interface method with a default implementation.
1 | interfaceStudentRepository{ |
2 | // ... |
3 | fundelete(student:Student){ |
4 | // do implementation |
5 | } |
6 | } |
In the preceding code, we added a new method delete()
with a default implementation (though I did not add the actual implementation code for demonstration purposes).
We also have the freedom to override the default implementation if we want.
1 | classStudentLocalDataSource:StudentRepository{ |
2 | // ... |
3 | overridefundelete(student:Student){ |
4 | // do implementation |
5 | } |
6 | } |
As stated, a Kotlin interface can have properties—but note that it can't maintain state. This means that the properties cannot store data. This is in contrast to abstract classes which can maintain state. So, the following interface definition with a property declaration will work.
1 | interfaceStudentRepository{ |
2 | valpropFoo:Boolean// will work |
3 | // ... |
4 | } |
But if we try to add some state to the interface by assigning a value to the property, it will not work.
1 | interfaceStudentRepository{ |
2 | valpropFoo:Boolean=true// Error: Property initializers are not allowed in interfaces |
3 | // .. |
4 | } |
However, an interface property in Kotlin can have getter and setter methods (though only the latter if the property is mutable). Note also that property in an interface cannot have a backing field.
1 | interfaceStudentRepository{ |
2 | varpropFoo:Boolean |
3 | get()=true |
4 | set(value){ |
5 | if(value){ |
6 | // do something |
7 | } |
8 | } |
9 | // ... |
10 | } |
We can also override an interface property if you want, so as to redefine it.
1 | classStudentLocalDataSource:StudentRepository{ |
2 | // ... |
3 | overridevarpropFoo:Boolean |
4 | get()=false |
5 | set(value){ |
6 | if(value){ |
7 | |
8 | } |
9 | } |
10 | } |
Let's look at a case where we have a class implementing multiple interfaces with the same method signature. How does the class decide which interface method to call?
1 | interfaceInterfaceA{ |
2 | funfunD(){} |
3 | } |
4 | |
5 | interfaceInterfaceB{ |
6 | funfunD(){} |
7 | } |
Here we have two interfaces that have a method with the same signature funD()
. Let's create a class that implements these two interfaces and overrides the funD()
method.
1 | classclassA:InterfaceA,InterfaceB{ |
2 | overridefunfunD(){ |
3 | super.funD()// Error: Many supertypes available, please specify the one you mean in angle brackets, e.g. 'super<Foo>' |
4 | } |
5 | } |
The compiler is confused about calling the super.funD()
method because the two interfaces that the class implements have the same method signature.
To solve this problem, we wrap the interface name for which we want to call the method in angle brackets <InterfaceName>
. (IntelliJ IDEA or Android Studio will give you a hint about solving this issue when it crops up.)
1 | classclassA:InterfaceA,InterfaceB{ |
2 | overridefunfunD(){ |
3 | super<InterfaceA>.funD() |
4 | } |
5 | } |
Here we are going to call the funD()
method of InterfaceA
. Problem solved!
One thing that you need to keep in mind when creating a class that derives from multiple interfaces is that you need to implement all the methods that you have inherited from the interfaces. On the other hand, when you create a class that inherits from a single interface, you only need to implement methods which don't have an implementation in the interface.
It is not just classes that can derive from an interface. You can also create an interface that derives from other interfaces. Here is an example:
1 | interfacePerson{ |
2 | valfirstName:String |
3 | vallastName:String |
4 | |
5 | fungoodMorning(){ |
6 | println("Good Morning, $firstName $lastName!") |
7 | } |
8 | } |
9 | |
10 | interfaceEmployee:Person{ |
11 | valcompany:String |
12 | |
13 | fungreetMe(){ |
14 | println("Hi, $firstName $lastName from $company.") |
15 | } |
16 | } |
17 | |
18 | classProgrammer(overridevalfirstName:String,overridevallastName:String,overridevalcompany:String):Employee{ |
19 | overridefungreetMe(){ |
20 | println("Hi programmer, $firstName $lastName from $company.") |
21 | } |
22 | } |
23 | |
24 | valprogrammer=Programmer("Chike","Mgebemena","Envato") |
25 | |
26 | programmer.goodMorning()// Good Morning, Chike Mgebemena! |
27 | programmer.greetMe()// Hi programmer, Chike Mgebemena from Envato. |
We begin by declaring an interface called Person
with two properties and one function. We use Person
to extend another interface called Employee
. This new interface defines another function and adds on more property to Emploee
. When we create a Programmer
class that derives from Employee
, we can directly call the function we implemented in Person
or call the greetMe()
function which we overrode.
You should note that we also need the keyword override
before all the properties that we declared inside our interfaces. This is necessary because otherwise, we are hiding the properties of the interfaces from which we derived our class.
3. Inheritance
You can create a new class (subclass) by acquiring an existing class's (superclass) members and perhaps redefining their default implementation. This mechanism is known as inheritance in object-oriented programming (OOP). One of the things that make Kotlin so awesome is that it encompasses both the OOP and functional programming paradigms—all in one language.
The base class for all classes in Kotlin is Any
.
1 | classPerson:Any{ |
2 | } |
The Any
type is equivalent to the Object
type we have in Java.
1 | publicopenclassAny{ |
2 | publicopenoperatorfunequals(other:Any?):Boolean |
3 | publicopenfunhashCode():Int |
4 | publicopenfuntoString():String |
5 | } |
The Any
type contains the following members: equals()
, hashcode()
, and also toString()
methods (similar to Java).
Our classes don't need to explicitly extend this type. If you don't explicitly specify which class a new class extends, the class extends Any
implicitly. For this reason, you typically don't need to include : Any
in your code—we do so in the code above for demonstration purposes.
Let's now look into creating classes in Kotlin with inheritance in mind.
1 | classStudent{ |
2 | |
3 | } |
4 | |
5 | classGraduateStudent:Student(){ |
6 | |
7 | } |
In the code above, the GraduateStudent
class extends the superclass Student
. But this code won't compile. Why? Because classes and methods are final
by default in Kotlin. In other words, they cannot be extended by default—unlike in Java where classes and methods are open by default.
Software engineering best practice recommends that you to begin making your classes and methods final
by default—i.e. if they aren't specifically intended to be redefined or overridden in subclasses. The Kotlin team (JetBrains) applied this coding philosophy and many more development best practices in developing this modern language.
For us to allow subclasses to be created from a superclass, we have to explicitly mark the superclass with the open
modifier. This modifier also applies to any superclass property or method that should be overridden by subclasses.
1 | openclassStudent{ |
2 | |
3 | } |
We simply put the open
modifier before the class
keyword. We have now instructed the compiler to allow our Student
class to be open for extension.
As stated earlier, members of a Kotlin class are also final by default.
1 | openclassStudent{ |
2 | |
3 | openfunschoolFees():BigDecimal{ |
4 | // do implementation |
5 | } |
6 | } |
In the preceding code, we marked the schoolFees
function as open
—so that subclasses can override it.
1 | openclassGraduateStudent:Student(){ |
2 | |
3 | overridefunschoolFees():BigDecimal{ |
4 | returnsuper.schoolFees()+calculateSchoolFees() |
5 | } |
6 | |
7 | privatefuncalculateSchoolFees():BigDecimal{ |
8 | // calculate and return school fees |
9 | } |
10 | } |
Here, the open schoolFees
function from the superclass Student
is overridden by the GraduateStudent
class—by adding the override
modifier before the fun
keyword. Note that if you override a member of a superclass or interface, the overriding member will also be open
by default, as in the example below:
1 | classComputerScienceStudent:GraduateStudent(){ |
2 | |
3 | overridefunschoolFees():BigDecimal{ |
4 | returnsuper.schoolFees()+calculateSchoolFess() |
5 | } |
6 | |
7 | privatefuncalculateSchoolFess():BigDecimal{ |
8 | // calculate and return school fees |
9 | } |
10 | } |
Even though we didn't mark the schoolFees()
method in the GraduateStudent
class with the open
modifier, we can still override it—as we did in the ComputerScienceStudent
class. For us to prevent this, we have to mark the overriding member as final
.
Remember that we can add new functionality to a class—even if it's final—by the use of extension functions in Kotlin. For a refresher on extension functions, check out my Advanced Functions in Kotlin post. Also, if you need a refresher on how to give even a final class new properties without inheriting from it, read the section on extension Properties in my Advanced Properties and Classes post.
If our superclass has a primary constructor like this:
1 | openclassStudent(valfirstName:String,vallastName:String){ |
2 | // ... |
3 | } |
Then any subclass has to call the primary constructor of the superclass.
1 | openclassGraduateStudent(firstName:String,lastName:String):Student(firstName,lastName){ |
2 | // ... |
3 | } |
We can simply create an object of the GraduateStudent
class as usual:
1 | valgraduateStudent=GraduateStudent("Jon","Snow") |
2 | println(graduateStudent.firstName)// Jon |
If the subclass wants to call the superclass constructor from its secondary constructor, we use the super
keyword (similar to how superclass constructors are invoked in Java).
1 | openclassGraduateStudent:Student{ |
2 | // ... |
3 | privatevarthesis:String="" |
4 | |
5 | constructor(firstName:String,lastName:String,thesis:String):super(firstName,lastName){ |
6 | this.thesis=thesis |
7 | } |
8 | } |
If you need a refresher on class constructors in Kotlin, kindly visit my Classes and Objects post.
Let's say you have a class in Kotlin that inherits multiple implementations of the same method from its immediate superclass or interface. In this case, you will have to override the implemented method and provide your own implementation. This provides clarity to Kotlin regarding what you want to do inside the inherited method.
4. Bonus: Type Alias
Another awesome thing we can do in Kotlin is to give a type an alias.
Let's see an example.
1 | dataclassPerson(valfirstName:String,vallastName:String,valage:Int) |
In the class above, we can assign the String
and Int
types for the Person
properties aliases using the typealias
modifier in Kotlin. This modifier is used to create an alias of any type in Kotlin—including the ones you have created.
1 | typealiasName=String |
2 | typealiasAge=Int |
3 | |
4 | dataclassPerson(valfirstName:Name,vallastName:Name,valage:Age) |
As you can see, we have created an alias Name
and Age
for both the String
and Int
types respectively. We have now replaced the firstName
and lastName
property type to our alias Name
—and also Int
type to Age
alias. Note that we didn't create any new types—we instead created an alias for the types.
These can be handy when you want to provide a better meaning or semantic to types in your Kotlin codebase. So use them wisely!
One advantage of using type aliases is that they allow you to provide a shorter alternate name to refer to a very long type name. This can improve the readability of your code and ultimately reduce chances of errors.
Conclusion
In this tutorial, you learned more about object-oriented programming in Kotlin. We covered the following:
- abstract classes
- interfaces
- inheritance
- type alias
If you have been learning Kotlin through our Kotlin From Scratch series, make sure you have been typing the code you see and running it on your IDE. One great tip to really grasp a new programming language (or any programming concept) you're learning is to make sure you don't just only read the learning resource or guide, but also type the actual code and run it!
In the next tutorial in the Kotlin From Scratch series, you'll be introduced to exception handling in Kotlin. See you soon!
To learn more about the Kotlin language, I recommend visiting the Kotlin documentation. Or check out some of our other Android app development posts here on Envato Tuts!