はじめに
はるか昔にListについての記事を書きましたが、色々あってKotlinの勉強を中々出来ませんでしたが、最近やっと復帰できたのでMapについて今回勉強しました。
環境
Java11 Oracle OpenJDK
IntelliJ
Kotlin 1.31
mapの種類
Listと同じくmutableなMapとreadOnlyなmapの2種類を持っています。
Listもそうですが、immutableなMapについてはstandardな実装とはならずembeddable側に実装されていて自力生成するというよりはmutableなmapをコピーしてimmutableするような使い方が想定されているようです。
(新規作成するofメソッドは空Mapか1つ分のKey-Valueしか渡せません)
また、これから出てくるMapの生成メソッドについて、Element1つの場合はimmutable、可変長引数を使用する場合はreadOnlyになると公式に書いてありましたので、そういう感じです。
mutableMap
まずはmutableなMapについてみていきます。
val map = mutableMapOf(1 to "hogehoge")
KotlinのMutableMap生成メソッドであるmutableMapOf
についてはPair<K,V>
を引数とします(複数指定可)。
そのため、少し書き方が特殊です。
to
については変数を使用して指定することも可能となっています。
val id = 1
val name = "hogehoge"
val map = mutableMapOf(id to name)
なので、このような指定も可能となっています。
生成後の型については1.31現在ではkotlin.collections.LinkedHashMap
に変換しているようです。
LinkedHashMap
以外にもHashMap
にしたい場合のメソッドも存在しています。
val id = 1
val name = "hogehoge"
val map = hashMapOf(id to name)
そもそもLinkedHashMap
を指定するメソッドも存在していたりします。
val id = 1
val name = "hogehoge"
val map = linkedMapOf(id to name)
Mapを操作する
Mapを追加する際はput
が用意されているためこちらを使うのだろうと思っていたらIntelliJでは警告を発していました。
どうやらput
は使用せずにindexing operatorで追加するほうが良いようです。
map[2] = "hogehoge2"
もちろんput
が使用出来ない訳ではありませんが、警告を無視する訳にはいかないので、こちらを覚えておきましょう。
ただし、put
もindexing operatorの指定もキーが重複した場合には上書きするという動作で統一されています。
put
はJavaと同様に戻り値がありますが、必要ない場合に使用するset
も存在していますが、こちらも同様の理由で警告が発します。
上書きしたくない場合はputIfabsent
を使用することで元の値を保持してくれます。
map.putIfAbsent(2, "change hogehoge2")
生成時にPairを扱っているのもあり、Pairからの追加も可能になっています。
ただし、副作用無しで新しいMapを生成するため、きちんと値を受け取ってあげる必要があります。
下の例ではPair1つ受け取っていますが、複数受け取ることも可能です。
val pair = Pair(2, "hogehoge")
map.plus(pair)
削除する場合はremove
で統一されていますが、keyとvalue両方とも指定したりすることも出来るので、より柔軟に削除することが可能になっています。
map.remove(2)
map.remove(2, "hogehoge2")
Mapを判定する
Map内にkeyが存在することの確認としてcontainsKey
の実装されているのと共にvalueの存在確認としてcontainsValue
も実装されているためどちらの存在確認も可能となっています。
Mapの存在チェックやNullチェックについても一揃いしています。
isNullOrEmpty
以外はNullの場合はビルドエラーとなってしまいますので、Nullの状況によって使い分けてあげてください。
map?.isEmpty())
map?.isNotEmpty())
map.isNullOrEmpty()
Map<K,V>?
-> Map<K,V>
への変換用のメソッドも存在しています。Kotlin内部だけなら必要ありませんが、Javaと両立している環境ではあった方が楽です。
map.orEmpty()
他にも特定の条件が存在するか確認したりなんだりとMapの中身を判定するものが存在しているため、やはり柔軟な使用が出来るようになっています。
Mapから値を取り出す
KotlinのMapから値を取り出す方法は様々です。
通常のget
も存在しますが、put
と同様にindexing operatorの使用が推奨されています。
NG : map.get(1)
OK : map[1]
存在しないkeyからの取り出しをするとnull
が返却される仕様となっていますが、getOrDefault
やgetOrElse
でkeyが存在しない場合の振る舞いを決めることが出来ます。
違いは値を返却するか処理(Lambda)を返すかの違いです。
getOrElse
については元のvalueから型を変更しても問題ないため、意味の分からない処理もかけてしまうため注意が必要です。
map.getOrDefault(15, "default")
map.getOrElse(15) {
"hogehoge".length
}
普通に取得する以外にも抽出して値を取得したり変換したりも可能となっていますが、量が多すぎるので割愛します。
さらに色んなものをまとめて取得することも可能となっています。
map.keys
map.values
map.entries
map.size
readOnlyMap
readOnlyのMapを生成するにはmapOf
を使用します。
このメソッドはmutableMapOf
と同じMaps.kt
とMapsJVM.kt
の両方に存在していますが、MapsJVM.kt
の方が使用されます。
Mapを複数個にした場合は前者の方が使用されるようですが、classを確認すると1つでも複数でも同じクラスとして取得されるので、ここの仕組みは良く分からないです。
生成している型はKotlin1.31時点でjava.util.Collections.singletonMap
となっています。
たぶん拡張からだと思うのですが、きちんと複数のkeyを扱うことが出来ます。
val map = mapOf(1 to "hogehoge")
readOnlyという形になっていますが、singletonMapを使用しているため、実質的にImmutableのようです。
そもそもMapについては副作用を与える操作は少ないので、あまり気にはならないかもしれません。
おわりに
JavaのMapもStreamの登場によって便利になった部分も、既にKotlinで実装可能です。
前にgoupingByの記事で書いたような集約をやらせてみましたが、同じように動作することが分かっています。
public static void main(String[] arg) {
List<String> names = List.of("taro", "jiro", "saburo");
List<Name> nameList = names.stream().map(it -> new Name(it, it, it)).collect(Collectors.toList());
nameList.add(new Name("taro", "jiro", "taro"));
Map<String,List<Name>> map = nameList.stream()
.collect(Collectors.groupingBy(it -> it.getName1() + "-" + it.getName3()));
System.out.println(map);
// => {jiro-jiro=[Main.Name(name1=jiro, name2=jiro, name3=jiro)], saburo-saburo=[Main.Name(name1=saburo, name2=saburo, name3=saburo)], taro-taro=[Main.Name(name1=taro, name2=taro, name3=taro), Main.Name(name1=taro, name2=jiro, name3=taro)]}
}
fun main(args : Array<String>) {
val names = listOf("taro", "jiro", "saburo")
val nameList = names.map { Name(it, it, it) }.plus(Name("taro", "jiro", "taro"))
val map = nameList.groupBy { s -> s.name1 + "-" + s.name3 }
println(map)
// -> {taro-taro=[Name(name1=taro, name2=taro, name3=taro), Name(name1=taro, name2=jiro, name3=taro)], jiro-jiro=[Name(name1=jiro, name2=jiro, name3=jiro)], saburo-saburo=[Name(name1=saburo, name2=saburo, name3=saburo)]}
}
data class Name(val name1: String, val name2: String, val name3: String)
やっぱりKotlinって楽だなーとやっていて思いました。
表現力が高いKotlinは機能を知れば知るほど色々表現が出来るようになるので勉強が楽しい点もいいですね。
これからも色々と勉強していきたいです。