1. Overview

in and !in are two Kotlin operators. In this tutorial, we’ll explore the common usage of these operators. Also, we’ll discuss when and how to overload the in operator through examples.

2. Introduction to in and !in Operators

in and !in are binary operators. First, the in operator can be used in a for loop while iterating over a collection:

for (item in someList) {
    print(item)
}

We won’t discuss much about using in in for loops in this tutorial.

The in operator is also designed to check for membership of one variable within the other. For example, item in someList” returns true if someList contains item. Conversely, “item !in someList” verifies whether item is not in someList. It complements the in operator, offering a straightforward means of confirming absence within a container.

Under the hood, in and !in are backed by the contains() function:

  • x in y -> y.contains(x)
  • x !in y  -> !y.contains(x)

In this tutorial, we’ll see some typical in and !in usages in our daily work and learn how to make our own types support these operators.

For simplicity, we’ll use unit test assertions to verify the result.

3. Common Usages

We’ve learned that the in operator is designed for containment checks. So next, let’s explore how these operators can be applied in real-world scenarios.

3.1. Collections and Arrays

Collections and arrays are common container data structures holding elements. in and !in can be used to determine whether a particular value is present in a collection or an array.

For example, we can check if a string Set contains or doesn’t contain a string:

val mySet = setOf("a", "b", "c", "d", "e")
assertTrue { "a" in mySet }
assertTrue { "x" !in mySet }

Similarly, we can apply the same operation on a string array:

val myArray = arrayOf("a", "b", "c", "d", "e")
assertTrue { "a" in myArray }
assertTrue { "x" !in myArray }

3.2. Ranges

In Kotlin, builtin Range types support the in and !in operators by default:

val myRange = 1..100
assertTrue { 42 in myRange }
assertTrue { 777 !in myRange }

As we can see, we can use in and !in to check if a value belongs to a range straightforwardly.

3.3. Strings

Additionally, we can use in and !in to check whether a string contains a particular substring or character:

val myString = "a b c d e"
assertTrue { "a b" in myString } //substring check
assertTrue { 'c' in myString } // character check
assertTrue { "X" !in myString }

4. Examples of Overloading in and !in

Kotlin allows us to provide custom implementations for the predefined set of operators on types. To do that, we provide a member function or an extension function with the operator modifier and the specific name for the predefined operator. This is called operator overloading.

So far, we’ve seen how in and !in are used with built-in types, such as Collections, Arrays, Ranges, and StringsSometimes, we may want to use in and !in with types that don’t support these operators.

So, in this section, we’ll see a couple of examples and learn how to use operator overloading to achieve that.

4.1. Players and Matches

Let’s say we have three data classes:

data class Player(val name: String, val rank: Int)
data class Team(val name: String, val players: Set<Player>)
data class Match(val place: String, val teams: Pair<Team, Team>)

As the code shows, a Team contains a set of Players. Further, two Teams participate in a Match. Next, let’s initialize some Players and Teams:

val eric = Player("Eric", 8)
val kai = Player("Kai", 7)
val teamA = Team("Team A", setOf(eric, kai))

val kevin = Player("Kevin", 6)
val saajan = Player("Saajan", 11)
val teamB = Team("Team B", setOf(kevin, saajan))

val tom = Player("Tom", 1)
val jerry = Player("Jerry", 9)
val teamC = Team("Team C", setOf(tom, jerry))

Then, let’s create a few Matches with Team Pairs:

val match1 = Match("Frankfurt", teamA to teamC)
val match2 = Match("Hamburg", teamB to teamC)
val match3 = Match("Berlin", teamA to teamB)

To check whether a Player has registered a Match, for example, if eric has registered match1, we must walk through match1 -> teamA and teamB -> Players and then check if eric is present in players.

If we could perform this check in this way: “eric in match1“, the code would be easier to read and understand. But our Match type doesn’t support the in operator.

Using operator overloading, we can make Match support the in and !in operators. We’ve learned that the in operator is backed by the contains() function: x in y -> y.contains(x)

Therefore, let’s create the Match.contains() extension function to overload the in operator:

operator fun Match.contains(player: Player): Boolean {
   return teams.toList().any { player in it.players }
}

Then, we can check if a Player is “in” a Match as we expected:

assertTrue { eric in match1 }
assertTrue { saajan !in match1 }

assertFalse { kai in match2 }
assertTrue { tom !in match3 }

Next, let’s see another example.

4.2. Determining the Pattern

Let’s say we have three predefined Regex patterns:

val pattern33 = Regex("^[a-z]{3} - [a-z]{3}$")
val pattern34 = Regex("^[a-z]{3} - [a-z]{4}$")
val pattern44 = Regex("^[a-z]{4} - [a-z]{4}$")

Now, we want to determine which regex pattern it matches for a given input string. We can straightforwardly implement the logic using if-else blocks:

fun determinePattern1(input: String): String {
    return if (input.matches(pattern33)) {
        "3-3"
    } else if (input.matches(pattern34)) {
        "3-4"
    } else if (input.matches(pattern44)) {
        "4-4"
    } else {
        "none"
    }
}

Some may think about replacing the if-else expression with Kotlin’s when expression to improve the readability:

fun determinePattern2(input: String): String {
    return when {
        input.matches(pattern33) -> "3-3"
        input.matches(pattern34) -> "3-4"
        input.matches(pattern44) -> "4-4"
        else -> "none"
    }
}

It’s cleaner than the if-else approach. However, there are still duplicate function calls. Next, let’s improve the implementation further.

First, let’s overload the in operator by extending the Regex class with the contains() function:

operator fun Regex.contains(input: String): Boolean {
    return this.matches(input)
}

This overloading allows us to check if an input string matches a Regex pattern in this way: “input in pattern”. Thus, we can use the in operator directly in the when block:

fun determinePattern(input: String): String {
    return when (input) {
        in pattern33 -> "3-3"
        in pattern34 -> "3-4"
        in pattern44 -> "4-4"
        else -> "none"
    }
}

As we can see, the code is pretty easy to read and understand now.

Finally, let’s write a test to verify if determinePattern() does the job:

assertEquals("3-3", determinePattern("abc - xyz"))
assertEquals("3-4", determinePattern("top - down"))
assertEquals("4-4", determinePattern("good - nice"))
assertEquals("none", determinePattern("1234 - 4321"))

5. Conclusion

In this article, we learned what the in and !in operators do. Also, we discussed the typical usage of these operators. Furthermore, we saw how to overload the in operator through examples.

As always, the complete source code for the examples is available over on GitHub.

Comments are closed on this article!