Kotlin Collections and Lamdas - mariamaged/Java-Android-Kotlin GitHub Wiki
Kotlin/JVM also has access to other standard Java collection types, like LinkedHashMap.
However, usually, you will use those only if there is no native Kotlin equivalent.
- Kotlin has an
Array
class that is analogues to Java arrays. - In Java, arrays are a primitive (non-object) construct.
- If you use Kotlin arrays, and you are running on Kotlin/JVM, your Kotlin arrays will be mapped to Java arrays.
- They maybe necessary if your code is going to
interoperate with Java code
.
- To create an array, the simplest approach is to the arrayOf() utility function.
val things = arrayOf("foo, "bar", "goo");
println(things::class) // prints kotlin.Array
- Kotlin also has dedicated classes for arrays of Java primitive types:
- BooelanArray.
- ByteArray.
- CharArray.
- DoubleArray.
- FloatArray.
- IntArray.
- LongArray.
- ShortArray.
- They have corresponding utility functions to create instances, such as
intArrayOf()
.
val things = intArrayOf(1, 3, 3, 7);
println(things::class) // Prints kotlin.IntArray.
The basic one-dimensional collection type in Kotlin is the list.
- This is analogous to a Java ArrayList.
- You can create instances of a list via
listOf()
, much like you create arrays witharrayOf()
and the other mentioned above. - An unusual aspect of lists is that
listOf()
creates an "immutable" list, one where we cannot replace items in the list.
val things = listOf("foo", "bar", "goo");
- Many programming languages offer a "set" collection type.
- Which is a one dimensional collection in which only distinct objects will be included.
- For example, if you try adding the four numbers 1, 3, 3, 7 to a list, you would have four entries.
- But adding them to a set results in a three-element set holding 1, 3, 7.
- The
duplicate
3 value is ignored.
val things = setOf(1, 3, 3, 7)
println(things.size) // Prints 3.
- This code snippet prints out the size of the things, which is a property in both sets and lists.
- And, with lists,
setOf()
creates animmutable set
.
Most "programming languages" have some support for a "map" or "dictionary" type, representing a key-value store.
You can then place data into the collection as
key-value
pairs, to be able to eventually retrieve values given their keys.
- To create a hard-coded map - the way we have created hard-coded lists and sets - you can use the
mapOf()
utility function. - This takes a
comma-delimited list
ofkey-value pairs
, where the key and value are separated by the to keyword.
val things = mapOf("key" to "value", "other-key" to "other-value")
val things = mapOf("odd"" to listOf(1, 3, 5, 7, 9), "even" to listOf(2, 4, 6, 8))
- To retrieve values from a collection (other than a Set), you can use [].
- For arrays and lists, the value passed into the [] operator is the
0-based index
into the collection. - For maps, the value passed into the [] operator is the
key
for which you wish to look up the corresponding value.
- For arrays and lists, the value passed into the [] operator is the
val thingsArray = arrayOf("foo", "bar", "goo")
println(thingsArray[0]) // Prints foo.
val thingsIntArray = intArrayOf(1, 3, 3, 7)
println(thingsIntArray[1]) // Prints 3.
val thingsList = listOf("foo", "bar", "goo")
println(thingsList[2]) // Prints goo.
val thingsMap = mapOf("key" to "value", "other-key" to "other-value")
println(thingsMap["other-key])) // Prints other-value.
- Things get a bit interesting if we start playing with that map of the lists of odd and even single-digit integers:
val oddEven = mapOf("odd" to listOf(1, 3, 5, 7, 9), "even" to listOf(2, 4, 6, 8))
println(oddEven["odd"][4])
- This will fail to run, with a compile error:
error: only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type List<Int>?
- A Map may return
null
forgiven key
, as there may not be a value for that key. - As it turns out, our Map has a value for the odd key, and so at runtime, this would succeed.
- And in most programming languages, we could actually compile and run this sort of operation.
- This fails in Kotlin, because of how Kotlin handles the possibility of null as a value.
- isEmpty(), for a quick Boolean value based on whether the collection is empty.
-
contains(), which will return true if the collection contains the requested item.
- For maps, this check the
keys
. - Use
containsValue
to check thevalues
.
- For maps, this check the
- For lists ans arrays, you have things like:
- first(), to get the first element.
- last(), to get the last element.
- firstOrNull(), to get the first element or null if the collection is empty.
-
Array offers
toList()
andtoSet()
. -
List offers
toSet()
andtoTypedArray()
. -
Set offers
toList()
andtoTypedArray()
.
- Array objects that we create with arrayOf() are mutable.
- Not only can we use [] syntax to
retrieve a value
based on its index, but we can use []= syntax toreplace a value
on its index.
val thingsArray = arrayOf("foo", "bar", "goo")
thingsArray[1] = "something completely different"
println(thingArray[1]) // Prints "something completely different.
- However, this fails if you try it with a List created using
listOf()
.
val thingsList = listOf("foo", "bar", "goo")
thingsList[1] = "something completely different"
println(thingsList[1]) // You get a weird syntax erorr, complaining that Kotlin cannot find anything to support [ ] =.
- If you need a list where you can replace its members, you can use
mutableListOf()
:
val thingsList = mutableListOf("foo", "bar", "goo")
thingsList[1] = "something completely different"
println(thingsList[1]) // Prints something completely different.
- The same distinction holds true for Map.
- A Map is immutable.
- But a MutableMap created via
mutableMapOf()
can be modified.
- It is particularly important when working with collections, as lamda expressions are critical for performing common operations, such as:
- Looping over the elements.
- Finding or filtering elements that match certain criteria.
- Providing comparison logic for sorting elements.
- And so on.
A simple way to start thinking about lamda expressions is to consider them just as a
set of Kotlin statements wrapped in curly braces
.
val lamda = {println("Hello, world!")}
- However, as the
val
indicates, a lamda expression is an object. - You can:
- Hold onto them in variables.
- Pass them as function parameters.
- Do everything that you might do with other type of objects.
Kotlin lamda expressions closely resemble their Java counterparts, along with similar constructs in other languages, such as Ruby blocks.
- If you receive a lamda expression, and you want to run it, call invoke():
val lamda = { println("Hello, world!") }
lamda.invoke() // Prints "Hello, world"
- At minimum, lamda expressions replace a lot of the "listener" or "callback"
patterns
that you might have used inJava
.- For example, instead of creating an implementation of a Comparator to support a
sort()
method, as we do in Java, you would use a lamda expression.
- For example, instead of creating an implementation of a Comparator to support a
- Kotlin also lends itself towards the functional programming paradigm, where business logic is made up of chains of
linked function calls
.
- Like functions, lamda expressions
can take parameters
:
val squarifier = {x: Int -> x *x }
println(squarifier.invoke(3)) // prints 9
- The
list of parameters
to a lamda expression come at thebeginning
. - With the
"body"
of the lamda expressionfollowing the ->
.
- However, single-parameter lamdas are very common.
- For that, Kotlin offers a shorthands syntax: it.
- If the Kotlin compiler
can determine the data type
for the parameter, you canskip
theparameter declaration
and just refer to the parameter as it.
- A lamda expression will return whatever
its last statement does
. - In the case of squarifier, it has only one statement: x * x.
- So, that value is what the lamda expression returns from the call to
invoke()
.
A common thing for a collection is to iterate over its contents.
- There are two ways to do that in Kotlin:
- forEach().
- for().
val things = listOf("foo", "bar", "goo")
things.forEach{ println(it) }
- In other ways,
forEach()
iterates over each member of the collection andinvokes the lamda expression
for each one in turn. - Our lamda expression happens to print out the value of the lamda's expression's parameter.
- In our case, we use the shorthand it reference to that parameter.
- Alternatively, we have could have given it a custom name:
val things = listOf("foo", "bar", "goo")
things.forEach{thing -> println(thing)}
-
forEach()
is a function, available for List and other collection types. -
forEach()
takes a lamda expression as a parameter. - It may not look like a function, given that it lacks parentheses after the
forEach
name. - In this case, this is a bit of simplified Kotlin syntax.
- A function that takes
one lamda expression parameter
can becalled
without the parentheses
.
val things = listOf("foo", "bar", "goo")
things.forEach({thing -> println(thing) })
- The lamda expression takes a single parameter.
- The type of the parameter is determined by the definition of the
forEach()
- just as a function can declare a parameter as being an Int or a String, it candeclare a parameter
as being alamda expression that takes itself a certain parameter
.
val things = listOf("foo", "bar", "goo")
for(thing in things) {
println(thing)
}
- The parenthetical expression after the for keyword consists of:
- A name to use for an individual member.
- And the collection.
- Separated by the in keyword.
- The block of code that follows can then refer to an individual member by whatever name you gave it, and that block will be executed once for each member in the sequence.
- If you have used RxJava, you will recall that its Observable class has a
filter() operator
, allowing you to pass on asubset of events
based on some criteria that you specify.
val things = listOf("foo", "bar", "goo")
things.filter{ it[1] == 'o'}.forEach{ println(it) }
- Here, we have chained expression.
- We filter() the things collection.
- The call forEach() on the output of filter().
While this can be written on one line, you will see it more often with each expression starting on its own line.
val things = listOf("foo", "bar", "goo")
things
.filter{ it[1] == 'o'}
.forEach{ println(it) }
- Prints:
foo
goo
filter() passes along a strict subset of the items in the collection, but the items themselves are unchanged.
- map(), by contrast, passes along each item from the collection, transformed by a lamda expression that you supply.
val things = listOf("foo", "bar", "goo")
things
.map { it.toUpperCase() }
.forEach { println(it) }
- Prints:
FOO
BAR
GOO
- There is no requirement for
map()
to return the same type as it starts with, though:
val primes = listOf(1, 2, 3, 5, 7)
primes
.map{ 1.0 / it }
.forEach{ println(it) }
- Prints:
1.0
0.5
0.3333333333333333
0.2
0.14285714285714285
Many programming languages have the concept of "varargs": parameters are declared for a method or function that that represent zero, one, or several values.
- In Java, we use
...
syntax. -
lots is treated as a
Java array
, in this case an array of String objects.
void something(String... lots) {
if(lots.length>0) {
// Do something.
}
}
-
JavaScript just dumps everything not matched by named parameters into an
arguments array
.
function something() {
if(arguments.length > 0) {
// Do something.
}
}
- Ruby uses * syntax:
def something(*lots) {
if(lots.length > 0) {
// Do something.
}
}
-
Kotlin dedicates the
vararg
keyword to this role. - Here,
reciprocate
can accept:- Zero parameters.
- One parameter.
- Many parameters.
- We assign the vararg a name (values).
- The parameters are given to us in the form of an
Array
.
fun reciprocate(vararg values: Int) {
values
.map { 1.0 / it }
.forEach { println(it) }
}
reciprocate(1, 2, 3, 5, 7)
- Any expression , including simple variable references, can be supplied for values.
val foo = 1
val bar = 2
val goo = 3
val baz = 5
val heyWhatComesAfterBaz = 7
reciprocate(foo, bar, goo, baz, heyWhatComesAfterBaz)
- In addition, Kotlin offers special support for an existing
Array
. - If you already have an Array with the values that you want to supply, but the function that you are calling uses
vararg
(instead of Array parameter), you can use the "spread operator".
fun capped(vararg string: String) = strings.map { it.toUpperCase }
val things = arrayOf("foo", "bar", "goo")
capped(*things)
.forEach { println(it) }
- Things get a bit strange with types that map to JVM primitives, though.
-
vararg
expects thededicated array types
to be used with thespread operator
.
fun reciprocate(vararg values: Int) {
values
.map { 1.0 / it)
.forEach { println(it) }
}
val primes = intArrayOf(1, 2, 3, 5, 7)
reciprocate(*primes)
- If you are using
Kotlin/JVM
- for example, if you are using Kotlin for ordinary Android development - you are welcome to useJVM collection types
such as LinkedList.
- On the whole, you should stick with Kotlin collection types where possible, as the Kotlin types are more "natural" for use in Kotlin.
- But sometimes you will find a Java class that fits your scenario better, such as using a LinkedHashMap to implement an LRU-style cache.