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 are introduced to generic classes, functions, and methods, and how they work in Kotlin.
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 syntax of Kotlin functions, classes, and methods
- How to create a new class in the IntelliJ IDEA and run a program
What you'll learn
- How to work with generic classes, methods, and functions
What you'll do
- Create a generic class and add constraints
- Create
in
andout
types - Create generic functions, methods, and extension functions
2. Task: Explore generic classes
Introduction to generics
Kotlin, like many programming languages, has generic types. A generic type allows you to make a class generic, and thereby make a class much more flexible.
Imagine you were implementing a MyList
class that holds a list of items. Without generics, you would need to implement a new version of MyList
for each type: one for Double
, one for String
, one for Fish
. With generics, you can make the list generic, so it can hold any type of object. It's like making the type a wildcard that will fit many types.
To define a generic type, put T in angle brackets <T>
after the class name. (You could use another letter or a longer name, but the convention for a generic type is T.)
class MyList<T> {
fun get(pos: Int): T {
TODO("implement")
}
fun addItem(item: T) {}
}
You can reference T
as if it were a normal type. The return type for get()
is T
, and the parameter to addItem()
is of type T
. Of course, generic lists are very useful, so the List
class is built into Kotlin.
Step 1: Make a type hierarchy
In this step you create some classes to use in the next step. Subclassing was covered in an earlier codelab, but here is a brief review.
- To keep the example uncluttered, create a new package under src and call it
generics
. - In the generics package, create a new
Aquarium.kt
file. This allows you to redefine things using the same names without conflicts, so the rest of your code for this codelab goes into this file. - Make a type hierarchy of water supply types. Start by making
WaterSupply
anopen
class, so it can be subclassed. - Add a boolean
var
parameter,needsProcessing
. This automatically creates a mutable property, along with a getter and setter. - Make a subclass
TapWater
that extendsWaterSupply
, and passtrue
forneedsProcessing
, because the tap water contains additives which are bad for fish. - In
TapWater
, define a function calledaddChemicalCleaners()
that setsneedsProcessing
tofalse
after cleaning the water. TheneedsProcessing
property can be set fromTapWater
, because it ispublic
by default and accessible to subclasses. Here is the completed code.
package generics
open class WaterSupply(var needsProcessing: Boolean)
class TapWater : WaterSupply(true) {
fun addChemicalCleaners() {
needsProcessing = false
}
}
- Create two more subclasses of
WaterSupply
, calledFishStoreWater
andLakeWater
.FishStoreWater
doesn't need processing, butLakeWater
must be filtered with thefilter()
method. After filtering, it does not need to be processed again, so infilter()
, setneedsProcessing = false
.
class FishStoreWater : WaterSupply(false)
class LakeWater : WaterSupply(true) {
fun filter() {
needsProcessing = false
}
}
If you need additional information, review the earlier lesson on inheritance in Kotlin.
Step 2: Make a generic class
In this step you modify the Aquarium
class to support different types of water supplies.
- In Aquarium.kt, define an
Aquarium
class, with<T>
in brackets after the class name. - Add an immutable property
waterSupply
of typeT
toAquarium
.
class Aquarium<T>(val waterSupply: T)
- Write a function called
genericsExample()
. This isn't part of a class, so it can go at the top level of the file, like themain()
function or the class definitions. In the function, make anAquarium
and pass it aWaterSupply
. Since thewaterSupply
parameter is generic, you must specify the type in angle brackets<>
.
fun genericsExample() {
val aquarium = Aquarium<TapWater>(TapWater())
}
- In
genericsExample()
your code can access the aquarium'swaterSupply
. Because it is of typeTapWater
, you can calladdChemicalCleaners()
without any type casts.
fun genericsExample() {
val aquarium = Aquarium<TapWater>(TapWater())
aquarium.waterSupply.addChemicalCleaners()
}
- When creating the
Aquarium
object, you can remove the angle brackets and what's between them because Kotlin has type inference. So there's no reason to sayTapWater
twice when you create the instance. The type can be inferred by the argument toAquarium
; it will still make anAquarium
of typeTapWater
.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
aquarium.waterSupply.addChemicalCleaners()
}
- To see what is happening, print
needsProcessing
before and after callingaddChemicalCleaners()
. Below is the completed function.
fun genericsExample() {
val aquarium = Aquarium<TapWater>(TapWater())
println("water needs processing: ${aquarium.waterSupply.needsProcessing}")
aquarium.waterSupply.addChemicalCleaners()
println("water needs processing: ${aquarium.waterSupply.needsProcessing}")
}
- Add a
main()
function to callgenericsExample()
, then run your program and observe the result.
fun main() {
genericsExample()
}
⇒ water needs processing: true water needs processing: false
Step 3: Make it more specific
Generic means you can pass almost anything, and sometimes that's a problem. In this step you make the Aquarium
class more specific about what you can put in it.
- In
genericsExample()
, create anAquarium
, passing a string for thewaterSupply
, then print the aquarium'swaterSupply
property.
fun genericsExample() {
val aquarium2 = Aquarium("string")
println(aquarium2.waterSupply)
}
- Run your program observe the result.
⇒ string
The result is the string you passed, because Aquarium
doesn't put any limitations on T.
Any type, including String
, can be passed in.
- In
genericsExample()
, create anotherAquarium
, passingnull
for thewaterSupply
. If thewaterSupply
is null, print"waterSupply is null"
.
fun genericsExample() {
val aquarium3 = Aquarium(null)
if (aquarium3.waterSupply == null) {
println("waterSupply is null")
}
}
- Run your program and observe the result.
⇒ waterSupply is null
Why can you pass null
when creating an Aquarium
? This is possible because by default, T
stands for the nullable Any?
type, the type at the top of the type hierarchy. The following is equivalent to what you typed earlier.
class Aquarium<T: Any?>(val waterSupply: T)
- To not allow passing
null
, makeT
of typeAny
explicitly, by removing the?
afterAny
.
class Aquarium<T: Any>(val waterSupply: T)
In this context, Any
is called a generic constraint. It means any type can be passed for T
as long as it isn't null
.
- What you really want is to make sure that only a
WaterSupply
(or one of its subclasses) can be passed forT
. ReplaceAny
withWaterSupply
to define a more specific generic constraint.
class Aquarium<T: WaterSupply>(val waterSupply: T)
Step 4: Add more checking
In this step you learn about the check()
function to help ensure your code is behaving as expected. The check()
function is a standard library function in Kotlin. It acts as an assertion and will throw an IllegalStateException
if its argument evaluates to false
.
- Add an
addWater()
method toAquarium
class to add water, with acheck()
that makes sure you don't need to process the water first.
class Aquarium<T: WaterSupply>(val waterSupply: T) {
fun addWater() {
check(!waterSupply.needsProcessing) { "water supply needs processing first" }
println("adding water from $waterSupply")
}
}
In this case, if needsProcessing
is true, check()
will throw an exception.
- In
genericsExample()
, add code to make anAquarium
withLakeWater
, and then add some water to it.
fun genericsExample() {
val aquarium4 = Aquarium(LakeWater())
aquarium4.addWater()
}
- Run your program, and you will get an exception, because the water needs to be filtered first.
⇒ Exception in thread "main" java.lang.IllegalStateException: water supply needs processing first at Aquarium.generics.Aquarium.addWater(Aquarium.kt:21)
- Add a call to filter the water before adding it to the
Aquarium
. Now when you run your program, there is no exception thrown.
fun genericsExample() {
val aquarium4 = Aquarium(LakeWater())
aquarium4.waterSupply.filter()
aquarium4.addWater()
}
⇒ adding water from generics.LakeWater@880ec60
The above covers the basics of generics. The following tasks cover more, but the important concept is how to declare and use a generic class with a generic constraint.
3. Task: Learn about in and out types
In this task, you learn about in and out types with generics. An in
type is a type that can only be passed into a class, not returned. An out
type is a type that can only be returned from a class.
Look at the Aquarium
class and you'll see that the generic type is only ever returned when getting the property waterSupply
. There aren't any methods that take a value of type T
as a parameter (except for defining it in the constructor). Kotlin lets you define out
types for exactly this case, and it can infer extra information about where the types are safe to use. Similarly, you can define in
types for generic types that are only ever passed into methods, not returned. This allows Kotlin to do extra checks for code safety.
The in
and out
types are directives for Kotlin's type system. Explaining the whole type system is outside the scope of this bootcamp (it's pretty involved); however, the compiler will flag types that are not marked in
and out
appropriately, so you need to know about them.
Step 1: Define an out type
- In the
Aquarium
class, changeT: WaterSupply
to be anout
type.
class Aquarium<out T: WaterSupply>(val waterSupply: T) {
...
}
- In the same file, outside the class, declare a function
addItemTo()
that expects anAquarium
ofWaterSupply
.
fun addItemTo(aquarium: Aquarium<WaterSupply>) = println("item added")
- Call
addItemTo()
fromgenericsExample()
and run your program.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
addItemTo(aquarium)
}
⇒ item added
Kotlin can ensure that addItemTo()
won't do anything type unsafe with the generic WaterSupply
, because it's declared as an out
type.
- If you remove the
out
keyword, the compiler will give an error when callingaddItemTo()
, because Kotlin can't ensure that you are not doing anything unsafe with the type.
Step 2: Define an in type
The in
type is similar to the out
type, but for generic types that are only ever passed into functions, not returned. If you try to return an in
type, you'll get a compiler error. In this example you'll define an in
type as part of an interface.
- In Aquarium.kt, define an interface
Cleaner
that takes a genericT
that's constrained toWaterSupply
. Since it is only used as an argument toclean()
, you can make it anin
parameter.
interface Cleaner<in T: WaterSupply> {
fun clean(waterSupply: T)
}
- To use the
Cleaner
interface, create a classTapWaterCleaner
that implementsCleaner
for cleaningTapWater
by adding chemicals.
class TapWaterCleaner : Cleaner<TapWater> {
override fun clean(waterSupply: TapWater) = waterSupply.addChemicalCleaners()
}
- In the
Aquarium
class, updateaddWater()
to take aCleaner
of typeT
, and clean the water before adding it.
class Aquarium<out T: WaterSupply>(val waterSupply: T) {
fun addWater(cleaner: Cleaner<T>) {
if (waterSupply.needsProcessing) {
cleaner.clean(waterSupply)
}
println("water added")
}
}
- Update the
genericsExample()
example code to make aTapWaterCleaner
, anAquarium
withTapWater
, and then add some water using the cleaner. It will use the cleaner as needed.
fun genericsExample() {
val cleaner = TapWaterCleaner()
val aquarium = Aquarium(TapWater())
aquarium.addWater(cleaner)
}
Kotlin will use the in
and out
type information to make sure your code uses the generics safely. Out
and in
are easy to remember: out
types can be passed outward as return values, in
types can be passed inward as arguments.
If you want to dig in more to the sort of problems in types and out types solve, the documentation covers them in depth.
4. Task: Find out about generic functions
In this task you will learn about generic functions and when to use them. Typically, making a generic function is a good idea whenever the function takes an argument of a class that has a generic type.
Step 1: Make a generic function
- In generics/Aquarium.kt, make a function
isWaterClean()
which takes anAquarium
. You need to specify the generic type of the parameter; one option is to useWaterSupply
.
fun isWaterClean(aquarium: Aquarium<WaterSupply>) {
println("aquarium water is clean: ${!aquarium.waterSupply.needsProcessing}")
}
But this means Aquarium
must have an out
type parameter for this to be called. Sometimes out
or in
is too restrictive because you need to use a type for both input and output. You can remove the out
requirement by making the function generic.
- To make the function generic, put angle brackets after the keyword
fun
with a generic typeT
and any constraints, in this case,WaterSupply
. ChangeAquarium
to be constrained byT
instead of byWaterSupply
.
fun <T: WaterSupply> isWaterClean(aquarium: Aquarium<T>) {
println("aquarium water is clean: ${!aquarium.waterSupply.needsProcessing}")
}
T
is a type parameter to isWaterClean()
that is being used to specify the generic type of the aquarium. This pattern is really common, and it's a good idea to take a moment to work through this.
- Call the
isWaterClean()
function by specifying the type in angle brackets right after the function name and before the parentheses.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
isWaterClean<TapWater>(aquarium)
}
- Because of type inference from the argument
aquarium
, the type isn't needed, so remove it. Run your program and observe the output.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
isWaterClean(aquarium)
}
⇒ aquarium water is clean: false
Step 2: Make a generic method with a reified type
You can use generic functions for methods too, even in classes that have their own generic type. In this step, you add a generic method to Aquarium
that checks if it has a type of WaterSupply
.
- In
Aquarium
class, declare a method,hasWaterSupplyOfType()
that takes a generic parameterR
(T
is already used) constrained toWaterSupply
, and returnstrue
ifwaterSupply
is of typeR
. This is like the function you declared earlier, but inside theAquarium
class.
fun <R: WaterSupply> hasWaterSupplyOfType() = waterSupply is R
- Notice that the final
R
is underlined in red. Hold the pointer over it to see what the error is. - To do an
is
check, you need to tell Kotlin that the type is reified, or real, and can be used in the function. To do that, putinline
in front of thefun
keyword, andreified
in in front of the generic typeR
.
inline fun <reified R: WaterSupply> hasWaterSupplyOfType() = waterSupply is R
Once a type is reified, you can use it like a normal type—because it is a real type after inlining. That means you can do is
checks using the type.
If you don't use reified
here, the type won't be "real" enough for Kotlin to allow is
checks. That's because non-reified types are only available at compile time, and can't be used at runtime by your program. This is discussed more in the next section.
- Pass
TapWater
as the type. Like calling generic functions, call generic methods by using angle brackets with the type after the function name. Run your program and observe the result.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
println(aquarium.hasWaterSupplyOfType<TapWater>()) // true
}
⇒ true
Step 3: Make extension functions
You can use reified types for regular functions and extension functions, too.
- Outside the
Aquarium
class, define an extension function onWaterSupply
calledisOfType()
that checks if the passedWaterSupply
is of a specific type, for example,TapWater
.
inline fun <reified T: WaterSupply> WaterSupply.isOfType() = this is T
- Call the extension function just like a method.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
println(aquarium.waterSupply.isOfType<TapWater>())
}
⇒ true
With these extension functions, it doesn't matter what type of Aquarium
it is (Aquarium
or TowerTank
or some other subclass), as long as it is an Aquarium
. Using the star-projection syntax is a convenient way to specify a variety of matches. And when you use a star-projection, Kotlin will make sure you don't do anything unsafe, too.
- To use a star-projection, put
<*>
afterAquarium
. MovehasWaterSupplyOfType()
to be an extension function, because it isn't really part of the core API ofAquarium
.
inline fun <reified R: WaterSupply> Aquarium<*>.hasWaterSupplyOfType() = waterSupply is R
- Change the call to
hasWaterSupplyOfType()
and run your program.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
println(aquarium.hasWaterSupplyOfType<TapWater>())
}
⇒ true
5. Concept: Reified types and type erasure
In the earlier example, you had to mark the generic type as reified
and make the function inline
, because Kotlin needs to know about them at runtime, not just compile time.
All generic types are only used at compile time by Kotlin. This lets the compiler make sure that you're doing everything safely. By runtime all the generic types are erased, hence the earlier error message about checking an erased type.
It turns out the compiler can create correct code without keeping the generic types until runtime. But it does mean that sometimes you do something, like is
checks on generic types, that the compiler can't support. That's why Kotlin added reified, or real, types.
You can read more about reified types and type erasure in the Kotlin documentation.
6. Summary
This lesson focused on generics, which are important for making code more flexible and easier to reuse.
- Create generic classes to make code more flexible.
- Add generic constraints to limit the types used with generics.
- Use
in
andout
types with generics to provide better type checking to restrict types being passed into or returned from classes. - Create generic functions and methods to work with generic types. For example:
fun <T: WaterSupply> isWaterClean(aquarium: Aquarium<T>) { ... }
- Use generic extension functions to add non-core functionality to a class.
- Reified types are sometimes necessary because of type erasure. Reified types, unlike generic types, persist to runtime.
- Use the
check()
function to verify your code is running as expected. For example:check(!waterSupply.needsProcessing) { "water supply needs processing first" }
7. 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
- Generics
- Generic constraints
- Star-projections
In
andout
types- Reified parameters
- Type erasure
check()
function
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.
8. 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
Which of the following is the convention for naming a generic type?
▢ <Gen>
▢ <Generic>
▢ <T>
▢ <X>
Question 2
A restriction on the types allowed for a generic type is called:
▢ a generic restriction
▢ a generic constraint
▢ disambiguation
▢ a generic type limit
Question 3
Reified means:
▢ The real execution impact of an object has been calculated.
▢ A restricted entry index has been set on the class.
▢ The generic type parameter has been made into a real type.
▢ A remote error indicator has been triggered.
9. Next codelab
For an overview of the course, including links to other codelabs, see "Kotlin Bootcamp for Programmers: Welcome to the course."