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
This is the final codelab in the Kotlin Bootcamp. In this codelab you learn about annotations and labeled breaks. You review lambdas and higher- order functions, which are key parts of Kotlin. You also learn more about inlining functions, and Single Abstract Method (SAM) interfaces. Finally, you learn more about the Kotlin Standard Library.
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
- The basics of lambdas and higher-order functions
What you'll learn
- The basics of annotations
- How to use labeled breaks
- More about higher-order functions
- About Single Abstract Method (SAM) interfaces
- About the Kotlin Standard Library
What you'll do
- Create a simple annotation.
- Use a labeled break.
- Review lambda functions in Kotlin.
- Use and create higher-order functions.
- Call some Single Abstract Method interfaces.
- Use some functions from the Kotlin Standard Library.
2. Task: Learn about annotations
Annotations are a way of attaching metadata to code, and are not something specific to Kotlin. The annotations are read by the compiler and used to generate code or logic. Many frameworks, such as Ktor and Kotlinx, as well as Room, use annotations to configure how they run and interact with your code. You are unlikely to encounter any annotations until you start using frameworks, but it's useful to be able to know how to read an annotation.
There are also annotations that are available through the Kotlin standard library that control the way code is compiled. They're really useful if you're exporting Kotlin to Java code, but otherwise you don't need them that often.
Annotations go right before the thing that is annotated, and most things can be annotated—classes, functions, methods, and even control structures. Some annotations can take arguments.
Here is an example of some annotations.
@file:JvmName("InteropFish")
class InteropFish {
companion object {
@JvmStatic fun interop()
}
}
This says the exported name of this file is InteropFish
with the JvmName
annotation; the JvmName
annotation is taking an argument of "InteropFish"
. In the companion object, @JvmStatic
tells Kotlin to make interop()
a static function in InteropFish
.
You can also create your own annotations, but this is mostly useful if you are writing a library that needs particular information about classes at runtime, that is reflection.
Step 1: Create a new package and file
- Under src, create a new package,
example
. - In example, create a new Kotlin file,
Annotations.kt
.
Step 2: Create your own annotation
- In
Annotations.kt
, create aPlant
class with two methods,trim()
andfertilize()
.
class Plant {
fun trim(){}
fun fertilize(){}
}
- Create a function that prints all the methods in a class. Use
::class
to get information about a class at runtime. UsedeclaredMemberFunctions
to get a list of the methods of a class. (To access this, you need to importkotlin.reflect.full.*
)
import kotlin.reflect.full.* // required import
class Plant {
fun trim(){}
fun fertilize(){}
}
fun testAnnotations() {
val classObj = Plant::class
for (m in classObj.declaredMemberFunctions) {
println(m.name)
}
}
- Create a
main()
function to call your test routine. Run your program and observe the output.
fun main() {
testAnnotations()
}
⇒ trim fertilize
- Create a simple annotation,
ImAPlant
.
annotation class ImAPlant
This doesn't do anything other than say it is annotated.
- Add the annotation in front of your
Plant
class.
@ImAPlant class Plant{
...
}
- Change
testAnnotations()
to print all the annotations of a class. Useannotations
to get all the annotations of a class. Run your program and observe the result.
fun testAnnotations() {
val plantObject = Plant::class
for (a in plantObject.annotations) {
println(a.annotationClass.simpleName)
}
}
⇒ ImAPlant
- Change
testAnnotations()
to find theImAPlant
annotation. UsefindAnnotation()
to find a specific annotation. Run your program and observe the result.
fun testAnnotations() {
val plantObject = Plant::class
val myAnnotationObject = plantObject.findAnnotation<ImAPlant>()
println(myAnnotationObject)
}
⇒ @example.ImAPlant()
Step 3: Create a targeted annotation
Annotations can target getters or setters. When they do, you can apply them with the @get:
or @set:
prefix. This comes up a lot when using frameworks with annotations.
- Declare two annotations,
OnGet
which can only be applied to property getters, andOnSet
which can only be applied to property setters. Use@Target(AnnotationTarget.PROPERTY_GETTER)
orPROPERTY_SETTER
on each.
annotation class ImAPlant
@Target(AnnotationTarget.PROPERTY_GETTER)
annotation class OnGet
@Target(AnnotationTarget.PROPERTY_SETTER)
annotation class OnSet
@ImAPlant class Plant {
@get:OnGet
val isGrowing: Boolean = true
@set:OnSet
var needsFood: Boolean = false
}
Annotations are really powerful for creating libraries that inspect things both at runtime and sometimes at compile time. However, typical application code just uses annotations provided by frameworks.
3. Task: Learn about labeled breaks
Kotlin has several ways of controlling flow. You are already familiar with return
, which returns from a function to its enclosing function. Using a break
is like return
, but for loops.
Kotlin gives you additional control over loops with what's called a labeled break. A break
qualified with a label jumps to the execution point right after the loop marked with that label. This is particularly useful when dealing with nested loops.
Any expression in Kotlin may be marked with a label. Labels have the form of an identifier followed by the @
sign.
- In
Annotations.kt
, try out a labeled break by breaking out from an inner loop.
fun labels() {
outerLoop@ for (i in 1..100) {
print("$i ")
for (j in 1..100) {
if (i > 10) break@outerLoop // breaks to outer loop
}
}
}
fun main() {
labels()
}
- Run your program and observe the output.
⇒ 1 2 3 4 5 6 7 8 9 10 11
Similarly, you can use a labeled continue
. Instead of breaking out of the labeled loop, the labeled continue proceeds to the next iteration of the loop.
4. Task: Create simple lambdas
Lambdas are anonymous functions, which are functions with no name. You can assign them to variables and pass them as arguments to functions and methods. They are extremely useful.
Step 1: Create a simple lambda
- Start the REPL in IntelliJ IDEA, Tools > Kotlin > Kotlin REPL.
- Create a lambda with an argument,
dirty: Int
that does a calculation, dividingdirty
by 2. Assign the lambda to a variable,waterFilter
.
val waterFilter = { dirty: Int -> dirty / 2 }
- Call
waterFilter
, passing a value of 30.
waterFilter(30)
⇒ res0: kotlin.Int = 15
Step 2: Create a filter lambda
- Still in the REPL, create a data class,
Fish
, with one property,name
.
data class Fish(val name: String)
- Create a list of 3
Fish
, with names Flipper, Moby Dick, and Dory.
val myFish = listOf(Fish("Flipper"), Fish("Moby Dick"), Fish("Dory"))
- Add a filter to check for names that contain the letter ‘i'.
myFish.filter { it.name.contains("i")}
⇒ res3: kotlin.collections.List<Line_1.Fish> = [Fish(name=Flipper), Fish(name=Moby Dick)]
In the lambda expression, it
refers to the current list element, and the filter is applied to each list element in turn.
- Apply
joinString()
to the result, using", "
as the separator.
myFish.filter { it.name.contains("i")}.joinToString(", ") { it.name }
⇒ res4: kotlin.String = Flipper, Moby Dick
The joinToString()
function creates a string by joining the filtered names, separated by the string specified. It is one of the many useful functions built into the Kotlin standard library.
5. Task: Write a higher-order function
Passing a lambda or other function as an argument to a function creates a higher-order function. The filter above is a simple example of this. filter()
is a function, and you pass it a lambda that specifies how to process each element of the list.
Writing higher-order functions with extension lambdas is one of the most advanced parts of the Kotlin language. It takes a while to learn how to write them, but they are really convenient to use.
Step 1: Create a new class
- Within the example package, create a new Kotlin file,
Fish.kt
. - In
Fish.kt
, create a data classFish
, with one property,name
.
data class Fish (var name: String)
- Create a function
fishExamples()
. InfishExamples()
, create a fish named"splashy"
, all lowercase.
fun fishExamples() {
val fish = Fish("splashy") // all lowercase
}
- Create a
main()
function which callsfishExamples()
.
fun main () {
fishExamples()
}
- Compile and run your program by clicking the green triangle to the left of
main()
. There is no output yet.
Step 2: Use a higher-order function
The with()
function lets you make one or more references to an object or property in a more compact way. Using this
. with()
is actually a higher-order function, and in the lamba you specify what to do with the supplied object.
- Use
with()
to capitalize the fish name infishExamples()
. Within the curly braces,this
refers to the object passed towith()
.
fun fishExamples() {
val fish = Fish("splashy") // all lowercase
with (fish.name) {
this.capitalize()
}
}
- There is no output, so add a
println()
around it. And thethis
is implicit and not needed, so you can remove it.
fun fishExamples() {
val fish = Fish("splashy") // all lowercase
with (fish.name) {
println(capitalize())
}
}
⇒ Splashy
Step 3: Create a higher-order function
Under the hood, with()
is a higher-order function. To see how this works, you can make your own greatly simplified version of with()
that just works for strings.
- In
Fish.kt
, define a function,myWith()
that takes two arguments. The arguments are the object to operate on, and a function that defines the operation. The convention for the argument name with the function isblock
. In this case, that function returns nothing, which is specified withUnit
.
fun myWith(name: String, block: String.() -> Unit) {}
Inside myWith()
, block()
is now an extension function of String
. The class being extended is often called the receiver object. So name
is the receiver object in this case.
- In the body of
myWith()
, apply the passed in function,block()
, to the receiver object,name
.
fun myWith(name: String, block: String.() -> Unit) {
name.block()
}
- In
fishExamples()
, replacewith()
withmyWith()
.
fun fishExamples() {
val fish = Fish("splashy") // all lowercase
myWith (fish.name) {
println(capitalize())
}
}
fish.name
is the name argument, and println(capitalize())
is the block function.
- Run the program, and it operates as before.
⇒ Splashy
Step 4: Explore more built in extensions
The with()
extension lambda is very useful, and is part of the Kotlin Standard Library. Here are a few of the others you might find handy: run()
, apply()
, and let()
.
The run()
function is an extension that works with all types. It takes one lambda as its argument and returns the result of executing the lambda.
- In
fishExamples()
, callrun()
onfish
to get the name.
fish.run {
name
}
This just returns the name
property. You could assign that to a variable or print it. This isn't actually a useful example, as you could just access the property, but run()
can be useful for more complicated expressions.
The apply()
function is similar to run()
, but it returns the changed object it was applied to instead of the result of the lambda. This can be useful for calling methods on a newly created object.
- Make a copy of
fish
and callapply()
to set the name of the new copy.
val fish2 = Fish(name = "splashy").apply {
name = "sharky"
}
println(fish2.name)
⇒ sharky
The let()
function is similar to apply()
, but it returns a copy of the object with the changes. This can be useful for chaining manipulations together.
- Use
let()
to get the name offish
, capitalize it, concatenate another string to it, get the length of that result, add 31 to the length, then print the result.
println(fish.let { it.name.capitalize()}
.let{it + "fish"}
.let{it.length}
.let{it + 31})
⇒ 42
In this example, the object type referred to by it
is Fish
, then String
, then String
again and finally Int
.
- Print
fish
after callinglet()
, and you will see that it hasn't changed.
println(fish.let { it.name.capitalize()}
.let{it + "fish"}
.let{it.length}
.let{it + 31})
println(fish)
⇒ 42 Fish(name=splashy)
6. Concept: Inline functions
Lambdas and higher-order functions are really useful, but there is something you should know: lambdas are objects. A lambda expression is an instance of a Function
interface, which is itself a subtype of Object
. Consider the earlier example of myWith()
.
myWith(fish.name) {
capitalize()
}
The Function
interface has a method, invoke()
, which is overridden to call the lambda expression. Written out longhand, it would look something like the code below.
// actually creates an object that looks like this
myWith(fish.name, object : Function1<String, Unit> {
override fun invoke(name: String) {
name.capitalize()
}
})
Normally this isn't a problem, because creating objects and calling functions doesn't incur much overhead, that is, memory and CPU time. But if you're defining something like myWith()
that you use everywhere, the overhead could add up.
Kotlin provides inline
as a way to handle this case to reduce overhead during runtime by adding a bit more work for the compiler. (You learned a little about inline
in the earlier lesson talking about reified types.) Marking a function as inline
means that every time the function is called, the compiler will actually transform the source code to "inline" the function. That is, the compiler will change the code to replace the lambda with the instructions inside the lambda.
If myWith()
in the above example is marked with inline
:
inline myWith(fish.name) {
capitalize()
}
it is transformed into a direct call:
// with myWith() inline, this becomes
fish.name.capitalize()
It is worth noting that inlining large functions does increase your code size, so it's best used for simple functions that are used many times like myWith()
. The extension functions from the libraries you learned about earlier are marked inline
, so you don't have to worry about extra objects being created.
7. Task: Learn about Single Abstract Methods
Single Abstract Method just means an interface with one method on it. They are very common when using APIs written in the Java programming language, so there is an acronym for it, SAM. Some examples are Runnable
, which has a single abstract method, run()
, and Callable
, which has a single abstract method, call()
.
In Kotlin, you have to call functions that take SAMs as parameters all the time. Try the example below.
- Inside example, create a Java class,
JavaRun
, and paste the following into the file.
package example;
public class JavaRun {
public static void runNow(Runnable runnable) {
runnable.run();
}
}
Kotlin lets you instantiate an object that implements an interface by preceding the type with object:
. It's useful for passing parameters to SAMs.
- Back in
Fish.kt
, create a functionrunExample()
, which creates aRunnable
usingobject:
The object should implementrun()
by printing"I'm a Runnable"
.
fun runExample() {
val runnable = object: Runnable {
override fun run() {
println("I'm a Runnable")
}
}
}
- Call
JavaRun.runNow()
with the object you created.
fun runExample() {
val runnable = object: Runnable {
override fun run() {
println("I'm a Runnable")
}
}
JavaRun.runNow(runnable)
}
- Call
runExample()
frommain()
and run the program.
⇒ I'm a Runnable
A lot of work to print something, but a good example of how a SAM works. Of course, Kotlin provides a simpler way to do this—use a lambda in place of the object to make this code a lot more compact.
- Remove the existing code in
runExample
, change it to callrunNow()
with a lambda, and run the program.
fun runExample() {
JavaRun.runNow({
println("Passing a lambda as a Runnable")
})
}
⇒ Passing a lambda as a Runnable
- You can make this even more concise using the last parameter call syntax, and get rid of the parentheses.
fun runExample() {
JavaRun.runNow {
println("Last parameter is a lambda as a Runnable")
}
}
⇒ Last parameter is a lambda as a Runnable
That's the basics of a SAM, a Single Abstract Method. You can instantiate, override and make a call to a SAM with one line of code, using the pattern: Class.singleAbstractMethod { lambda_of_override }
8. Summary
This lesson reviewed lambdas and went into more depth with higher-order functions—key parts of Kotlin. You also learned about annotations and labeled breaks.
- Use annotations to specify things to the compiler. For example:
@file:JvmName("Foo")
- Use labeled breaks to let your code exit from inside nested loops. For example:
if (i > 10) break@outerLoop // breaks to outerLoop label
- Lambdas can be very powerful when coupled with higher-order functions.
- Lambdas are objects. To avoid creating the object, you can mark the function with
inline
, and the compiler will put the contents of the lambda in the code directly. - Use
inline
carefully, but it can help reduce resource usage by your program. - SAM, Single Abstract Method, is a common pattern, and made simpler with lambdas. The basic pattern is:
Class.singleAbstractMethod { lamba_of_override }
- The Kotlin Standard Library provides numerous useful functions, including several SAMs, so get to know what's in it.
There's lots more to Kotlin than was covered in the course, but you now have the basics to begin developing your own Kotlin programs. Hopefully you're excited about this expressive language, and looking forward to creating more functionality while writing less code (especially if you're coming from the Java programming language.) Practice and learning as you go is the best way to become an expert in Kotlin, so continue to explore and learn about Kotlin on your own.
9. 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
- Annotations
- Reflection
- Labeled breaks
- Higher-order functions and lambdas
- Inline functions
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.
Kotlin Standard Library
The Kotlin Standard Library provides numerous useful functions. Before you write your own function or interface, always check the Standard Library to see if someone has saved you some work. Check back occasionally, because new functionality is added frequently.
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.
10. 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
In Kotlin, SAM stands for:
▢ Safe Argument Matching
▢ Simple Access Method
▢ Single Abstract Method
▢ Strategic Access Methodology
Question 2
Which one of the following is not a Kotlin Standard Library extension function?
▢ elvis()
▢ apply()
▢ run()
▢ with()
Question 3
Which one of the following is not true of lambdas in Kotlin?
▢ Lambdas are anonymous functions.
▢ Lambdas are objects unless inlined.
▢ Lambdas are resource intensive and shouldn't be used.
▢ Lambdas can be passed to other functions.
Question 4
Labels in Kotlin are indicated with an identifier followed by:
▢ :
▢ ::
▢ @:
▢ @
11. Next steps
Congratulations! You've completed the Kotlin Bootcamp for Programmers codelab.
For an overview of the course, including links to other codelabs, see "Kotlin Bootcamp for Programmers: Welcome to the course."
If you're a Java programmer, you may be interested in the Refactoring to Kotlin codelab. The automated Java to Kotlin conversion tools cover the basics, but you can create more concise, robust code with a little extra work.
If you're interested in developing apps for Android, take a look at Android Kotlin Fundamentals.