As in Java, Kotlin uses typed collections to hold multiple objects. Unlike Java, Kotlin adds many interesting methods directly to the collection classes, rather than going through a stream intermediary.
Recipes in this chapter discuss ways to process both arrays and collections, ranging from sorting and searching, to providing read-only views, to accessing windows of data, and more.
Use the arrayOf
function to create them, and the properties and methods inside the Array
class to work with the contained values.
Virtually every programming language has arrays, and Kotlin is no exception. This book focuses on Kotlin running on the JVM, and in Java arrays are handled a bit differently than they are in Kotlin. In Java you instantiate an array using the keyword new
and dimensioning the array, as in Example 5-1.
String
[]
strings
=
new
String
[
4
];
strings
[
0
]
=
"an"
;
strings
[
1
]
=
"array"
;
strings
[
2
]
=
"of"
;
strings
[
3
]
=
"strings"
;
// or, more easily,
strings
=
"an array of strings"
.
split
(
" "
);
Kotlin provides a simple factory method called arrayOf
for creating arrays, and while it uses the same syntax for accessing elements, in Kotlin Array
is a class. Example 5-2 shows how the factory method works.
arrayOf
factory methodval
strings
=
arrayOf
(
"this"
,
"is"
,
"an"
,
"array"
,
"of"
,
"strings"
)
You can also use the factory method arrayOfNulls
to create (as you might guess) an array containing only nulls, as in Example 5-3.
val
nullStringArray
=
arrayOfNulls
<
String
>(
5
)
It’s interesting that even though the array contains only null
values, you still have to choose a particular data type for it. After all, it may not contain nulls forever, and the compiler needs to know what type of reference you plan to add to it. The factory method emptyArray
works the same way.
There is only one public constructor in the Array
class. It takes two arguments:
size
of type Int
init
, a lambda of type (Int) -> T
The lambda is invoked on each index when creating the array. For example, to create an array of strings containing the first five integers squared, see Example 5-4.
val
squares
=
Array
(
5
)
{
i
-
>
(
i
*
i
)
.
toString
(
)
}
The Array
class declares public operator methods get
and set
, which are invoked when you access elements of the array using square brackets, as in squares[1]
.
Kotlin has specialized classes to represent arrays of primitive types to avoid the cost of autoboxing and unboxing. The functions booleanArrayOf
, byteArrayOf
, shortArrayOf
, charArrayOf
, intArrayOf
, longArrayOf
, floatArrayOf
, and doubleArrayOf
create the associated types (BooleanArray
, ByteArray
, ShortArray
, etc.) exactly the way you would expect.
Even though Kotlin doesn’t have explicit primitives, the generated bytecodes use Java wrapper classes like Integer
and Double
when the values are nullable, and primitive types like int
and double
if not.
Many of the extension methods on arrays are the same as their counterparts on collections, which are discussed in the rest of this chapter. A couple are unique to arrays, however. For example, if you want to know the valid index values for a given array, use the property indices
, as in Example 5-5.
@Test
fun
`
valid
indices
`
()
{
val
strings
=
arrayOf
(
"this"
,
"is"
,
"an"
,
"array"
,
"of"
,
"strings"
)
val
indices
=
strings
.
indices
assertThat
(
indices
,
contains
(
0
,
1
,
2
,
3
,
4
,
5
))
}
Normally you iterate over an array using the standard for-in loop, but if you want the index values as well, use the function withIndex
.
fun
<
T
>
Array
<
out
T
>.
withIndex
():
Iterable
<
IndexedValue
<
T
>>
data
class
IndexedValue
<
out
T
>(
public
val
index
:
Int
,
public
val
value
:
T
)
The class IndexedValue
is the data class shown, with properties called index
and value
. Use it as shown in Example 5-6.
withIndex
@Test
fun
`
withIndex
returns
IndexValues
`
(
)
{
val
strings
=
arrayOf
(
"this"
,
"is"
,
"an"
,
"array"
,
"of"
,
"strings"
)
for
(
(
index
,
value
)
in
strings
.
withIndex
(
)
)
{
println
(
"Index $index maps to $value"
)
assertTrue
(
index
in
0.
.
5
)
}
}
The results printed to standard output are:
Index 0 maps to this Index 1 maps to is Index 2 maps to an Index 3 maps to array Index 4 maps to of Index 5 maps to strings
In general, Kotlin arrays behave the same way arrays in other languages do.
Use one of the functions designed to produce either an unmodifiable collection, like listOf
, setOf
, and mapOf
, or their mutable equivalents, mutableListOf
, mutableSetOf
, and mutableMapOf
.
If you want an immutable view of a collection, the kotlin.collections package provides a series of utility functions for doing so.
One example is listOf(vararg elements: T): List<T>
, whose implementation is shown in Example 5-7.
listOf
functionpublic
fun
<
T
>
listOf
(
vararg
elements
:
T
):
List
<
T
>
=
if
(
elements
.
size
>
0
)
elements
.
asList
()
else
emptyList
()
The referenced asList
function is an extension function on Array
that returns a List
that wraps the specified array. The resulting list is called immutable, but should more properly be considered read-only: you cannot add to nor remove elements from it, but if the contained objects are mutable, the list will appear to change.
The implementation of asList
delegates to Java’s Arrays.asList
, which returns a read-only list.
Similar functions in the same package include the following:
listOf
setOf
mapOf
Example 5-8 shows how to create lists and sets.
var
numList
=
listOf
(
3
,
1
,
4
,
1
,
5
,
9
)
var
numSet
=
setOf
(
3
,
1
,
4
,
1
,
5
,
9
)
// numSet.size == 5
var
map
=
mapOf
(
1
to
"one"
,
2
to
"two"
,
3
to
"three"
)
By default, Kotlin collections are “immutable,” in the sense that they do not support methods for adding or removing elements. If the elements themselves can be modified, the collection can appear to change, but only read-only operations are supported on the collection itself.
Methods to modify collections are in the “mutable” interfaces, provided by the factory methods:
mutableListOf
mutableSetOf
mutableMapOf
Example 5-9 shows the analogous mutable examples.
var
numList
=
mutableListOf
(
3
,
1
,
4
,
1
,
5
,
9
)
var
numSet
=
mutableSetOf
(
3
,
1
,
4
,
1
,
5
,
9
)
var
map
=
mutableMapOf
(
1
to
"one"
,
2
to
"two"
,
3
to
"three"
)
The implementation of the mapOf
function in the standard library is shown here:
public
fun
<
K
,
V
>
mapOf
(
vararg
pairs
:
Pair
<
K
,
V
>):
Map
<
K
,
V
>
=
if
(
pairs
.
size
>
0
)
pairs
.
toMap
(
LinkedHashMap
(
mapCapacity
(
pairs
.
size
)))
else
emptyMap
()
The argument to the mapOf
function is a variable argument list of Pair
instances, so the infix to
operator function is used to create the map entries. A similar function is used to create mutable maps.
You can also instantiate classes that implement the List
, Set
, or Map
interfaces directly, as shown in Example 5-10.
To make a new, read-only collection, use the toList
, toSet
, or toMap
methods. To make a read-only view on an existing collection, assign it to a variable of type List
, Set
, or Map
.
Consider a mutable list created with the mutableList
factory method. The resulting list has methods like add
, remove
, and so on that allow the list to grow or shrink as desired:
val
mutableNums
=
mutableListOf
(
3
,
1
,
4
,
1
,
5
,
9
)
There are two ways to create a read-only version of a mutable list. The first is to invoke the toList
method, which returns a reference of type List
:
@Test
fun
`
toList
on
mutableList
makes
a
readOnly
new
list
`
(
)
{
val
readOnlyNumList
:
List
<
Int
>
=
mutableNums
.
toList
(
)
assertEquals
(
mutableNums
,
readOnlyNumList
)
assertNotSame
(
mutableNums
,
readOnlyNumList
)
}
The test shows that the return type from the toList
method is List<T>
, which represents an immutable list, so methods like add
or remove
are not available. The rest of the test shows that the method is creating a separate object, however, so while it has the same contents as the original, it doesn’t represent the same objects anymore:
@Test
internal
fun
`
modify
mutable
list
does
not
change
read
-
only
list
`
()
{
val
readOnly
:
List
<
Int
>
=
mutableNums
.
toList
()
assertEquals
(
mutableNums
,
readOnly
)
mutableNums
.
add
(
2
)
assertThat
(
readOnly
,
not
(
contains
(
2
)))
}
If you want a read-only view of the same contents, assign the mutable list to a reference of type List
, as shown in Example 5-11.
@Test
internal
fun
`
read
-
only
view
of
a
mutable
list
`
(
)
{
val
readOnlySameList
:
List
<
Int
>
=
mutableNums
assertEquals
(
mutableNums
,
readOnlySameList
)
assertSame
(
mutableNums
,
readOnlySameList
)
mutableNums
.
add
(
2
)
assertEquals
(
mutableNums
,
readOnlySameList
)
assertSame
(
mutableNums
,
readOnlySameList
)
}
This time, the mutable list is assigned to a reference of type List
. Not only is the result still the same object, but if the underlying mutable list is modified, the read-only view shows the updated values. You can’t modify the list from the read-only reference, but it is attached to the same object as the original.
As you might expect, the toSet
and toMap
functions work the same way, as does assigning mutable sets and maps to references of type Set
or Map
.
Say you have a set of keys and want to map each of them to a generated value. One way to do that is to use the associate
function, as in Example 5-12.
associate
to generate valuesval
keys
=
'a'
..
'f'
val
map
=
keys
.
associate
{
it
to
it
.
toString
().
repeat
(
5
).
capitalize
()
}
println
(
map
)
Executing this snippet results in the following:
{
a
=
Aaaaa,b
=
Bbbbb,c
=
Ccccc,d
=
Ddddd,e
=
Eeeee}
The associate
function is an inline extension function on Iterable<T>
that takes a lambda that transforms T
into a Pair<K,V>
. In this example, the to
function is an infix function that produces a Pair
from the left- and right-side arguments.
This works, but in Kotlin 1.3 a new function was added called associateWith
that simplifies the code. Example 5-13 shows the previous code reworked with associateWith
.
associateWith
to generate valuesval
keys
=
'a'
..
'f'
val
map
=
keys
.
associateWith
{
it
.
toString
().
repeat
(
5
).
capitalize
()
}
println
(
map
)
The result is the same, but the argument now is a function that produces a String
value rather than a Pair<Char, String>
.
Both examples produce the same result, but the associateWith
function is slightly simpler to write and understand.
Say you have a data class called Product
that wraps a name, a price, and a boolean field to indicate whether the product is on sale, as in Example 5-14.
data
class
Product
(
val
name
:
String
,
var
price
:
Double
,
var
onSale
:
Boolean
=
false
)
If you have a list of products and you want the names of the products that are on sale, you could do a simple filtering operation, as follows:
fun
namesOfProductsOnSale
(
products
:
List
<
Product
>)
=
products
.
filter
{
it
.
onSale
}
.
map
{
it
.
name
}
.
joinToString
(
separator
=
", "
)
The idea is to take a list of products, filter them by the boolean onSale
property, and map them to just the names, which then are joined into a single string. The problem is that if no products are on sale, the filter will return an empty collection, which will then be converted into an empty string.
If you would rather return a specific string when the result is empty, you can use a function called ifEmpty
on both Collection
and String
. Example 5-15 shows how to use either one.
ifEmpty
on Collection
and String
fun
onSaleProducts_ifEmptyCollection
(
products
:
List
<
Product
>
)
=
products
.
filter
{
it
.
onSale
}
.
map
{
it
.
name
}
.
ifEmpty
{
listOf
(
"none"
)
}
.
joinToString
(
separator
=
", "
)
fun
onSaleProducts_ifEmptyString
(
products
:
List
<
Product
>
)
=
products
.
filter
{
it
.
onSale
}
.
map
{
it
.
name
}
.
joinToString
(
separator
=
", "
)
.
ifEmpty
{
"none"
}
In either case, a collection of products that are not on sale will return the string "none"
as shown in the tests in Example 5-16.
class
IfEmptyOrBlankKtTest
{
private
val
overthruster
=
Product
(
"Oscillation Overthruster"
,
1
_000_000
.
0
)
private
val
fluxcapacitor
=
Product
(
"Flux Capacitor"
,
299
_999
.
95
,
onSale
=
true
)
private
val
tpsReportCoverSheet
=
Product
(
"TPS Report Cover Sheet"
,
0.25
)
@Test
fun
productsOnSale
()
{
val
products
=
listOf
(
overthruster
,
fluxcapacitor
,
tpsReportCoverSheet
)
assertAll
(
"On sale products"
,
{
assertEquals
(
"Flux Capacitor"
,
onSaleProducts_ifEmptyCollection
(
products
))
},
{
assertEquals
(
"Flux Capacitor"
,
onSaleProducts_ifEmptyString
(
products
))
})
}
@Test
fun
productsNotOnSale
()
{
val
products
=
listOf
(
overthruster
,
tpsReportCoverSheet
)
assertAll
(
"No products on sale"
,
{
assertEquals
(
"none"
,
onSaleProducts_ifEmptyCollection
(
products
))
},
{
assertEquals
(
"none"
,
onSaleProducts_ifEmptyString
(
products
))
})
}
}
Java in version 8 added a class called Optional<T>
, which is often used as a return type wrapper when a query may legitimately return a null or empty value. Kotlin supports this as well, but it’s easy enough to return a specific value instead by using the ifEmpty
function.
Use the coerceIn
function on ranges, either with a range argument or specified min and max values.
There are two overloads of the coerceIn
function for ranges: one that takes a closed range as an argument, and one that takes min and max values.
For the first variation, consider an integer range from 3 to 8, inclusive. The test in Example 5-17 shows that coerceIn
returns the value if it is contained in the range, or the boundaries if not.
@Test
fun
`
coerceIn
given
a
range
`
(
)
{
val
range
=
3.
.
8
assertThat
(
5
,
`is`
(
5.
coerceIn
(
range
)
)
)
assertThat
(
range
.
start
,
`is`
(
1.
coerceIn
(
range
)
)
)
assertThat
(
range
.
endInclusive
,
`is`
(
9.
coerceIn
(
range
)
)
)
}
Likewise, if you have the min and max values you want, you don’t have to create a range to use the coerceIn
function, as Example 5-18 shows.
@Test
fun
`
coerceIn
given
min
and
max
`
()
{
val
min
=
2
val
max
=
6
assertThat
(
5
,
`is`
(
5.
coerceIn
(
min
,
max
)))
assertThat
(
min
,
`is`
(
1.
coerceIn
(
min
,
max
)))
assertThat
(
max
,
`is`
(
9.
coerceIn
(
min
,
max
)))
}
This version returns the value if it is between min and max, and the boundary values if not.
Given an iterable collection, the chunked
function splits it into a list of lists, where each has the given size or smaller. The function can return the list of lists, or you can also supply a transformation to apply to the resulting lists. The signatures of the chunked
function are as follows:
fun
<
T
>
Iterable
<
T
>.
chunked
(
size
:
Int
):
List
<
List
<
T
>>
fun
<
T
,
R
>
Iterable
<
T
>.
chunked
(
size
:
Int
,
transform
:
(
List
<
T
>)
->
R
):
List
<
R
>
This all sounds more complicated than it is in practice. For example, consider a simple range of integers from 0 to 10. The test in Example 5-19 breaks it into groups of three consecutive numbers, or computes their sums or averages.
@Test
internal
fun
chunked
()
{
val
range
=
0.
.
10
val
chunked
=
range
.
chunked
(
3
)
assertThat
(
chunked
,
contains
(
listOf
(
0
,
1
,
2
),
listOf
(
3
,
4
,
5
),
listOf
(
6
,
7
,
8
),
listOf
(
9
,
10
)))
assertThat
(
range
.
chunked
(
3
)
{
it
.
sum
()
},
`is`
(
listOf
(
3
,
12
,
21
,
19
)))
assertThat
(
range
.
chunked
(
3
)
{
it
.
average
()
},
`is`
(
listOf
(
1.0
,
4.0
,
7.0
,
9.5
)))
}
The first call simply returns the List<List<Int>>
consisting of [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10]]
. The second and third calls provide a lambda to compute the sums of each list or the average of each list, respectively.
The chunked
function is actually a special case of the windowed
function. Example 5-20 shows how the implementation of chunked
delegates to windowed
.
chunked
in the standard librarypublic
fun
<
T
>
Iterable
<
T
>.
chunked
(
size
:
Int
):
List
<
List
<
T
>>
{
return
windowed
(
size
,
size
,
partialWindows
=
true
)
}
The windowed
function takes three arguments, two of which are optional:
size
The number of elements in each window
step
The number of elements to move forward on each step (defaults to 1)
partialWindows
A boolean that defaults to false
and tells whether to keep the last section if it doesn’t have the required number of elements
The chunked
function calls windowed
with both the size
and step
parameters equal to the chunked
argument, so it moves the window forward by exactly that size each time. You can use windowed
directly, however, to change that.
An example is to compute a moving average. Example 5-21 shows how to use windowed
both to behave the same way as chunked
and to move the window forward by only one element each time.
@Test
fun
windowed
()
{
val
range
=
0.
.
10
assertThat
(
range
.
windowed
(
3
,
3
),
contains
(
listOf
(
0
,
1
,
2
),
listOf
(
3
,
4
,
5
),
listOf
(
6
,
7
,
8
)))
assertThat
(
range
.
windowed
(
3
,
3
)
{
it
.
average
()
},
contains
(
1.0
,
4.0
,
7.0
))
assertThat
(
range
.
windowed
(
3
,
1
),
contains
(
listOf
(
0
,
1
,
2
),
listOf
(
1
,
2
,
3
),
listOf
(
2
,
3
,
4
),
listOf
(
3
,
4
,
5
),
listOf
(
4
,
5
,
6
),
listOf
(
5
,
6
,
7
),
listOf
(
6
,
7
,
8
),
listOf
(
7
,
8
,
9
),
listOf
(
8
,
9
,
10
)))
assertThat
(
range
.
windowed
(
3
,
1
)
{
it
.
average
()
},
contains
(
1.0
,
2.0
,
3.0
,
4.0
,
5.0
,
6.0
,
7.0
,
8.0
,
9.0
)
The chunked
and windowed
functions are useful for processing time-series data in stages.
Assign the list to a group of at most five elements.
Destructuring is the process of extracting values from an object by assigning them to a collection of variables.
Example 5-22 shows how you can assign the first few elements of a list to defined variables in one step.
val
list
=
listOf
(
"a"
,
"b"
,
"c"
,
"d"
,
"e"
,
"f"
,
"g"
)
val
(
a
,
b
,
c
,
d
,
e
)
=
list
println
(
"$a $b $c $d $e"
)
This code prints the string a b c d e
, because the first five elements of the created list are assigned to the variables of the same name. This works because the List
class has extension functions defined in the standard library called componentN
, where N goes from 1 to 5, as shown in Example 5-23.
component1
extension function on List
(from the standard library)/**
* Returns 1st *element* from the collection.
*/
@kotlin
.
internal
.
InlineOnly
public
inline
operator
fun
<
T
>
List
<
T
>.
component1
():
T
{
return
get
(
0
)
}
Destructuring relies on the existence of componentN
functions. The List
class contains implementations for component1
, component2
, component3
, component4
, and component5
, so the preceding code works.
Data classes automatically add the associated component methods for all of their defined attributes. If you define your own class (and don’t make it a data class), you can manually define any needed component methods as well.
Destructuring is a convenient way to extract multiple elements from an object. At the moment, the List
class defines component functions for the first five elements. That may change in later Kotlin versions.
Use the sortedWith
and compareBy
functions.
Say we have a simple data class called Golfer
, with a sample collection shown in Example 5-24.
data
class
Golfer
(
val
score
:
Int
,
val
first
:
String
,
val
last
:
String
)
val
golfers
=
listOf
(
Golfer
(
70
,
"Jack"
,
"Nicklaus"
),
Golfer
(
68
,
"Tom"
,
"Watson"
),
Golfer
(
68
,
"Bubba"
,
"Watson"
),
Golfer
(
70
,
"Tiger"
,
"Woods"
),
Golfer
(
68
,
"Ty"
,
"Webb"
)
)
If you would like to sort the golfers by score, then sort equal scores by last name, and finally sort those equal scores and last names by first name, you can use the code in Example 5-25.
val
sorted
=
golfers
.
sortedWith
(
compareBy
({
it
.
score
},
{
it
.
last
},
{
it
.
first
})
)
sorted
.
forEach
{
println
(
it
)
}
The result is as follows:
Golfer(
score
=
68,first
=
Bubba,last
=
Watson)
Golfer(
score
=
68,first
=
Tom,last
=
Watson)
Golfer(
score
=
68,first
=
Ty,last
=
Webb)
Golfer(
score
=
70,first
=
Jack,last
=
Nicklaus)
Golfer(
score
=
70,first
=
Tiger,last
=
Woods)
The three golfers who shot a 68 appear before the two who scored 70. Within the 68s, both Watsons appear ahead of Webb, and in the 70s, Nicklaus is before Woods. For the golfers named Watson who both scored 68s, Bubba appears before Tom.
The full signatures of the sortedWith
and compareBy
functions are given by Example 5-26.
sortedWith
in the standard libraryfun
<
T
>
Iterable
<
T
>.
sortedWith
(
comparator
:
Comparator
<
in
T
>
):
List
<
T
>
fun
<
T
>
compareBy
(
vararg
selectors
:
(
T
)
->
Comparable
<*>?
):
Comparator
<
T
>
So the sortedWith
function takes a Comparator
, and the compareBy
function produces a Comparator
. What’s interesting about compareBy
is that you can provide a list of selectors, each of which extracts a Comparable
property (note that the property’s class has to implement the Comparable
interface), and the function will create a Comparator
that sorts by them in turn.
The sortBy
and sortWith
functions sort their elements in place, and therefore require mutable collections.
Another way to solve the same problem is to build the Comparator
by using the thenBy
function, which applies a comparison after the previous one. The same collection is sorted in this manner in Example 5-27.
val
comparator
=
compareBy
<
Golfer
>(
Golfer
::
score
)
.
thenBy
(
Golfer
::
last
)
.
thenBy
(
Golfer
::
first
)
golfers
.
sortedWith
(
comparator
)
.
forEach
(
::
println
)
Define an operator function that returns an iterator, which implements both a next
and a hasNext
function.
The Iterator design pattern has an implementation in Java by defining the Iterator
interface. Example 5-28 provides the corresponding definition in Kotlin.
kotlin.collections
interface
Iterator
<
out
T
>
{
operator
fun
next
():
T
operator
fun
hasNext
():
Boolean
}
In Java, the for-each loop lets you iterate over any class that implements Iterable
. In Kotlin, a similar constraint works on the for-in loop. Consider a data class called Player
and a class called Team
, as given in Example 5-29.
Player
and Team
classesdata
class
Player
(
val
name
:
String
)
class
Team
(
val
name
:
String
,
val
players
:
MutableList
<
Player
>
=
mutableListOf
())
{
fun
addPlayers
(
vararg
people
:
Player
)
=
players
.
addAll
(
people
)
// ... other functions as needed ...
}
A Team
contains a mutable list of Player
instances. If you have a team with several players and you want to iterate over the players, you need to access the players
property, as in Example 5-30.
val
team
=
Team
(
"Warriors"
)
team
.
addPlayers
(
Player
(
"Curry"
)
,
Player
(
"Thompson"
)
,
Player
(
"Durant"
)
,
Player
(
"Green"
)
,
Player
(
"Cousins"
)
)
for
(
player
in
team
.
players
)
{
println
(
player
)
}
This can be (slightly) simplified by defining an operator
function called iterator
on the Team
. Example 5-31 shows how to do this as an extension function, and the resulting simplified loop.
operator
fun
Team
.
iterator
(
)
:
Iterator
<
Player
>
=
players
.
iterator
(
)
for
(
player
in
team
)
{
println
(
player
)
}
Either way, the output is as follows:
Player(
name
=
Curry)
Player(
name
=
Thompson)
Player(
name
=
Durant)
Player(
name
=
Green)
Player(
name
=
Cousins)
In reality, the idea is to make the Team
class implement the Iterable
interface, which includes the abstract operator
function iterator
. That means the alternative to writing the extension function is to modify Team
, as shown in Example 5-32.
Iterable
interfaceclass
Team
(
val
name
:
String
,
val
players
:
MutableList
<
Player
>
=
mutableListOf
())
:
Iterable
<
Player
>
{
override
fun
iterator
():
Iterator
<
Player
>
=
players
.
iterator
()
// ... other functions as needed ...
}
The result is the same, except that now all the extension functions on Iterable
are available on Team
, so you can write code like that in Example 5-33.
Iterator
extension functions on Team
assertEquals
(
"Cousins, Curry, Durant, Green, Thompson"
,
team
.
map
{
it
.
name
}.
joinToString
())
The map
function here iterates over the players, so it.name
represents each player’s name. Other extension functions can be used in the same way.
Use the extension functions filterIsInstance
or filterIsInstanceTo
.
Collections in Kotlin include an extension function called filter
that takes a predicate, which can be used to extract elements satisfying any boolean condition, as in Example 5-34.
val
list
=
listOf
(
"a"
,
LocalDate
.
now
(),
3
,
1
,
4
,
"b"
)
val
strings
=
list
.
filter
{
it
is
String
}
for
(
s
in
strings
)
{
// s.length // does not compile; type is erased
}
Although the filtering operation works, the inferred type of the strings
variable is List<Any>
, so Kotlin does not smart cast the individual elements to type String
.
You could add an is
check or simply use the filterIsInstance
function instead, as in Example 5-35.
val
list
=
listOf
(
"a"
,
LocalDate
.
now
(),
3
,
1
,
4
,
"b"
)
val
all
=
list
.
filterIsInstance
<
Any
>()
val
strings
=
list
.
filterIsInstance
<
String
>()
val
ints
=
list
.
filterIsInstance
<
Int
>()
val
dates
=
list
.
filterIsInstance
(
LocalDate
::
class
.
java
)
assertThat
(
all
,
`is`
(
list
))
assertThat
(
strings
,
containsInAnyOrder
(
"a"
,
"b"
))
assertThat
(
ints
,
containsInAnyOrder
(
1
,
3
,
4
))
assertThat
(
dates
,
contains
(
LocalDate
.
now
()))
In this case, the filterIsInstance
function uses reified types, so the resulting collections are of a known type, and you don’t have to check the type before using its properties.
The implementation of the filterIsInstance
function in the library is shown here:
public
inline
fun
<
reified
R
>
Iterable
<*>.
filterIsInstance
():
List
<
R
>
{
return
filterIsInstanceTo
(
ArrayList
<
R
>())
}
The reified
keyword applied to an inline
function preserves the type, so the returned type is List<R>
.
The implementation calls the function filterIsInstanceTo
, which takes a collection argument of a particular type and populates it with elements of that type from the original. That function can also be used directly, as in Example 5-36.
val
list
=
listOf
(
"a"
,
LocalDate
.
now
(),
3
,
1
,
4
,
"b"
)
val
all
=
list
.
filterIsInstanceTo
(
mutableListOf
())
val
strings
=
list
.
filterIsInstanceTo
(
mutableListOf
<
String
>())
val
ints
=
list
.
filterIsInstanceTo
(
mutableListOf
<
Int
>())
val
dates
=
list
.
filterIsInstanceTo
(
mutableListOf
<
LocalDate
>())
assertThat
(
all
,
`is`
(
list
))
assertThat
(
strings
,
containsInAnyOrder
(
"a"
,
"b"
))
assertThat
(
ints
,
containsInAnyOrder
(
1
,
3
,
4
))
assertThat
(
dates
,
contains
(
LocalDate
.
now
()))
The argument to the filterIsInstanceTo
function is a MutableCollection<in R>
, so by specifying the type of the desired collection, you populate it with the instances of that type.
Create a progression of your own.
In Kotlin, a range is created when you use the double dot operator, as in 1..5
, which instantiates an IntRange
. A range is a closed interval, defined by two endpoints that are both included in the range.
The standard library adds an extension function called rangeTo
to any generic type T
that implements the Comparable
interface. Example 5-37 provides its implementation.
rangeTo
function for Comparable
typesoperator
fun
<
T
:
Comparable
<
T
>>
T
.
rangeTo
(
that
:
T
):
ClosedRange
<
T
>
=
ComparableRange
(
this
,
that
)
The class ComparableRange
simply extends Comparable
, defines start
and endInclusive
properties of type T
, and overrides equals
, hashCode
, and toString
functions appropriately. The return type on rangeTo
is ClosedRange
, which is a simple interface defined in Example 5-38.
ClosedRange
interfaceinterface
ClosedRange
<
T
:
Comparable
<
T
>>
{
val
start
:
T
val
endInclusive
:
T
operator
fun
contains
(
value
:
T
):
Boolean
=
value
>=
start
&&
value
<=
endInclusive
fun
isEmpty
():
Boolean
=
start
>
endInclusive
}
The operator function contains
lets you use the in
infix function to check whether a value is contained inside the range.
All this means that you can create a range based on any class that implements Comparable
, and the infrastructure to support it is already there. As an example, for java.time.LocalDate
, see Example 5-39.
LocalDate
in a range@Test
fun
`
LocalDate
in
a
range
`
()
{
val
startDate
=
LocalDate
.
now
()
val
midDate
=
startDate
.
plusDays
(
3
)
val
endDate
=
startDate
.
plusDays
(
5
)
val
dateRange
=
startDate
..
endDate
assertAll
(
{
assertTrue
(
startDate
in
dateRange
)
},
{
assertTrue
(
midDate
in
dateRange
)
},
{
assertTrue
(
endDate
in
dateRange
)
},
{
assertTrue
(
startDate
.
minusDays
(
1
)
!
in
dateRange
)
},
{
assertTrue
(
endDate
.
plusDays
(
1
)
!
in
dateRange
)
}
)
}
That’s all well and good, but the surprising part comes when you try to iterate over the range:
for
(
date
in
dateRange
)
println
(
it
)
// compiler error!
(
startDate
..
endDate
).
forEach
{
/* ... */
}
// compiler error!
The problem is that a range is not a progression. A progression is simply an ordered sequence of values. Custom progressions implement the Iterable
interface, just as the existing progressions IntProgression
, LongProgression
, and CharProgression
in the standard library do.
To demonstrate how to create a progression, consider the classes in Example 5-40 and Example 5-41.
The code in this example is based on the 2017 DZone article by Grzegorz Ziemoński entitled, “What Are Kotlin Progressions and Why Should You Care?”
First, here is the LocalDateProgression
class, which implements both Iterable<LocalDate>
and ClosedRange<LocalDate>
interfaces.
LocalDate
import
java.time.LocalDate
class
LocalDateProgression
(
override
val
start
:
LocalDate
,
override
val
endInclusive
:
LocalDate
,
val
step
:
Long
=
1
)
:
Iterable
<
LocalDate
>,
ClosedRange
<
LocalDate
>
{
override
fun
iterator
():
Iterator
<
LocalDate
>
=
LocalDateProgressionIterator
(
start
,
endInclusive
,
step
)
infix
fun
step
(
days
:
Long
)
=
LocalDateProgression
(
start
,
endInclusive
,
days
)
}
From the Iterator
interface, the only function that must be implemented is iterator
. Here it instantiates the class LocalDateProgressionIterator
, shown next. The infix step
function instantiates the class with the proper increment in days. The ClosedRange
interface, as shown in the preceding code, defines the start
and endInclusive
properties, so they are overridden here in the primary constructor.
LocalDateProgression
classimport
java.time.LocalDate
internal
class
LocalDateProgressionIterator
(
start
:
LocalDate
,
val
endInclusive
:
LocalDate
,
val
step
:
Long
)
:
Iterator
<
LocalDate
>
{
private
var
current
=
start
override
fun
hasNext
()
=
current
<=
endInclusive
override
fun
next
():
LocalDate
{
val
next
=
current
current
=
current
.
plusDays
(
step
)
return
next
}
}
The Iterator
interface requires overriding next
and hasNext
, as shown.
Finally, use an extension function to redefine the rangeTo
function to return an instance of the progression:
operator
fun
LocalDate
.
rangeTo
(
other
:
LocalDate
)
=
LocalDateProgression
(
this
,
other
)
Now LocalDate
can be used to create a range that can be iterated over, as shown in the tests in Example 5-42.
LocalDate
progression@Test
fun
`
use
LocalDate
as
a
progression
`
()
{
val
startDate
=
LocalDate
.
now
()
val
endDate
=
startDate
.
plusDays
(
5
)
val
dateRange
=
startDate
..
endDate
dateRange
.
forEachIndexed
{
index
,
localDate
->
assertEquals
(
localDate
,
startDate
.
plusDays
(
index
.
toLong
()))
}
val
dateList
=
dateRange
.
map
{
it
.
toString
()
}
assertEquals
(
6
,
dateList
.
size
)
}
@Test
fun
`
use
LocalDate
as
a
progression
with
a
step
`
()
{
val
startDate
=
LocalDate
.
now
()
val
endDate
=
startDate
.
plusDays
(
5
)
val
dateRange
=
startDate
..
endDate
step
2
dateRange
.
forEachIndexed
{
index
,
localDate
->
assertEquals
(
localDate
,
startDate
.
plusDays
(
index
.
toLong
()
*
2
))
}
val
dateList
=
dateRange
.
map
{
it
.
toString
()
}
assertEquals
(
3
,
dateList
.
size
)
}
Using the double dot operator creates a range, which in this case supports iteration, which is used by the forEachIndexed
function. In this example, creating a progression requires two classes and an extension function, but the pattern is easy enough to replicate for your own classes.
18.224.51.145