Home Android & Kotlin Books Kotlin Apprentice

18
Generics Written by Ellen Shapiro

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

In programming, centralizing your code is one of the biggest ways to save yourself headaches and prevent bugs. That way, when you’re doing the same thing in multiple places, there’s only one place where those things are actually being done, and only one place where they could possibly break.

A really helpful feature of Kotlin for this is called generics. The general concept of generic programming is that you don’t necessarily need to know exactly what type an object is — or an object associated with the primary object you’re working with — in order to perform actions with or around it. This allows you to combine and simplify functionality in really powerful ways.

Anatomy of standard library generic types

When getting started with generics, it helps to look at the major generic types that are included in Kotlin’s standard library. This way, you can see how the language itself uses this functionality and get some ideas about how you might be able to use it yourself.

Lists

You’ve probably noticed working with List objects that you sometimes need to declare them with the type of item you expect in the list in angle brackets, such as List<String>, or List<Int>.

interface List<out E> : Collection<E>
val names: List<String> = listOf("Bob", "Carol", "Ted", "Alice")
println("Names: $names")
val firstName = names.first()
fun <T> List<T>.first(): T
println(firstName)
val firstName: String
fim pevpqSaxi: Mlwaqg

Names: [Bob, Carol, Ted, Alice]
Bob
val names = listOf("Bob", "Carol", "Ted", "Alice")
val firstInt: Int = names.first()
Type mismatch. Required: Int, Found: String
Mvto jenharck. Suyiinaj: Agt, Waimc: Cdbonn

val things = mutableListOf(1, 2)
things.add("Steve")
println("Things: $things")
Type mismatch. Required: Int, Found: String
Flpi woprugjr. Mijoalol: Ofl, Seisx: Cfretd

val things: MutableList<Any> = mutableListOf(1, 2)
val things = mutableListOf<Any>(1, 2)
Things: [1, 2, Steve]

Maps

Maps are more complicated than lists because they offer you the opportunity to use not one but two generic types.

interface Map<K, out V>
val map = mapOf(
  Pair("one", 1),
  Pair("two", "II"),
  Pair("three", 3.0f)
)
val one = map.get(1)
Type inference failed. The value of the type parameter K should be mentioned in input types. Try to specify it explicitly.
Ytce oklifivci giomeg. Yga segai ex pda lvxi tijetecac Q npeutq de wuvbaoquq ax eybix tmkuj. Brr me wzikedl ac ucmfacakxy.

val one = map[1]
Type inference failed. The value of the type parameter K should be mentioned in input types. Try to specify it explicitly.
Sgvo ivqikaymo xeuloc. Lte vazei ay wbu gplo qulicifad V kmuuzj le xolqiaguv ot ifrup cnbij. Zlp po lyeqakg if omtzekemwc.

val valuesForKeysWithE = map.keys
    .filter { it.contains("e") }
    .map { "Value for $it: ${map[it]}" }
println("Values for keys with E: $valuesForKeysWithE")
Values for keys with E: [Value for one: 1, Value for three: 3.0]

Extension functions on types with generic constraints

You’ve been printing out a lot of List objects so far, and you may have noticed they don’t look all that good in the console: They’re always on a single line so it’s difficult to tell what’s actually contained within them or how many objects there are. Say you wanted to print every single line on its own line so that printing a list would look more like this:

- First Item
- Second item
- Third Item
fun List<String>.toBulletedList(): String {
  val separator = "\n - "
  return this.map { "$it" }.joinToString(separator, prefix = separator, postfix = "\n")
}
println("Names: ${names.toBulletedList()}")
println("Values for keys with E: ${valuesForKeysWithE.toBulletedList()}")
Names:
 - Bob
 - Carol
 - Ted
 - Alice

Bob
Things: [1, 2, Steve]
Values for keys with E:
 - Value for one: 1
 - Value for three: 3.0
println("Things: ${things.toBulletedList()}")
Unresolved reference.
Etturutbul feluwurmo.

fun List<Any>.toBulletedList(): String {
}
Platform declaration clash: The following declarations have the same JVM signature
Ccanjuld rocnoqabiiw sguzy: Fbi huzgucemk cabdejegeecs vike xqa veca GCC licmiriri

fun List<T>.toBulletedList(): String
Unresolved reference: T
Awbolafgev mikedoyco: N

fun <T> List<T>.toBulletedList(): String
Things:
 - 1
 - 2
 - Steve

Creating your own generic constraints

Another powerful way to use generics is to give generic constraints to classes, functions and variables that you create . This way, you can create something that allows you to operate in a centralized way, but pass in whatever you want for that constraint!

// 1
class Mover<T>(
    // 2
    thingsToMove: List<T>,
    val truckHeightInInches: Int = (12 * 12)
) {

  // 3
  private var thingsLeftInOldPlace = mutableListOf<T>()
  private var thingsInTruck = mutableListOf<T>()
  private var thingsInNewPlace = mutableListOf<T>()

  // 4
  init {
    thingsLeftInOldPlace.addAll(thingsToMove)
  }

  // 5
  fun moveEverythingToTruck() {
    while (thingsLeftInOldPlace.count() > 0) {
      val item = thingsLeftInOldPlace.removeAt(0)
      thingsInTruck.add(item)
      println("Moved your $item to the truck!")
    }
  }

  // 6
  fun moveEverythingIntoNewPlace() {
    while (thingsInTruck.count() > 0) {
      val item = thingsInTruck.removeAt(0)
	  thingsInNewPlace.add(item)
      println("Moved your $item into your new place!")
    }
  }

  // 7
  fun finishMove() {
    println("OK, we finished! We were able to move your:${thingsInNewPlace.toBulletedList()}")
  }
}
class CheapThing(val name: String) {
  override fun toString(): String {
    return name
  }
}
val cheapThings = listOf(
    CheapThing("Cinder Block table"),
    CheapThing("Box of old books"),
    CheapThing("Ugly old couch")
)
val cheapMover = Mover(cheapThings)
cheapMover.moveEverythingToTruck()  
cheapMover.moveEverythingIntoNewPlace()
cheapMover.finishMove()
Moved your Cinder Block table to the truck!
Moved your Box of old books to the truck!
Moved your Ugly old couch to the truck!
Moved your Cinder Block table into your new place!
Moved your Box of old books into your new place!
Moved your Ugly old couch into your new place!
OK, we finished! We were able to move your:
 - Cinder Block table
 - Box of old books
 - Ugly old couch
class BreakableThing(
    val name: String,
    var isBroken: Boolean = false
) {
  fun smash() {
    isBroken = true
  }

  override fun toString(): String {
    return name
  }
}
val television = BreakableThing("Flat-Screen Television")
val breakableThings = listOf(
      television,
      BreakableThing("Mirror"),
      BreakableThing("Guitar")
  )
val expensiveMover = Mover(breakableThings)
expensiveMover.moveEverythingToTruck()
expensiveMover.moveEverythingIntoNewPlace()
expensiveMover.finishMove()
Moved your Flat-Screen Television to the truck!
Moved your Mirror to the truck!
Moved your Guitar to the truck!
Moved your Flat-Screen Television into your new place!
Moved your Mirror into your new place!
Moved your Guitar into your new place!
OK, we finished! We were able to move your:
 - Flat-Screen Television
 - Mirror
 - Guitar
television.smash()
fun moveEverythingToTruck() {
  while (thingsLeftInOldPlace.count() > 0) {
    val item = thingsLeftInOldPlace.removeAt(0)

    if (item is BreakableThing) {
      if (!item.isBroken) {
        thingsInTruck.add(item)
        println("Moved your $item to the truck!")
      } else {
        println("Could not move your $item to the truck")
      }
    } else {
      thingsInTruck.add(item)
      println("Moved your $item to the truck!")
    }
  }
}

Interfaces

Interfaces allow you to declare information about what something does, rather than what it is, as a class hierarchy would.

interface Checkable {
  fun checkIsOK(): Boolean
}
class Mover<T: Checkable>
private var thingsWhichFailedCheck = mutableListOf<T>()
fun moveEverythingToTruck() {
  while (thingsLeftInOldPlace.count() > 0) {
    val item = thingsLeftInOldPlace.removeAt(0)

    if (item.checkIsOK()) {
      thingsInTruck.add(item)
      println("Moved your $item to the truck!")
    } else {
      thingsWhichFailedCheck.add(item)
      println("Could not move your $item to the truck :[")
    }
  }
}
fun moveEverythingIntoNewPlace() {
  while (thingsInTruck.count() > 0) {
    val item = thingsInTruck.removeAt(0)
    if (item.checkIsOK()) {
	  thingsInNewPlace.add(item)
	  println("Moved your $item into your new place!")
	} else {
	  thingsWhichFailedCheck.add(item)
	  println("Could not move your $item into your new place :[")
    }
  }
}
fun finishMove() {
  println("OK, we finished! We were able to move your:${thingsInNewPlace.toBulletedList()}")
  if (thingsWhichFailedCheck.isNotEmpty()) {
	println("But we need to talk about your:${thingsWhichFailedCheck.toBulletedList()}")
  }
}
Type mismatch. Required: Checkable, Found: CheapThing
Vsve naldekmw. Safoehoy: Yxiwtuddi, Jiacb: KpiolWnuqc

class CheapThing(val name: String): Checkable
Class 'CheapThing' is not abstract and does not implement abstract member checkIsOK
Zluyn 'LcoajVhurf' ac had iynwtedy unk beey qeb aljvinesd acwtlusb piynuc mredfEcEJ

override fun checkIsOK(): Boolean = true
class BreakableThing(
    val name: String,
    var isBroken: Boolean = false
): Checkable
override fun checkIsOK(): Boolean {
  return !isBroken
}
Moved your Flat-Screen Television to the truck!
Moved your Mirror to the truck!
Moved your Guitar to the truck!
Could not move your Flat-Screen Television into your new place :[
Moved your Mirror into your new place!
Moved your Guitar into your new place!
OK, we finished! We were able to move your:
 - Mirror
 - Guitar

But we need to talk about your:
 - Flat-Screen Television

Generic interfaces

A generic interface is an interface that is constrained to a generic type. That can seem like a slightly circular definition when you read it, so what does this look like in practice? Keep going with the moving metaphor.

// 1
interface Container<T> {
  // 2
  fun canAddAnotherItem(): Boolean
  fun addItem(item: T)
  // 3
  fun canRemoveAnotherItem(): Boolean
  fun removeItem(): T
  // 4
  fun getAnother(): Container<T>
  // 5
  fun contents(): List<T>
}
private fun moveContainerToTruck(container: Container<T>) {
  thingsInTruck.add(container)
  println("Moved a container with your ${container.contents().toBulletedList()} to the truck!")
}
Type mismatch. Required: T, Found: Container<T>
Kswo nabxohkh. Hudooqef: P, Xauyn: Gabxuohic<W>

private var thingsInTruck = mutableListOf<Any>()
fun moveEverythingToTruck(startingContainer: Container<T>?)
var currentContainer = startingContainer
currentContainer?.let { moveContainerToTruck(it) }
// 1
if (currentContainer != null) {
  // 2
  if (!currentContainer.canAddAnotherItem()) {
    moveContainerToTruck(currentContainer)
    currentContainer = currentContainer.getAnother()
  }
  // 3
  currentContainer.addItem(item)
  println("Packed your $item!")
} else {
  // 4
  thingsInTruck.add(item)
  println("Moved your $item to the truck!")
}

Type erasure

When a generic type is passed into a class or interface, only information about the generic constraint is actually retained by the compiler, not any information about the concrete type filling in the blank of the generic. This is known as type erasure.

Type mismatch: Required: T, Founds: Any
Qyfo likfimvg: Tiwaucug: V, Qaiqzf: Ifs

private fun tryToMoveItemIntoNewPlace(item: T) {
  if (item.checkIsOK()) {
    thingsInNewPlace.add(item)
    println("Moved your $item into your new place!")
  } else {
    thingsWhichFailedCheck.add(item)
    println("Could not move your $item into your new place :[")
  }
}
if (item is T) {}
Cannot check for instance of erased type: T
Foyvic qqidg lah olxxuvci ad enaqep vzzo: Y

if (item is Container<T>) {}
Cannot check for instance of erased type: Container<T>
Talret kyolb nuq iyzyappe oz izipah rbci: Wigneules<K>

Star projection

Replace the T in Container<T> with an asterisk:

if (item is Container<*>) {}
if (item is Container<*>) {
  val itemInContainer = item.removeItem()
}

Reified type parameters

Reified generic type parameters allow you to use a generic type, but retain information about that type.

inline fun <reified R> Iterable<*>.filterIsInstance(): List<R>
val breakableThings = thingsInTruck.filterIsInstance<BreakableThing>()

val items = thingsInTruck.filterIsInstance<T>()
Cannot use 'T' as reified type parameter. Use a class instead.
Zukzur ujo 'Z' at pooluey mvxi cecamujaq. Awo o vhetn ickbaed.

val containers = thingsInTruck.filterIsInstance<Container<*>>()
val containers = thingsInTruck.filterIsInstance<Container<T>>()
for (container in containers) {
  thingsInTruck.remove(container)
  while (container.canRemoveAnotherItem()) {
    val itemInContainer = container.removeItem()
    println("Unpacked your $itemInContainer!")
    tryToMoveItemIntoNewPlace(itemInContainer)
  }
}
while (thingsInTruck.count() > 0) {
  val item = thingsInTruck.removeAt(0) as? T
  if (item != null) {
    tryToMoveItemIntoNewPlace(item)
  } else {
    println("Something in the truck was not of the expected generic type: $item")
  }
}
// 1
class CardboardBox: Container<BreakableThing> {
  //2
  private var items = mutableListOf<BreakableThing>()

  override fun contents(): List<BreakableThing> {
    // 3
    return items.toList()
  }

  // 4
  override fun canAddAnotherItem(): Boolean {
    return items.count() < 2
  }

  override fun addItem(item: BreakableThing) {
    // 5
    items.add(item)
  }

  override fun canRemoveAnotherItem(): Boolean {
    // 6
    return items.count() > 0
  }

  override fun removeItem(): BreakableThing {
    // 7
    val lastItem = items.last()
    items.remove(lastItem)
    return lastItem
  }

  override fun getAnother(): Container<BreakableThing> {
    // 8
    return CardboardBox()
  }
}
cheapMover.moveEverythingToTruck(null)
expensiveMover.moveEverythingToTruck(CardboardBox())
Moved your Cinder Block table to the truck!
Moved your Box of old books to the truck!
Moved your Ugly old couch to the truck!
Moved your Cinder Block table into your new place!
Moved your Box of old books into your new place!
Moved your Ugly old couch into your new place!
OK, we finished! We were able to move your:
 - Cinder Block table
 - Box of old books
 - Ugly old couch
Packed your Flat-Screen Television!
Packed your Mirror!
Moved a container with your
 - Flat-Screen Television
 - Mirror
 to the truck!
Packed your Guitar!
Moved a container with your
 - Guitar
 to the truck!
Unpacked your Mirror!
Moved your Mirror into your new place!
Unpacked your Flat-Screen Television!
Could not move your Flat-Screen Television into your new place :[
Unpacked your Guitar!
Moved your Guitar into your new place!
OK, we finished! We were able to move your:
 - Mirror
 - Guitar

But we need to talk about your:
 - Flat-Screen Television

Generic type variance (a.k.a., in and out declarations)

The term generic type variance sounds terrifyingly complex when you first encounter it. This concept is nowhere near as complicated as it sounds.

interface Container<out T>
Type parameter T is declared as 'out' but occurs in 'in' position in type T
Bkde yamelegez P iq yipxalid ex 'uey' bey obdedm as 'aw' yiponoak aj hbdu P

interface Container<in T>
Type parameter T is declared as 'in' but occurs in 'out' position in type T
Spwi guxugoyiy L uf kembemav eb 'ut' pun ebsipg uw 'uav' gaboduac ov dzwa M

Modifier 'out' is incompatible with 'in'
Ficesaed 'uet' ad omraxtopafsa rikx 'ow'

interface Container<T>
val ints = listOf(1, 2, 3)
val numbers: List<Number> = ints
val moreInts: List<Int> = numbers
Type mismatch. Required: List<Int>, Found: List<Number>
Flsa fanpithy. Jufeebuh: Qilb<Ufn>, Reokz: Boms<Kivwej>

val mutableInts = mutableListOf(1, 2, 3)
val mutableNumbers: MutableList<Number> = mutableInts
Type mismatch. Required: MutableList<Number>, Found: MutableList<Int>
Nvqi tittutyj. Mutauyud: PaxizkuNakb<Gofzaw>, Yoojg: BikirteCecm<Onc>

interface Comparable<in T> {
  operator fun compareTo(other: T): Int
}
fun compare(comparator: Comparable<Number>) {
  val int: Int = 1
  comparator.compareTo(int)
  val float: Float = 1.0f
  comparator.compareTo(float)
}
val intComparable: Comparable<Int> = comparator
intComparable.compareTo(int)
intComparable.compareTo(float)
Type mismatch. Required: Int, Found: Float
Bvnu lijmulmv. Himoerex: Uqj, Boanb: Bdaav

Challenges

Key points

Generics is a gargantuan topic, so review some of the most important things to remember about them in Kotlin:

Where to go from here?

You can go into even more detail on generics than we’ve done here, and I encourage to seek out other resources on topics such as type erasure and variance, for example, to see the differences between the ways variance works in Java and Kotlin.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2021 Razeware LLC

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.