
Associative container mapping keys to multiple values, supporting insert/insert_many, flat iteration, customizable hasher and FromIterator conversion — a thin HashMap-backed multimap implementation.
A Kotlin Multiplatform multimap collection (MultiMap<K, V>) that associates each key with an ordered list of multiple values. This library is a faithful 1:1 Kotlin Multiplatform port of the Rust multimap crate.
MultiMap<K, V> is implemented as a lightweight, zero-overhead wrapper around HashMap<K, MutableList<V>>, preserving insertion order for values stored under the same key while providing O(1) average-time key lookup, rich entry-based mutation, flat and grouped iteration views, and built-in serde-kotlin support.
map[key], get(key), getMut(key)) or the entire value list (getVec(key), getVecMut(key)).entry(key) (Entry.Occupied / Entry.Vacant, orInsert, orInsertWith, orInsertVec, orInsertVecWith).iter() / iterMut() (Iter<K, V> / IterMut<K, V> with len() and sizeHint())iterAll() / iterAllMut() (Iterable<Pair<K, MutableList<V>>>)(K, V) pairs across all values: flatIter() / flatIterMut()
serde-kotlin): Implements Serialize and provides multiMapDeserialize and MultiMapVisitor matching upstream serde.rs.XCFramework packaging.multimap-kotlin is published to Maven Central under io.github.kotlinmania:multimap-kotlin.
kotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.kotlinmania:multimap-kotlin:0.1.4")
}
}
}dependencies {
implementation("io.github.kotlinmania:multimap-kotlin:0.1.4")
}[versions]
multimap = "0.1.4"
[libraries]
multimap-kotlin = { module = "io.github.kotlinmania:multimap-kotlin", version.ref = "multimap" }import io.github.kotlinmania.multimap.MultiMap
import io.github.kotlinmania.multimap.multimapOf
fun main() {
val map = MultiMap<String, Int>()
map.insert("key1", 42)
map.insert("key1", 1337)
map.insert("key2", 2332)
check(map["key1"] == 42)
check(map.get("key1") == 42)
check(map.getVec("key1") == listOf(42, 1337))
check(map.isVec("key1"))
check(!map.isVec("key2"))
}Create an empty MultiMap, pre-allocate capacity, build one with multimapOf, or construct one from key-value iterables:
import io.github.kotlinmania.multimap.MultiMap
import io.github.kotlinmania.multimap.multimapOf
// Empty multimap
val empty = MultiMap<String, String>()
// Pre-allocated capacity
val withCap = MultiMap.withCapacity<String, Int>(20)
// Builder function
val breeds = multimapOf(
"dog" to "husky",
"dog" to "retriever",
"dog" to "shiba inu",
"cat" to "siamese",
)
// From an iterable of Pair<K, V>
val fromPairs = MultiMap.fromIter(
listOf("a" to 1, "a" to 2, "b" to 3)
)
// From an iterable of Pair<K, List<V>>
val fromVectors = MultiMap.fromIteratorVec(
listOf("a" to listOf(1, 2), "b" to listOf(3))
)val map = MultiMap<String, Int>()
// Insert a single value for a key
map.insert("scores", 10)
map.insert("scores", 20)
// Insert multiple values from an Iterable<V>
map.insertMany("scores", listOf(30, 40))
// Insert multiple values from a Collection<V> slice
map.insertManyFromSlice("scores", listOf(50, 60))
// Extend with Pair<K, V> elements or another MultiMap
map.extend(listOf("bonus" to 5, "scores" to 70))
// Extend with Pair<K, List<V>> elements
map.extendVec(listOf("bonus" to listOf(10, 15)))MultiMap distinguishes between accessing the first value associated with a key and accessing the entire vector of values for that key:
val map = multimapOf(
"host" to "10.0.0.1",
"host" to "10.0.0.2",
"port" to "8080",
)
// Key presence and counts
check(map.containsKey("host"))
check(map.len() == 2) // number of distinct keys
check(!map.isEmpty())
// First value for a key
val primaryHost: String? = map.get("host") // "10.0.0.1"
val indexedHost: String = map["host"] // "10.0.0.1" (throws NoSuchElementException if missing)
// All values for a key
val allHosts: List<String>? = map.getVec("host") // ["10.0.0.1", "10.0.0.2"]
// Check whether a key has more than one value
check(map.isVec("host")) // true (2 values)
check(!map.isVec("port")) // false (1 value)
// Mutate the value list in place
map.getVecMut("host")?.add("10.0.0.3")
// Remove all values for a key and return them
val removedPorts: MutableList<String>? = map.remove("port") // ["8080"]
// Retain only matching (key, value) pairs; keys whose value lists become empty are removed automatically
map.retain { key, value -> key == "host" && value != "10.0.0.1" }Use entry(key) for in-place inspection and insertion without redundant hash lookups:
import io.github.kotlinmania.multimap.Entry
import io.github.kotlinmania.multimap.MultiMap
val map = MultiMap<String, Int>()
// Insert default first value if vacant, and return the mutable list of values remains accessible via orInsertVec
map.entry("counter").orInsert(1)
map.entry("counter").orInsertVec(mutableListOf(10, 20)).add(30)
check(map.getVec("counter") == listOf(1, 30))
// Pattern match on Entry.Occupied vs Entry.Vacant
when (val e = map.entry("counter")) {
is Entry.Occupied -> {
println("First value: ${e.entry.get()}")
e.entry.getVecMut().add(40)
}
is Entry.Vacant -> {
e.entry.insert(0)
}
}val map = multimapOf(
"k1" to 10,
"k1" to 20,
"k2" to 30,
)
// 1. Iterate over keys and their FIRST value: Pair<K, V>
for ((key, firstValue) in map.iter()) {
println("$key -> first: $firstValue")
}
// 2. Iterate over keys and their COMPLETE value list: Pair<K, MutableList<V>>
for ((key, valueList) in map.iterAll()) {
println("$key -> all: $valueList")
}
// 3. Iterate over every flattened (key, value) pair: Pair<K, V>
// Produces ("k1", 10), ("k1", 20), ("k2", 30)
for ((key, value) in map.flatIter()) {
println("$key -> $value")
}
// 4. Direct for-in over MultiMap yields Pair<K, MutableList<V>>
for ((key, values) in map) {
println("$key has ${values.size} value(s)")
}MultiMap<K, V> implements io.github.kotlinmania.serdecore.ser.Serialize directly and provides multiMapDeserialize and MultiMapVisitor for serde-kotlin deserialization, serializing as a map from K to List<V>:
import io.github.kotlinmania.multimap.MultiMap
import io.github.kotlinmania.multimap.multiMapDeserialize
import io.github.kotlinmania.serdecore.de.CharDeserialize
import io.github.kotlinmania.serdecore.de.UByteDeserialize
import io.github.kotlinmania.serdetest.Token
import io.github.kotlinmania.serdetest.assertTokens
val map = MultiMap<Char, UByte>()
map.insert('x', 1u)
map.insert('x', 5u)
assertTokens(
value = map,
deserialize = multiMapDeserialize(CharDeserialize, UByteDeserialize),
tokens = arrayOf(
Token.Map(1),
Token.Char('x'),
Token.Seq(2),
Token.U8(1u),
Token.U8(5u),
Token.SeqEnd,
Token.MapEnd,
),
)| Category | Targets |
|---|---|
| JVM |
jvm (JDK 21 toolchain) |
| Android | Android KMP library (compileSdk 34, minSdk 24), androidNativeArm64, androidNativeX64
|
| Apple (macOS / iOS / tvOS / watchOS) |
macosArm64, iosArm64, iosSimulatorArm64, iosX64, tvosArm64, tvosSimulatorArm64, watchosArm64, watchosDeviceArm64, watchosSimulatorArm64 (includes Swift Export + Multimap XCFramework) |
| Linux |
linuxX64, linuxArm64
|
| Windows | mingwX64 |
| Web (JS & Wasm) |
js (Browser & Node.js), wasmJs (Browser & Node.js), wasmWasi (Node.js) |
# Run the full multiplatform test suite (JVM, Android host, macOS, JS, WasmJS, WasmWASI, and Swift Export)
./gradlew test
# Build all configured Kotlin Multiplatform targets, XCFramework, Swift Export package, Detekt, and Ktlint
./gradlew buildLicensed under either of:
at your option.
This Kotlin Multiplatform library is a port of the original Rust multimap crate (crates.io/crates/multimap, docs.rs/multimap).
Special thanks to Håvar Nøvik (@havarnov) and all contributors to havarnov/multimap for designing and maintaining the original Rust implementation.
Kotlin Multiplatform port maintained by Sydney Renee and The Solace Project for KotlinMania.
A Kotlin Multiplatform multimap collection (MultiMap<K, V>) that associates each key with an ordered list of multiple values. This library is a faithful 1:1 Kotlin Multiplatform port of the Rust multimap crate.
MultiMap<K, V> is implemented as a lightweight, zero-overhead wrapper around HashMap<K, MutableList<V>>, preserving insertion order for values stored under the same key while providing O(1) average-time key lookup, rich entry-based mutation, flat and grouped iteration views, and built-in serde-kotlin support.
map[key], get(key), getMut(key)) or the entire value list (getVec(key), getVecMut(key)).entry(key) (Entry.Occupied / Entry.Vacant, orInsert, orInsertWith, orInsertVec, orInsertVecWith).iter() / iterMut() (Iter<K, V> / IterMut<K, V> with len() and sizeHint())iterAll() / iterAllMut() (Iterable<Pair<K, MutableList<V>>>)(K, V) pairs across all values: flatIter() / flatIterMut()
serde-kotlin): Implements Serialize and provides multiMapDeserialize and MultiMapVisitor matching upstream serde.rs.XCFramework packaging.multimap-kotlin is published to Maven Central under io.github.kotlinmania:multimap-kotlin.
kotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.kotlinmania:multimap-kotlin:0.1.4")
}
}
}dependencies {
implementation("io.github.kotlinmania:multimap-kotlin:0.1.4")
}[versions]
multimap = "0.1.4"
[libraries]
multimap-kotlin = { module = "io.github.kotlinmania:multimap-kotlin", version.ref = "multimap" }import io.github.kotlinmania.multimap.MultiMap
import io.github.kotlinmania.multimap.multimapOf
fun main() {
val map = MultiMap<String, Int>()
map.insert("key1", 42)
map.insert("key1", 1337)
map.insert("key2", 2332)
check(map["key1"] == 42)
check(map.get("key1") == 42)
check(map.getVec("key1") == listOf(42, 1337))
check(map.isVec("key1"))
check(!map.isVec("key2"))
}Create an empty MultiMap, pre-allocate capacity, build one with multimapOf, or construct one from key-value iterables:
import io.github.kotlinmania.multimap.MultiMap
import io.github.kotlinmania.multimap.multimapOf
// Empty multimap
val empty = MultiMap<String, String>()
// Pre-allocated capacity
val withCap = MultiMap.withCapacity<String, Int>(20)
// Builder function
val breeds = multimapOf(
"dog" to "husky",
"dog" to "retriever",
"dog" to "shiba inu",
"cat" to "siamese",
)
// From an iterable of Pair<K, V>
val fromPairs = MultiMap.fromIter(
listOf("a" to 1, "a" to 2, "b" to 3)
)
// From an iterable of Pair<K, List<V>>
val fromVectors = MultiMap.fromIteratorVec(
listOf("a" to listOf(1, 2), "b" to listOf(3))
)val map = MultiMap<String, Int>()
// Insert a single value for a key
map.insert("scores", 10)
map.insert("scores", 20)
// Insert multiple values from an Iterable<V>
map.insertMany("scores", listOf(30, 40))
// Insert multiple values from a Collection<V> slice
map.insertManyFromSlice("scores", listOf(50, 60))
// Extend with Pair<K, V> elements or another MultiMap
map.extend(listOf("bonus" to 5, "scores" to 70))
// Extend with Pair<K, List<V>> elements
map.extendVec(listOf("bonus" to listOf(10, 15)))MultiMap distinguishes between accessing the first value associated with a key and accessing the entire vector of values for that key:
val map = multimapOf(
"host" to "10.0.0.1",
"host" to "10.0.0.2",
"port" to "8080",
)
// Key presence and counts
check(map.containsKey("host"))
check(map.len() == 2) // number of distinct keys
check(!map.isEmpty())
// First value for a key
val primaryHost: String? = map.get("host") // "10.0.0.1"
val indexedHost: String = map["host"] // "10.0.0.1" (throws NoSuchElementException if missing)
// All values for a key
val allHosts: List<String>? = map.getVec("host") // ["10.0.0.1", "10.0.0.2"]
// Check whether a key has more than one value
check(map.isVec("host")) // true (2 values)
check(!map.isVec("port")) // false (1 value)
// Mutate the value list in place
map.getVecMut("host")?.add("10.0.0.3")
// Remove all values for a key and return them
val removedPorts: MutableList<String>? = map.remove("port") // ["8080"]
// Retain only matching (key, value) pairs; keys whose value lists become empty are removed automatically
map.retain { key, value -> key == "host" && value != "10.0.0.1" }Use entry(key) for in-place inspection and insertion without redundant hash lookups:
import io.github.kotlinmania.multimap.Entry
import io.github.kotlinmania.multimap.MultiMap
val map = MultiMap<String, Int>()
// Insert default first value if vacant, and return the mutable list of values remains accessible via orInsertVec
map.entry("counter").orInsert(1)
map.entry("counter").orInsertVec(mutableListOf(10, 20)).add(30)
check(map.getVec("counter") == listOf(1, 30))
// Pattern match on Entry.Occupied vs Entry.Vacant
when (val e = map.entry("counter")) {
is Entry.Occupied -> {
println("First value: ${e.entry.get()}")
e.entry.getVecMut().add(40)
}
is Entry.Vacant -> {
e.entry.insert(0)
}
}val map = multimapOf(
"k1" to 10,
"k1" to 20,
"k2" to 30,
)
// 1. Iterate over keys and their FIRST value: Pair<K, V>
for ((key, firstValue) in map.iter()) {
println("$key -> first: $firstValue")
}
// 2. Iterate over keys and their COMPLETE value list: Pair<K, MutableList<V>>
for ((key, valueList) in map.iterAll()) {
println("$key -> all: $valueList")
}
// 3. Iterate over every flattened (key, value) pair: Pair<K, V>
// Produces ("k1", 10), ("k1", 20), ("k2", 30)
for ((key, value) in map.flatIter()) {
println("$key -> $value")
}
// 4. Direct for-in over MultiMap yields Pair<K, MutableList<V>>
for ((key, values) in map) {
println("$key has ${values.size} value(s)")
}MultiMap<K, V> implements io.github.kotlinmania.serdecore.ser.Serialize directly and provides multiMapDeserialize and MultiMapVisitor for serde-kotlin deserialization, serializing as a map from K to List<V>:
import io.github.kotlinmania.multimap.MultiMap
import io.github.kotlinmania.multimap.multiMapDeserialize
import io.github.kotlinmania.serdecore.de.CharDeserialize
import io.github.kotlinmania.serdecore.de.UByteDeserialize
import io.github.kotlinmania.serdetest.Token
import io.github.kotlinmania.serdetest.assertTokens
val map = MultiMap<Char, UByte>()
map.insert('x', 1u)
map.insert('x', 5u)
assertTokens(
value = map,
deserialize = multiMapDeserialize(CharDeserialize, UByteDeserialize),
tokens = arrayOf(
Token.Map(1),
Token.Char('x'),
Token.Seq(2),
Token.U8(1u),
Token.U8(5u),
Token.SeqEnd,
Token.MapEnd,
),
)| Category | Targets |
|---|---|
| JVM |
jvm (JDK 21 toolchain) |
| Android | Android KMP library (compileSdk 34, minSdk 24), androidNativeArm64, androidNativeX64
|
| Apple (macOS / iOS / tvOS / watchOS) |
macosArm64, iosArm64, iosSimulatorArm64, iosX64, tvosArm64, tvosSimulatorArm64, watchosArm64, watchosDeviceArm64, watchosSimulatorArm64 (includes Swift Export + Multimap XCFramework) |
| Linux |
linuxX64, linuxArm64
|
| Windows | mingwX64 |
| Web (JS & Wasm) |
js (Browser & Node.js), wasmJs (Browser & Node.js), wasmWasi (Node.js) |
# Run the full multiplatform test suite (JVM, Android host, macOS, JS, WasmJS, WasmWASI, and Swift Export)
./gradlew test
# Build all configured Kotlin Multiplatform targets, XCFramework, Swift Export package, Detekt, and Ktlint
./gradlew buildLicensed under either of:
at your option.
This Kotlin Multiplatform library is a port of the original Rust multimap crate (crates.io/crates/multimap, docs.rs/multimap).
Special thanks to Håvar Nøvik (@havarnov) and all contributors to havarnov/multimap for designing and maintaining the original Rust implementation.
Kotlin Multiplatform port maintained by Sydney Renee and The Solace Project for KotlinMania.