1. Welcome
This codelab is part of the Kotlin Bootcamp for Programmers course. You'll get the most value out of this course if you work through the codelabs in sequence. Depending on your knowledge, you may be able to skim some sections. This course is geared towards programmers who know an object-oriented language, and want to learn Kotlin.
Introduction
In this codelab, you create a Kotlin program and learn about classes and objects in Kotlin. Much of this content will be familiar to you if you know another object-oriented language, but Kotlin has some important differences to reduce the amount of code you need to write. You also learn about abstract classes and interface delegation.
Rather than build a single sample app, the lessons in this course are designed to build your knowledge, but be semi-independent of each other so you can skim sections you're familiar with. To tie them together, many of the examples use an aquarium theme. And if you want to see the full aquarium story, check out the Kotlin Bootcamp for Programmers Udacity course.
What you should already know
- The basics of Kotlin, including types, operators, and looping
- Kotlin's function syntax
- The basics of object-oriented programming
- The basics of an IDE such as IntelliJ IDEA or Android Studio
What you'll learn
- How to create classes and access properties in Kotlin
- How to create and use class constructors in Kotlin
- How to create a subclass, and how inheritance works
- About abstract classes, interfaces, and interface delegation
- How to create and use data classes
- How to use singletons, enums, and sealed classes
What you'll do
- Create a class with properties
- Create a constructor for a class
- Create a subclass
- Examine examples of abstract classes and interfaces
- Create a simple data class
- Learn about singletons, enums, and sealed classes
2. Terminology
The following programming terms should already be familiar to you:
- Classes are blueprints for objects. For example, an
Aquarium
class is the blueprint for making an aquarium object. - Objects are instances of classes; an aquarium object is one actual
Aquarium
. - Properties are characteristics of classes, such as the length, width, and height of an
Aquarium
. - Methods, also called member functions, are the functionality of the class. Methods are what you can "do" with the object. For example, you can
fillWithWater()
anAquarium
object. - An interface is a specification that a class can implement. For example, cleaning is common to objects other than aquariums, and cleaning generally happens in similar ways for different objects. So you could have an interface called
Clean
that defines aclean()
method. TheAquarium
class could implement theClean
interface to clean the aquarium with a soft sponge. - Packages are a way to group related code to keep it organized, or to make a library of code. Once a package is created, you can import the package's contents into another file and reuse the code and classes in it.
3. Task: Create a class
In this task, you create a new package and a class with some properties and a method.
Step 1: Create a package
Packages can help you keep your code organized.
- In the Project pane, under the Hello Kotlin project, right-click on the src folder.
- Select New > Package and call it
example.myapp
.
Step 2: Create a class with properties
Classes are defined with the keyword class
, and class names by convention start with a capital letter.
- Right-click on the example.myapp package.
- Select New > Kotlin File / Class.
- Under Kind, select Class, and name the class
Aquarium
. IntelliJ IDEA includes the package name in the file and creates an emptyAquarium
class for you. - Inside the
Aquarium
class, define and initializevar
properties for the width, height, and length (in centimeters). Initialize the properties with default values.
package example.myapp
class Aquarium {
var width: Int = 20
var height: Int = 40
var length: Int = 100
}
Under the hood, Kotlin automatically creates getters and setters for the properties you defined in the Aquarium
class, so you can access the properties directly, for example, myAquarium.length
.
Step 3: Create a main() function
Create a new file called main.kt
to hold the main()
function.
- In the Project pane on the left, right-click on the example.myapp package.
- Select New > Kotlin File / Class.
- Under the Kind dropdown, keep the selection as File, and name the file
main.kt
. IntelliJ IDEA includes the package name, but doesn't include a class definition for a file. - Define a
buildAquarium()
function and inside create an instance ofAquarium
. To create an instance, reference the class as if it were a function,Aquarium()
. This calls the constructor of the class and creates an instance of theAquarium
class, similar to usingnew
in other languages. - Define a
main()
function and callbuildAquarium()
.
package example.myapp
fun buildAquarium() {
val myAquarium = Aquarium()
}
fun main() {
buildAquarium()
}
Step 4: Add a method
- In the
Aquarium
class, add a method to print the aquarium's dimension properties.
fun printSize() {
println("Width: $width cm " +
"Length: $length cm " +
"Height: $height cm ")
}
- In
main.kt
, inbuildAquarium()
, call theprintSize()
method onmyAquarium
.
fun buildAquarium() {
val myAquarium = Aquarium()
myAquarium.printSize()
}
- Run your program by clicking the green triangle next to the
main()
function. Observe the result.
⇒ Width: 20 cm Length: 100 cm Height: 40 cm
- In
buildAquarium()
, add code to set the height to 60 and print the changed dimension properties.
fun buildAquarium() {
val myAquarium = Aquarium()
myAquarium.printSize()
myAquarium.height = 60
myAquarium.printSize()
}
- Run your program and observe the output.
⇒ Width: 20 cm Length: 100 cm Height: 40 cm Width: 20 cm Length: 100 cm Height: 60 cm
4. Task: Add class constructors
In this task, you create a constructor for the class, and continue working with properties.
Step 1: Create a constructor
In this step, you add a constructor to the Aquarium
class you created in the first task. In the earlier example, every instance of Aquarium
is created with the same dimensions. You can change the dimensions once it is created by setting the properties, but it would be simpler to create it the correct size to begin with.
In some programming languages, the constructor is defined by creating a method within the class that has the same name as the class. In Kotlin, you define the constructor directly in the class declaration itself, specifying the parameters inside parentheses as if the class was a method. As with functions in Kotlin, those parameters can include default values.
- In the
Aquarium
class you created earlier, change the class definition to include three constructor parameters with default values forlength
,width
andheight
, and assign them to the corresponding properties.
class Aquarium(length: Int = 100, width: Int = 20, height: Int = 40) {
// Dimensions in cm
var length: Int = length
var width: Int = width
var height: Int = height
...
}
- The more compact Kotlin way is to define the properties directly with the constructor, using
var
orval
, and Kotlin also creates the getters and setters automatically. Then you can remove the property definitions in the body of the class.
class Aquarium(var length: Int = 100, var width: Int = 20, var height: Int = 40) {
...
}
- When you create an
Aquarium
object with that constructor, you can specify no arguments and get the default values, or specify just some of them, or specify all of them and create a completely custom-sizedAquarium
. In thebuildAquarium()
function, try out different ways of creating anAquarium
object using named parameters.
fun buildAquarium() {
val aquarium1 = Aquarium()
aquarium1.printSize()
// default height and length
val aquarium2 = Aquarium(width = 25)
aquarium2.printSize()
// default width
val aquarium3 = Aquarium(height = 35, length = 110)
aquarium3.printSize()
// everything custom
val aquarium4 = Aquarium(width = 25, height = 35, length = 110)
aquarium4.printSize()
}
- Run the program and observe the output.
⇒ Width: 20 cm Length: 100 cm Height: 40 cm Width: 25 cm Length: 100 cm Height: 40 cm Width: 20 cm Length: 110 cm Height: 35 cm Width: 25 cm Length: 110 cm Height: 35 cm
Notice that you didn't have to overload the constructor and write a different version for each of these cases (plus a few more for the other combinations). Kotlin creates what is needed from the default values and named parameters.
Step 2: Add init blocks
The example constructors above just declare properties and assign the value of an expression to them. If your constructor needs more initialization code, it can be placed in one or more init
blocks. In this step, you add some init
blocks to Aquarium
class.
- In the
Aquarium
class, add aninit
block to print that the object is initializing, and a second block to print the volume in liters.
class Aquarium (var length: Int = 100, var width: Int = 20, var height: Int = 40) {
init {
println("aquarium initializing")
}
init {
// 1 liter = 1000 cm^3
println("Volume: ${width * length * height / 1000} l")
}
}
- Run the program and observe the output.
aquarium initializing
Volume: 80 l
Width: 20 cm Length: 100 cm Height: 40 cm
aquarium initializing
Volume: 100 l
Width: 25 cm Length: 100 cm Height: 40 cm
aquarium initializing
Volume: 77 l
Width: 20 cm Length: 110 cm Height: 35 cm
aquarium initializing
Volume: 96 l
Width: 25 cm Length: 110 cm Height: 35 cm
Notice that the init
blocks are executed in the order in which they appear in the class definition, and all of them are executed when the constructor is called.
Step 3: Learn about secondary constructors
In this step, you learn about secondary constructors and add one to your class. In addition to a primary constructor, which can have one or more init
blocks, a Kotlin class can also have one or more secondary constructors to allow constructor overloading, that is, constructors with different arguments.
- In the
Aquarium
class, add a secondary constructor that takes a number of fish as its argument, using theconstructor
keyword. Create aval
tank property for the calculated volume of the aquarium in liters based on the number of fish. Assume 2 liters (2,000 cm^3) of water per fish, plus a little extra room so the water doesn't spill.
constructor(numberOfFish: Int) : this() {
// 2,000 cm^3 per fish + extra room so water doesn't spill
val tank = numberOfFish * 2000 * 1.1
}
- Inside the secondary constructor, keep the length and width (which were set in the primary constructor) the same, and calculate the height needed to make the tank the given volume.
// calculate the height needed
height = (tank / (length * width)).toInt()
- In the
buildAquarium()
function, add a call to create anAquarium
using your new secondary constructor. Print the size and volume.
fun buildAquarium() {
val aquarium6 = Aquarium(numberOfFish = 29)
aquarium6.printSize()
println("Volume: ${aquarium6.width * aquarium6.length * aquarium6.height / 1000} l")
}
- Run your program and observe the output.
⇒ aquarium initializing Volume: 80 l Width: 20 cm Length: 100 cm Height: 31 cm Volume: 62 l
Notice that the volume is printed twice, once by the init
block in the primary constructor before the secondary constructor is executed, and once by the code in buildAquarium()
.
You could have included the constructor
keyword in the primary constructor, too, but it's not necessary in most cases.
Step 4: Add a new property getter
In this step, you add an explicit property getter. Kotlin automatically defines getters and setters when you define properties, but sometimes the value for a property needs to be adjusted or calculated. For example, above, you printed the volume of the Aquarium
. You can make the volume available as a property by defining a variable and a getter for it. Because volume
needs to be calculated, the getter needs to return the calculated value, which you can do with a one-line function.
- In the
Aquarium
class, define anInt
property calledvolume
, and define aget()
method that calculates the volume in the next line.
val volume: Int
get() = width * height * length / 1000 // 1000 cm^3 = 1 l
- Remove the
init
block that prints the volume. - Remove the code in
buildAquarium()
that prints the volume. - In the
printSize()
method, add a line to print the volume.
fun printSize() {
println("Width: $width cm " +
"Length: $length cm " +
"Height: $height cm "
)
// 1 l = 1000 cm^3
println("Volume: $volume l")
}
- Run your program and observe the output.
⇒ aquarium initializing Width: 20 cm Length: 100 cm Height: 31 cm Volume: 62 l
The dimensions and volume are the same as before, but the volume is only printed once after the object is fully initialized by both the primary constructor and the secondary constructor.
Step 5: Add a property setter
In this step, you create a new property setter for the volume.
- In the
Aquarium
class, changevolume
to avar
so it can be set more than once. - Add a setter for the
volume
property by adding aset()
method below the getter, which recalculates the height based on the supplied amount of water. By convention, the name of the setter parameter isvalue
, but you can change it if you prefer.
var volume: Int
get() = width * height * length / 1000
set(value) {
height = (value * 1000) / (width * length)
}
- In
buildAquarium()
, add code to set the volume of the Aquarium to 70 liters. Print the new size.
fun buildAquarium() {
val aquarium6 = Aquarium(numberOfFish = 29)
aquarium6.printSize()
aquarium6.volume = 70
aquarium6.printSize()
}
- Run your program again and observe the changed height and volume.
⇒ aquarium initialized
Width: 20 cm Length: 100 cm Height: 31 cm
Volume: 62 l
Width: 20 cm Length: 100 cm Height: 35 cm
Volume: 70 l
5. Concept: Learn about visibility modifiers
There have been no visibility modifiers, such as public
or private
, in the code so far. That's because by default, everything in Kotlin is public, which means that everything can be accessed everywhere, including classes, methods, properties, and member variables.
In Kotlin, classes, objects, interfaces, constructors, functions, properties, and their setters can have visibility modifiers:
public
means visible outside the class. Everything is public by default, including variables and methods of the class.internal
means it will only be visible within that module. A module is a set of Kotlin files compiled together, for example, a library or application.private
means it will only be visible in that class (or source file if you are working with functions).protected
is the same asprivate
, but it will also be visible to any subclasses.
See Visibility Modifiers in the Kotlin documentation for more information.
Member variables
Properties within a class, or member variables, are public
by default. If you define them with var
, they are mutable, that is, readable and writable. If you define them with val
, they are read-only after initialization.
If you want a property that your code can read or write, but outside code can only read, you can leave the property and its getter as public and declare the setter private, as shown below.
var volume: Int
get() = width * height * length / 1000
private set(value) {
height = (value * 1000) / (width * length)
}
6. Task: Learn about subclasses and inheritance
In this task, you learn how subclasses and inheritance work in Kotlin. They are similar to what you've seen in other languages, but there are some differences.
In Kotlin, by default, classes cannot be subclassed. Similarly, properties and member variables cannot be overridden by subclasses (though they can be accessed).
You must mark a class as open
to allow it to be subclassed. Similarly, you must mark properties and member variables as open
, in order to override them in the subclass. The open
keyword is required, to prevent accidentally leaking implementation details as part of the class's interface.
Step 1: Make the Aquarium class open
In this step, you make the Aquarium
class open
, so that you can override it in the next step.
- Mark the
Aquarium
class and all its properties with theopen
keyword.
open class Aquarium (open var length: Int = 100, open var width: Int = 20, open var height: Int = 40) {
open var volume: Int
get() = width * height * length / 1000
set(value) {
height = (value * 1000) / (width * length)
}
- Add an open
shape
property with the value"rectangle"
.
open val shape = "rectangle"
- Add an open
water
property with a getter that returns 90% of the volume of theAquarium
.
open var water: Double = 0.0
get() = volume * 0.9
- Add code to the
printSize()
method to print the shape, and the amount of water as a percentage of the volume.
fun printSize() {
println(shape)
println("Width: $width cm " +
"Length: $length cm " +
"Height: $height cm ")
// 1 l = 1000 cm^3
println("Volume: $volume l Water: $water l (${water/volume*100.0}% full)")
}
- In
buildAquarium()
, change the code to create anAquarium
withwidth = 25
,length = 25
, andheight = 40
.
fun buildAquarium() {
val aquarium6 = Aquarium(length = 25, width = 25, height = 40)
aquarium6.printSize()
}
- Run your program and observe the new output.
⇒ aquarium initializing rectangle Width: 25 cm Length: 25 cm Height: 40 cm Volume: 25 l Water: 22.5 l (90.0% full)
Step 2: Create a subclass
- Create a subclass of
Aquarium
calledTowerTank
, which implements a rounded cylinder tank instead of a rectangular tank. You can addTowerTank
belowAquarium
, because you can add another class in the same file as theAquarium
class. - In
TowerTank
, override theheight
property, which is defined in the constructor. To override a property, use theoverride
keyword in the subclass.
- Make the constructor for
TowerTank
take adiameter
. Use thediameter
for bothlength
andwidth
when calling the constructor in theAquarium
superclass.
class TowerTank (override var height: Int, var diameter: Int): Aquarium(height = height, width = diameter, length = diameter) {
- Override the volume property to calculate a cylinder. The formula for a cylinder is pi times the radius squared times the height. You need to import the constant
PI
fromjava.lang.Math
.
override var volume: Int
// ellipse area = π * r1 * r2
get() = (width/2 * length/2 * height / 1000 * PI).toInt()
set(value) {
height = ((value * 1000 / PI) / (width/2 * length/2)).toInt()
}
- In
TowerTank
, override thewater
property to be 80% of the volume.
override var water = volume * 0.8
- Override the
shape
to be"cylinder"
.
override val shape = "cylinder"
- Your final
TowerTank
class should look something like the code below.
Aquarium.kt
:
package example.myapp
import java.lang.Math.PI
... // existing Aquarium class
class TowerTank (override var height: Int, var diameter: Int): Aquarium(height = height, width = diameter, length = diameter) {
override var volume: Int
// ellipse area = π * r1 * r2
get() = (width/2 * length/2 * height / 1000 * PI).toInt()
set(value) {
height = ((value * 1000 / PI) / (width/2 * length/2)).toInt()
}
override var water = volume * 0.8
override val shape = "cylinder"
}
- In
buildAquarium()
, create aTowerTank
with a diameter of 25 cm and a height of 45 cm. Print the size.
main.kt:
package example.myapp
fun buildAquarium() {
val myAquarium = Aquarium(width = 25, length = 25, height = 40)
myAquarium.printSize()
val myTower = TowerTank(diameter = 25, height = 40)
myTower.printSize()
}
- Run your program and observe the output.
⇒ aquarium initializing rectangle Width: 25 cm Length: 25 cm Height: 40 cm Volume: 25 l Water: 22.5 l (90.0% full) aquarium initializing cylinder Width: 25 cm Length: 25 cm Height: 40 cm Volume: 18 l Water: 14.4 l (80.0% full)
7. Task: Compare abstract classes and interfaces
Sometimes you want to define common behavior or properties to be shared among some related classes. Kotlin offers two ways to do that, interfaces and abstract classes. In this task, you create an abstract AquariumFish
class for properties that are common to all fish. You create an interface called FishAction
to define behavior common to all fish.
- Neither an abstract class nor an interface can be instantiated on its own, which means you cannot create objects of those types directly.
- Abstract classes have constructors.
- Interfaces can't have any constructor logic or store any state.
Step 1. Create an abstract class
- Under example.myapp, create a new file,
AquariumFish.kt
. - Create a class, also called
AquariumFish
, and mark it withabstract
. - Add one
String
property,color
, and mark it withabstract
.
package example.myapp
abstract class AquariumFish {
abstract val color: String
}
- Create two subclasses of
AquariumFish
,Shark
andPlecostomus
. - Because
color
is abstract, the subclasses must implement it. MakeShark
gray andPlecostomus
gold.
class Shark: AquariumFish() {
override val color = "gray"
}
class Plecostomus: AquariumFish() {
override val color = "gold"
}
- In main.kt, create a
makeFish()
function to test your classes. Instantiate aShark
and aPlecostomus
, then print the color of each. - Delete your earlier test code in
main()
and add a call tomakeFish()
. Your code should look something like the code below.
main.kt
:
package example.myapp
fun makeFish() {
val shark = Shark()
val pleco = Plecostomus()
println("Shark: ${shark.color}")
println("Plecostomus: ${pleco.color}")
}
fun main () {
makeFish()
}
- Run your program and observe the output.
⇒ Shark: gray Plecostomus: gold
The following diagram represents the Shark
class and Plecostomus
class, which subclass the abstract class, AquariumFish
.
Step 2. Create an interface
- In AquariumFish.kt, create an interface called
FishAction
with a methodeat()
.
interface FishAction {
fun eat()
}
- Add
FishAction
to each of the subclasses, and implementeat()
by having it print what the fish does.
class Shark: AquariumFish(), FishAction {
override val color = "gray"
override fun eat() {
println("hunt and eat fish")
}
}
class Plecostomus: AquariumFish(), FishAction {
override val color = "gold"
override fun eat() {
println("eat algae")
}
}
- In the
makeFish()
function, have each fish you created eat something by callingeat()
.
fun makeFish() {
val shark = Shark()
val pleco = Plecostomus()
println("Shark: ${shark.color}")
shark.eat()
println("Plecostomus: ${pleco.color}")
pleco.eat()
}
- Run your program and observe the output.
⇒ Shark: gray hunt and eat fish Plecostomus: gold eat algae
The following diagram represents the Shark
class and the Plecostomus
class, both of which are composed of and implement the FishAction
interface.
When to use abstract classes versus interfaces
The examples above are simple, but when you have a lot of interrelated classes, abstract classes and interfaces can help you keep your design cleaner, more organized, and easier to maintain.
As noted above, abstract classes can have constructors, and interfaces cannot, but otherwise they are very similar. So, when should you use each?
When you use interfaces to compose a class, the class's functionality is extended by way of the class instances that it contains. Composition tends to make code easier to reuse and reason about than inheritance from an abstract class. Also, you can use multiple interfaces in a class, but you can only subclass from one abstract class.
Composition often leads to better encapsulation, lower coupling (interdependence), cleaner interfaces, and more usable code. For these reasons, using composition with interfaces is the preferred design. On the other hand, inheritance from an abstract class tends to be a natural fit for some problems. So you should prefer composition, but when inheritance makes sense Kotlin lets you do that too!
- Use an interface if you have a lot of methods and one or two default implementations, for example as in
AquariumAction
below.
interface AquariumAction {
fun eat()
fun jump()
fun clean()
fun catchFish()
fun swim() {
println("swim")
}
}
- Use an abstract class any time you can't complete a class. For example, going back to the
AquariumFish
class, you can make allAquariumFish
implementFishAction
, and provide a default implementation foreat
while leavingcolor
abstract, because there isn't really a default color for fish.
interface FishAction {
fun eat()
}
abstract class AquariumFish: FishAction {
abstract val color: String
override fun eat() = println("yum")
}
8. Task: Use interface delegation
The previous task introduced abstract classes, interfaces, and the idea of composition. Interface delegation is an advanced technique where the methods of an interface are implemented by a helper (or delegate) object, which is then used by a class. This technique can be useful when you use an interface in a series of unrelated classes: you add the needed interface functionality to a separate helper class, and each of the classes uses an instance of the helper class to implement the functionality.
In this task, you use interface delegation to add functionality to a class.
Step 1: Make a new interface
- In AquariumFish.kt, remove the
AquariumFish
class. Instead of inheriting from theAquariumFish
class,Plecostomus
andShark
are going to implement interfaces for both the fish action and their color. - Create a new interface,
FishColor
, that defines the color as a string.
interface FishColor {
val color: String
}
- Change
Plecostomus
to implement two interfaces,FishAction
, and aFishColor
. You need to override thecolor
fromFishColor
andeat()
fromFishAction
.
class Plecostomus: FishAction, FishColor {
override val color = "gold"
override fun eat() {
println("eat algae")
}
}
- Change your
Shark
class to also implement the two interfaces,FishAction
andFishColor
, instead of inheriting fromAquariumFish
.
class Shark: FishAction, FishColor {
override val color = "gray"
override fun eat() {
println("hunt and eat fish")
}
}
- Your finished code should look something like this:
package example.myapp
interface FishAction {
fun eat()
}
interface FishColor {
val color: String
}
class Plecostomus: FishAction, FishColor {
override val color = "gold"
override fun eat() {
println("eat algae")
}
}
class Shark: FishAction, FishColor {
override val color = "gray"
override fun eat() {
println("hunt and eat fish")
}
}
Step 2: Make a singleton class
Next, you implement the setup for the delegation part by creating a helper class that implements FishColor
. You create a basic class called GoldColor
that implements FishColor
—all it does is say that its color is gold.
It doesn't make sense to make multiple instances of GoldColor
, because they'd all do exactly the same thing. So Kotlin lets you declare a class where you can only create one instance of it by using the keyword object
instead of class
. Kotlin will create that one instance, and that instance is referenced by the class name. Then all other objects can just use this one instance—there is no way to make other instances of this class. If you're familiar with the singleton pattern, this is how you implement singletons in Kotlin.
- In AquariumFish.kt, create an object for
GoldColor
. Override the color.
object GoldColor : FishColor {
override val color = "gold"
}
Step 3: Add interface delegation for FishColor
Now you're ready to use interface delegation.
- In AquariumFish.kt, remove the override of
color
fromPlecostomus
. - Change the
Plecostomus
class to get its color fromGoldColor
. You do this by addingby GoldColor
to the class declaration, creating the delegation. What this says is that instead of implementingFishColor
, use the implementation provided byGoldColor
. So every timecolor
is accessed, it is delegated toGoldColor
.
class Plecostomus: FishAction, FishColor by GoldColor {
override fun eat() {
println("eat algae")
}
}
With the class as is, all Plecos will be golden, but these fish actually come in many colors. You can address this by adding a constructor parameter for the color with GoldColor
as the default color for Plecostomus
.
- Change the
Plecostomus
class to take a passed infishColor
with its constructor, and set its default toGoldColor
. Change the delegation fromby GoldColor
toby fishColor
.
class Plecostomus(fishColor: FishColor = GoldColor): FishAction,
FishColor by fishColor {
override fun eat() {
println("eat algae")
}
}
Step 4: Add interface delegation for FishAction
In the same way, you can use interface delegation for the FishAction
.
- In AquariumFish.kt make a
PrintingFishAction
class that implementsFishAction
, which takes aString
,food
, then prints what the fish eats.
class PrintingFishAction(val food: String) : FishAction {
override fun eat() {
println(food)
}
}
- In
Plecostomus
class, remove the override functioneat()
, because you will replace it with a delegation. - In the declaration of
Plecostomus
, delegateFishAction
toPrintingFishAction
, passing"eat algae"
. - With all that delegation, there's no code in the body of the
Plecostomus
class, so remove the{}
, because all the overrides are handled by interface delegation
class Plecostomus (fishColor: FishColor = GoldColor):
FishAction by PrintingFishAction("eat algae"),
FishColor by fishColor
The following diagram represents the Shark
and the Plecostomus
classes, both composed of the PrintingFishAction
and FishColor
interfaces, but delegating the implementation to them.
Interface delegation is powerful, and you should generally consider how to use it whenever you might use an abstract class in another language. It lets you use composition to plug in behaviors, instead of requiring lots of subclasses, each specialized in a different way.
9. Task: Create a data class
A data class is similar to a struct
in some other languages—it exists mainly to hold some data—but a data class object is still an object. Kotlin data class objects have some extra benefits, such as utilities for printing and copying. In this task, you create a simple data class and learn about the support Kotlin provides for data classes.
Step 1: Create a data class
- Add a new package
decor
under the example.myapp package to hold the new code. Right-click on example.myapp in the Project pane and select File > New > Package. - In the package, create a new class called
Decoration
.
package example.myapp.decor
class Decoration {
}
- To make
Decoration
a data class, prefix the class declaration with the keyworddata
. - Add a
String
property calledrocks
to give the class some data.
data class Decoration(val rocks: String) {
}
- In the file, outside the class, add a
makeDecorations()
function to create and print an instance of aDecoration
with"granite"
.
fun makeDecorations() {
val decoration1 = Decoration("granite")
println(decoration1)
}
- Add a
main()
function to callmakeDecorations()
, and run your program. Notice the sensible output that is created because this is a data class.
⇒ Decoration(rocks=granite)
- In
makeDecorations()
, instantiate two moreDecoration
objects that are both "slate" and print them.
fun makeDecorations() {
val decoration1 = Decoration("granite")
println(decoration1)
val decoration2 = Decoration("slate")
println(decoration2)
val decoration3 = Decoration("slate")
println(decoration3)
}
- In
makeDecorations()
, add a print statement that prints the result of comparingdecoration1
withdecoration2
, and a second one comparingdecoration3
withdecoration2
. Use the equals() method that is provided by data classes.
println (decoration1.equals(decoration2))
println (decoration3.equals(decoration2))
- Run your code.
⇒ Decoration(rocks=granite) Decoration(rocks=slate) Decoration(rocks=slate) false true
Step 2. Use destructuring
To get at the properties of a data object and assign them to variables, you could assign them one at a time, like this.
val rock = decoration.rock
val wood = decoration.wood
val diver = decoration.diver
Instead, you can make variables, one for each property, and assign the data object to the group of variables. Kotlin puts the property value in each variable.
val (rock, wood, diver) = decoration
This is called destructuring and is a useful shorthand. The number of variables should match the number of properties, and the variables are assigned in the order in which they are declared in the class. Here is a complete example you can try in Decoration.kt.
// Here is a data class with 3 properties.
data class Decoration2(val rocks: String, val wood: String, val diver: String){
}
fun makeDecorations() {
val d5 = Decoration2("crystal", "wood", "diver")
println(d5)
// Assign all properties to variables.
val (rock, wood, diver) = d5
println(rock)
println(wood)
println(diver)
}
⇒ Decoration2(rocks=crystal, wood=wood, diver=diver) crystal wood diver
If you don't need one or more of the properties, you can skip them by using _
instead of a variable name, as shown in the code below.
val (rock, _, diver) = d5
10. Task: Learn about singletons, enums, and sealed classes
In this task, you learn about some of the special-purpose classes in Kotlin, including the following:
- Singleton classes
- Enums
- Sealed classes
Step 1: Recall singleton classes
Recall the earlier example with the GoldColor
class.
object GoldColor : FishColor {
override val color = "gold"
}
Because every instance of GoldColor
does the same thing, it is declared as an object
instead of as a class
to make it a singleton. There can be only one instance of it.
Step 2: Create an enum
Kotlin also supports enums, which allow you to enumerate something and refer to it by name, much like in other languages. Declare an enum by prefixing the declaration with the keyword enum
. A basic enum declaration only needs a list of names, but you can also define one or more fields associated with each name.
- In Decoration.kt, try out an example of an enum.
enum class Color(val rgb: Int) {
RED(0xFF0000), GREEN(0x00FF00), BLUE(0x0000FF);
}
Enums are a bit like singletons—there can be only one, and only one of each value in the enumeration. For example, there can only be one Color.RED
, one Color.GREEN
, and one Color.BLUE
. In this example, the RGB values are assigned to the rgb
property to represent the color components. You can also get the ordinal value of an enum using the ordinal
property, and its name using the name
property.
- Try out another example of an enum.
enum class Direction(val degrees: Int) {
NORTH(0), SOUTH(180), EAST(90), WEST(270)
}
fun main() {
println(Direction.EAST.name)
println(Direction.EAST.ordinal)
println(Direction.EAST.degrees)
}
⇒ EAST 2 90
Step 3: Create a sealed class
A sealed class is a class that can be subclassed, but only inside the file in which it's declared. If you try to subclass the class in a different file, you get an error.
Because the classes and subclasses are in the same file, Kotlin will know all the subclasses statically. That is, at compile time, the compiler sees all the classes and subclasses and knows that this is all of them, so the compiler can do extra checks for you.
- In AquariumFish.kt, try out an example of a sealed class, keeping with the aquatic theme.
sealed class Seal
class SeaLion : Seal()
class Walrus : Seal()
fun matchSeal(seal: Seal): String {
return when(seal) {
is Walrus -> "walrus"
is SeaLion -> "sea lion"
}
}
The Seal
class can't be subclassed in another file. If you want to add more Seal
types, you have to add them in the same file. This makes sealed classes a safe way to represent a fixed number of types. For example, sealed classes are great for returning success or error from a network API.
11. Summary
This lesson covered a lot of ground. While much of it should be familiar from other object-oriented programming languages, Kotlin adds some features to keep code concise and readable.
Classes and constructors
- Define a class in Kotlin using
class
. - Kotlin automatically creates setters and getters for properties.
- Define the primary constructor directly in the class definition. For example:
class Aquarium(var length: Int = 100, var width: Int = 20, var height: Int = 40)
- If a primary constructor needs additional code, write it in one or more
init
blocks. - A class can define one or more secondary constructors using
constructor
, but Kotlin style is to use a factory function instead.
Visibility modifiers and subclasses
- All classes and functions in Kotlin are
public
by default, but you can use modifiers to change the visibility tointernal
,private
, orprotected
. - To make a subclass, the parent class must be marked
open
. - To override methods and properties in a subclass, the methods and properties must be marked
open
in the parent class. - A sealed class can be subclassed only in the same file where it is defined. Make a sealed class by prefixing the declaration with
sealed
.
Data classes, singletons, and enums
- Make a data class by prefixing the declaration with
data
. - Destructuring is a shorthand for assigning the properties of a
data
object to separate variables. - Make a singleton class by using
object
instead ofclass
. - Define an enum using
enum class
.
Abstract classes, interfaces, and delegation
- Abstract classes and interfaces are two ways to share common behavior between classes.
- An abstract class defines properties and behavior, but leaves the implementation to subclasses.
- An interface defines behavior, and may provide default implementations for some or all of the behavior.
- When you use interfaces to compose a class, the class's functionality is extended by way of the class instances that it contains.
- Interface delegation uses composition, but also delegates the implementation to the interface classes.
- Composition is a powerful way to add functionality to a class using interface delegation. In general composition is preferred, but inheritance from an abstract class is a better fit for some problems.
12. Learn more
Kotlin documentation
If you want more information on any topic in this course, or if you get stuck, https://kotlinlang.org is your best starting point.
- Kotlin coding conventions
- Kotlin idioms
- Classes and inheritance
- Constructors
- Factory functions
- Properties and fields
- Visibility modifiers
- Abstract classes
- Interfaces
- Delegation
- Data classes
- Equality
- Destructuring
- Object declarations
- Enum classes
- Sealed classes
- Handling Optional Errors Using Kotlin Sealed Classes
Kotlin tutorials
The https://play.kotlinlang.org website includes rich tutorials called Kotlin Koans, a web-based interpreter, and a complete set of reference documentation with examples.
Udacity course
To view the Udacity course on this topic, see Kotlin Bootcamp for Programmers.
IntelliJ IDEA
Documentation for the IntelliJ IDEA can be found on the JetBrains website.
13. Homework
This section lists possible homework assignments for students who are working through this codelab as part of a course led by an instructor. It's up to the instructor to do the following:
- Assign homework if required.
- Communicate to students how to submit homework assignments.
- Grade the homework assignments.
Instructors can use these suggestions as little or as much as they want, and should feel free to assign any other homework they feel is appropriate.
If you're working through this codelab on your own, feel free to use these homework assignments to test your knowledge.
Answer these questions
Question 1
Classes have a special method that serves as a blueprint for creating objects of that class. What is the method called?
▢ A builder
▢ An instantiator
▢ A constructor
▢ A blueprint
Question 2
Which of the following statements about interfaces and abstract classes is NOT correct?
▢ Abstract classes can have constructors.
▢ Interfaces can't have constructors.
▢ Interfaces and abstract classes can be instantiated directly.
▢ Abstract properties must be implemented by subclasses of the abstract class.
Question 3
Which of the following is NOT a Kotlin visibility modifier for properties, methods, etc.?
▢ internal
▢ nosubclass
▢ protected
▢ private
Question 4
Consider this data class: data class Fish(val name: String, val species:String, val colors:String)
Which of the following is NOT valid code to create and destructure a Fish
object?
▢ val (name1, species1, colors1) = Fish("Pat", "Plecostomus", "gold")
▢ val (name2, _, colors2) = Fish("Bitey", "shark", "gray")
▢ val (name3, species3, _) = Fish("Amy", "angelfish", "blue and black stripes")
▢ val (name4, species4, colors4) = Fish("Harry", "halibut")
Question 5
Let's say you own a zoo with lots of animals that all need to be taken care of. Which of the following would NOT be part of implementing caretaking?
▢ An interface
for different types of foods animals eat.
▢ An abstract Caretaker
class from which you can create different types of caretakers.
▢ An interface
for giving clean water to an animal.
▢ A data
class for an entry in a feeding schedule.
14. Next codelab
For an overview of the course, including links to other codelabs, see "Kotlin Bootcamp for Programmers: Welcome to the course."