Mapの基本概念と特徴
Mapは、Javaでデータを管理する際に非常によく使われるデータ構造の一つです。
「キーと値のペア」という形でデータを保存することで、辞書のように「言葉を調べたら意味が出てくる」ような使い方ができます。
ここでは、Mapとは何か、他のコレクション(ListやSet)との違い、そして代表的な実装クラスについて学んでいきましょう。
Mapの概要と役割
Mapは、一意な(重複しない)キーに対して値を関連付けて保存する仕組みです。
例えば、動物の名前(キー)と年齢(値)を管理したい場合を考えてみましょう。「ライオン」というキーに対して「5歳」という値を結びつけて保存できます。
この仕組みの便利なところは、大量のデータの中から、キーを使って目的の値を瞬時に取り出せることです。
Mapの注意点として、次の2つの重要なルールがあります。
- キーは重複できない(同じキーは1つだけ)
- 値は重複してもいい(複数のキーが同じ値を持てる)
実際のコード例を見てみましょう。
出力結果
{ライオン=5, ゾウ=10}この例では、Map<String, Integer>という形で「キーはString型(文字列)、値はInteger型(整数)」と宣言しています。
その後、put()メソッドで、キーと値のペアを追加しています。
List・Setとの違い
JavaにはMap以外にも、データをまとめて扱うコレクションがあります。コレクションとは、複数の要素(データ)をひとまとまりにして管理するためのオブジェクトのことです。
データを扱うための「入れ物」だと考えるとわかりやすいと思います。
それぞれの違いを下記に整理しましたので、ここで理解しておきましょう。
それぞれの特徴
- List:順番を保持し、重複を許可する。インデックス(番号)でデータにアクセスする
- Set:順番は保持せず、重複を許可しない。値そのものでデータを管理する
- Map:キーと値のペアで管理。キーは重複不可、値は重複できる
アクセス方法の違い
- List:list.get(0) のようにインデックスでアクセス
- Set:特定の値が含まれるかを確認する用途が多い
Map:map.get("ライオン") のようにキーでアクセス
出力結果
List: [ライオン, ゾウ]
Set: [ライオン, ゾウ]
Map: {ライオン=肉食動物, ゾウ=草食動物}Listは順番通りに表示され、Setも要素を保持し、Mapはキーと値のペアで表示されています。
それぞれの用途に応じて使い分けることが大切です。
Mapの主な実装クラス
Javaには、Mapを実装した3つの主要なクラスがあります。
それぞれの特徴を確認しておきましょう。
HashMap(最も一般的)
最も高速で、通常はこれを使います。ただし、要素の順序は保証されません。データを追加した順番と、取り出す順番が異なる場合があります。
TreeMap(自動ソート)
キーを自動的にソート(並び替え)して保存します。文字列なら辞書順(あいうえお順、ABC順)、数値なら小さい順に自動的に並びます。
LinkedHashMap(順序を保持)
要素を追加した順番を記憶します。データを入れた順番通りに取り出したい場合に使います。
出力結果
HashMap: {ライオン=中型, ゾウ=大型} // 順序不定
TreeMap: {ゾウ=大型, ライオン=中型} // キー順ソート
LinkedHashMap: {ゾウ=大型, ライオン=中型} // 挿入順同じデータを入れても、実装クラスによって出力される順序が異なります。
用途に応じて適切なクラスを選ぶことが重要です。
Mapの基本操作
Mapを使いこなすには、基本的な操作方法をしっかり理解する必要があります。
ここでは、要素の追加・取得・削除といった基本操作から、キーや値が存在するかを確認する方法まで、実際のコード例とともにくわしく解説していきます。
このような操作は、Mapを使う上で最も頻繁に使用するので、ここでマスターしてしまいましょう。
要素の追加と上書き(put)
put()メソッドを使って、Mapにキーと値のペアを追加します。
その際、新規追加か変更なのかで挙動が少し異なります。
- 新しいキーの場合:そのまま追加され、nullが返される
- 既存のキーの場合:値が上書きされ、古い値が返される
この戻り値を利用することで、「新規追加なのか、更新なのか」を判断できます。
実際のコード例を見てみましょう。
出力結果
以前の値: null
更新前の値: 200
現在のMap: {ライオン=220}最初のput()では新規追加なのでnullが返されます。
2回目のput()では、既に「ライオン」というキーが存在するため、古い値(200)が返され、新しい値(220)で上書きされます。
要素の取得(get)
get()メソッドを使って、キーに対応する値を取り出します。
もし、存在しないキーを指定するとnullが返されます。
ただし、nullが返された場合、「キーが存在しない」のか「値としてnullが保存されている」のかを区別できません。
くわしくは後述しますが、containsKey()メソッドと組み合わせて使うことで、キーの存在を確認できます。
ちなみに、Java8以降ではgetOrDefault()メソッドを使うと、キーが存在しない場合にデフォルト値を指定できます。
実際のコード例を見てみましょう。
出力結果
ライオンの鳴き声: ガオー
ゾウの鳴き声: null
イヌの鳴き声: ワンワン「ライオン」は値が存在するので「ガオー」が取得できます。「ゾウ」は値としてnullが保存されているのでnullが返されます。
「イヌ」はキー自体が存在しないので、getOrDefault()で指定したデフォルト値「ワンワン」が返されます。
要素の削除(remove)
remove()メソッドを使って、Mapから要素を削除することができます。キーのみを指定する方法と、キーと値の両方を指定する方法があります。
- キーのみ指定:remove(キー) → 該当するキーの要素を削除する
- キーと値を両方指定:remove(キー, 値) → 組み合わせが完全一致する場合のみ削除する
キーのみの場合は削除された値が返され、キーと値を両方指定した場合は削除の成否(true/false)が返されます。
実際のコード例を見てみましょう。
出力結果
削除された値: サバンナ
条件削除の結果: false
残りのMap: {ペンギン=南極}「ライオン」はキーのみを指定して削除したので成功し、削除された値「サバンナ」が返されます。
「ペンギン」は値として「北極」を指定しましたが、実際の値は「南極」なので一致せず、削除されないためfalseが返されるという結果になります。
キーや値の確認(containsKey・containsValue)
キーや値が存在するかを確認するには、専用のメソッドを使います。
- containsKey(キー):指定したキーが存在するか確認する
- containsValue(値):指定した値が存在するか確認する
containsKey()はハッシュという仕組みを使うため非常に高速ですが、containsValue()はすべての値を順番に確認するため時間がかかります。
実際のコード例を見てみましょう。
出力結果
チーターがいるか: true
時速100kmの動物がいるか: true
ライオンがいるか: false「チーター」というキーは存在するのでtrue、値として100は存在するのでtrue、「ライオン」というキーは存在しないのでfalseが返されます。
ちなみに、これらのメソッドは条件分岐で「存在する場合のみ処理を実行する」といった場面でよく使われます。
Mapの繰り返し処理
Mapに保存されたデータをすべて処理したいという場面には、よく遭遇します。
例えば、「すべての動物の名前と年齢を表示したい」といったケースです。
Mapの繰り返し処理には、キーだけを取り出す方法、値だけを取り出す方法、キーと値の両方を取り出す方法など、いくつかのパターンがあります。
ここでは、それぞれの方法を具体的なコード例とともに解説します。
keySet・entrySet・valuesの基本
Mapから要素を取り出すやり方として、主に次の3つの方法があります。
- keySet():キーだけを取り出す
- values():値だけを取り出す
- entrySet():キーと値のペアを取り出す
どれを使うべきかは、次のようなケースに応じて判断してください。
- キーだけが必要な場合 → keySet()
- 値だけが必要な場合 → values()
- キーと値の両方が必要な場合 → entrySet()
実際のコード例を見てみましょう。
出力結果(例)
キー: [ニワトリ, イヌ]
値: [2, 4]
エントリー: [ニワトリ=2, イヌ=4]keySet()を使うと動物の名前だけ、values()を使うと足の本数だけ、entrySet()を使うと両方がセットで取得できているのが確認できますね。
拡張for文によるMapのループ処理
拡張for文(for-each文)を使うと、Mapの要素を簡潔に繰り返し処理できます。
entrySet()と組み合わせることで、キーと値の両方に効率的にアクセスできます。この方法は記述が簡単で読みやすいため、基本的なループ処理に最適です。
実際のコード例を見てみましょう。
出力結果
キリンは黄色
ゾウは灰色entry.getKey()でキー(動物の名前)を取得し、entry.getValue()で値(色)を取得しています。この書き方は、すべての要素を順番に処理したい場合に非常に便利です。
ちなみに、拡張for文の基本的な書き方は
という形です。
この構文を使うことで、コレクション内の要素を順番に取り出して処理できます。
Iteratorを使ったループ処理
Iterator(反復子)を使うと、ループ中に安全に要素を削除できます。
拡張for文でループ中に要素を削除しようとすると、エラーが発生する可能性があります。しかし、Iteratorのremove()メソッドを使えば、安全に削除できます。
出力結果
削除後のMap: {ゾウ=5}このプログラムでは、年齢が10歳より大きい動物を削除しています。「ライオン」は15歳なので削除され、「ゾウ」は5歳なので残るというわけです。
条件に基づいて要素を削除したい場合は、この方法を使いましょう。
Stream APIによるモダンなMap操作
Java8以降では、Stream APIを使ってより洗練された方法でMapを操作できます。
Stream APIを使うと、filter()(フィルタリング)、map()(変換)、forEach()(繰り返し処理)などのメソッドをつなげて、複雑な処理も簡潔に記述できます。ラムダ式と組み合わせることで、読みやすく保守性の高いコードが書けます。
実際のコード例を見てみましょう。
出力結果
ライオン: 100この例では、filter()を使ってサイズが50より大きい動物だけを抽出し、forEach()でその該当する動物を表示しています。
「アリ」はサイズが1なので表示されず、「ライオン」だけが表示されます。Stream APIは、条件に合う要素だけを処理したい場合に非常に便利です。
Mapのソートと順番の制御
Mapを使っていると、要素の順番が気になることがあるかもしれません。Mapの実装クラスによって、要素の順序の扱い方が異なるためです。
- 順序をまったく保証しない
- 追加した順番を保持する
- 自動的にソートする
などといった特徴の違いがあります。
ここでは、各実装クラスの順序に関する特性と、Stream APIを使った柔軟なソート方法について解説します。
Mapの順序と格納順の違い
HashMapはパフォーマンスを優先するため、要素の格納順序を保持しません。
内部では「ハッシュ値」という仕組みを使ってデータを効率的に管理しています。そのため、要素を追加した順番と、取り出される順番が異なります。
「順序は気にしないが、とにかく速く動作してほしい」という場合に最適です。
実際のコード例を見てみましょう。
出力結果(例)
HashMapの順序:
ウサギ: 草食
クマ: 雑食
ライオン: 肉食このように、HashMapでは追加した順番は保持されません。
JVMのバージョンや実行環境によって、上記の出力結果と変わる可能性もあります。
LinkedHashMapによる順序保持
LinkedHashMapは、要素を追加した順番を記憶しておくMapです。
HashMapの高速性を維持しながら、内部的に追加順序を記録します。そのため、要素を取り出す際は、追加した順番通りに取得できます。
「処理速度も重要だが、順序も保ちたい」という場合に使います。
実際のコード例を見てみましょう。
出力結果
LinkedHashMapの順序:
ゾウ: 3000kg
ライオン: 200kg
ウサギ: 2kgこのように、取り出すときも追加したときと同じ順序で取得できます。処理結果を元の順序で表示したい場合に便利です。
TreeMapによるキー順ソート
TreeMapは、キーを自動的にソート(並び替え)して保存するMapです。
文字列なら辞書順(あいうえお順、ABC順)、数値なら昇順(小さい順)で自動的に並びます。要素を追加するたびに内部的にソートが維持されるため、常に順序が保たれた状態でデータを取得できます。
実際のコード例を見てみましょう。
出力結果
TreeMapの順序(キー順):
カンガルー: オーストラリア
パンダ: 中国
ライオン: アフリカ自動的にソートされていることが確認できますね。
streamでのソート操作
Stream APIのsorted()メソッドを使うと、既存のMapをさまざまな条件でソートできます。
キーでソート、値でソート、逆順ソートなど、柔軟な並び替えが可能です。重要なのは、元のMapは変更されず、ソート結果を新しいMapとして生成できる点です。
これにより、元のデータを安全に保ったまま、さまざまな順序で表示できます。
実際のコード例を見てみましょう。
出力結果
値でソートした結果:
{ウサギ=30, ライオン=120, キリン=500}この例では、値(身長)の小さい順にソートしています。
comparingByValue()で値を基準にソートし、結果をLinkedHashMapに格納することで順序を保持しています。
元のanimalHeightsは変更されず、ソートされた新しいMapが作成されます。
Mapの応用テクニック
基本操作に慣れてきたら、より便利な応用メソッドも使ってみましょう。
Java8以降では、条件付きでの要素追加や、既存値の更新・結合など、便利なメソッドが追加されています。こういったメソッドを使いこなすことで、複雑な処理も簡潔に書けるようになります。実際の開発でよく使われる応用テクニックを紹介します。
値が存在しない場合のみ追加
putIfAbsent()メソッドは、キーが存在しない場合にのみ値を追加します。
既にキーが存在する場合は何もせず、既存の値をそのまま保持します。これにより、「初期値を設定したいが、既に値がある場合は上書きしたくない」という処理を、条件分岐なしで実現できます。
出力結果
既存値: 肉食動物
新規値: null
最終的なMap: {ライオン=肉食動物, ゾウ=草食動物}put()メソッドの挙動を覚えているでしょうか。
新規追加か変更かで、次のように挙動が違いました。
- 新しいキーの場合:そのまま追加され、nullが返される
- 既存のキーの場合:値が上書きされ、古い値が返される
「ライオン」はすでに「肉食動物」という値が存在するため、「大型動物」で上書きされず、既存の値「肉食動物」が返されます。
「ゾウ」は新規追加なのでnullが返され、「草食動物」が追加されるという流れです。
既存値の更新や結合
merge()メソッドは、既存の値と新しい値を組み合わせて更新します。
キーが存在しない場合は新しい値を追加し、存在する場合は指定した関数で値を結合します。カウンターの実装や文字列の連結など、値を累積的に処理したい場合に便利です。
実際のコード例を見てみましょう。
出力結果
ライオン: 5
ゾウ: 5
最終的なMap: {ライオン=5, ゾウ=5}Integer::sumは「足し算をする」という指示です。
「ゾウ」は新規追加なので、そのまま5が設定されます。
このメソッドは、出現回数をカウントする処理でよく使われるので、覚えておきましょう。
Mapの初期化・まとめて生成する方法
Mapを作成する際、毎回put()で一つずつ追加するのは手間がかかります。
そんなとき、Java9以降ではMap.of()メソッドを使うことで、複数の要素をまとめて設定できるので便利です。
このメソッドで作成したMapは、後から要素を追加したり削除したりできません。設定ファイルのような、変更されては困るデータを扱う際に有用です。
また、既にあるリストなどのコレクションから、Mapを作りたい場合もあります。そうした際、Stream APIのCollectors.toMap()を使うと、既存のデータを元に新しいMapを自動生成できます。
出力結果(例)
不変Map: {ライオン=百獣の王, ゾウ=巨体動物, ウサギ=俊敏動物}
名前の長さMap: {ライオン=4, ゾウ=2, ウサギ=3}Map.of()で作成したMapは不変という特徴があるので、後から要素を追加したり削除したりすることはできません。定数として使いたい場合に適しています。
2つ目の例では、動物名のリストから「名前をキー、文字数を値」とするMapを自動生成しています。
よくある質問(Q&A)
Q: HashMapとTreeMapはどう使い分ければよいですか?
A: パフォーマンスを重視し順序が不要ならHashMap、キーを常にソートした状態で保持したいならTreeMapを選択しましょう。HashMapは高速ですが順序は保証されません。TreeMapは自動ソートされますが、挿入・検索に少し時間がかかるという特徴があります。
Q: Mapでnullは値として使用できますか?
A: 基本的に、どのMapでも値としてnullを使用することができます。キーについては、HashMapとLinkedHashMapは1つだけnullキーを許可し、TreeMapはnullキーを許可しません。
クラス |
キーに null |
値に null |
|---|---|---|
HashMap |
1つだけ許可 |
複数許可 |
LinkedHashMap |
1つだけ許可 |
複数許可 |
TreeMap |
許可しない(例外発生) |
複数許可 |
Q: Mapのサイズを事前に指定すべきですか?
A: 要素数が事前にわかっている場合は、初期容量を指定することで性能向上が期待できます。HashMapは容量不足になると内部的に再構築が発生するため、適切な初期容量を設定することで効率が向上します。
Q: 複数のMapを結合するにはどうすればよいですか?
A: putAll()メソッドで別のMapの全要素を追加できます。Java8以降はStream APIを使って複数のMapを結合することも可能です。同じキーが存在する場合は、後から追加された値で上書きされます。
Q: Mapの要素をループ中に削除しても安全ですか?
A: 拡張for文を使ってMapをループ処理している最中に、要素を削除しようとすると問題が発生する可能性があります。
これは、ループ中にMapの構造が変わることで、プログラムが混乱してしまうためです。
安全に削除するにはIteratorのremove()メソッドを使用するか、削除対象のキーを先に収集してからremoveAll()でまとめて削除しましょう。
まとめ
JavaのMapは、キーと値のペアでデータを効率的に管理できる基本的なデータ構造です。
この記事では、Mapの基本概念から実装クラスの違い、基本操作、繰り返し処理、ソート方法、そして応用テクニックまで解説してきました。
Mapが特に活躍するのは、次のような場面です。
Mapが活躍する場面
- キーを使って特定のデータを高速に検索・取得したいとき
- 辞書のような「名前と値」のペアでデータを管理したいとき
- 出現回数のカウントや集計処理を行いたいとき
- 設定情報や変換テーブルを効率的に保持したいとき
最後に、重要なポイントをおさらいしましょう。
重要なポイント
- キーと値のペア構造により高速なデータ検索ができる
- HashMap、TreeMap、LinkedHashMapの特徴
- put()、get()、remove()などの基本操作
- 拡張for文やStream APIを活用すると効率的に繰り返し処理ができる
- putIfAbsent()やmerge()などの応用メソッド
Mapの使い方を理解することで、データ処理の幅が大きく広がります。
特に、最初のうちは基本的なHashMapを使って、put()やget()といった基本操作に慣れることが大切です。
慣れてきたら、順序を保持したい場合はLinkedHashMap、自動ソートが必要な場合はTreeMapというように、用途に応じて使い分けてみてください。
また、キーと値のペアという仕組みは、プログラミングの様々な場面で活用できる重要な概念です。
この記事で学んだ知識をしっかり身に付けることで、より実用的で効率的なJavaプログラムを作れるようになるでしょう。