S-JIS[2014-07-06/2023-09-24] 変更履歴
|
|
マップは、キーとなるオブジェクト(よく使われるのは文字列や列挙型)に対し、それに該当する値を保持するコレクション。
→他言語のマップ(ディクショナリー・連想配列)
マップは、java.util.Mapインターフェースで表される。
Mapは(ListやSetとは異なり)CollectionやIterableインターフェースを継承していない。
したがって、全要素に対して順次処理を行いたい場合は、キーや値の一覧を取得し、それに対して処理する。
→順次アクセス処理
MapのサブインターフェースとしてSortedMap(キーの並び順が保持されるMap)があり、さらにその下にNavigableMapがある。
SortedMap(NavigableMap)の代表例はTreeMap。
他にConcurrentMapというインターフェースもあって、代表例はConcurrentHashMap。
さらにそのサブインターフェースにConcurrentNavigableMapがあり(これはNavigableMapも継承している)、代表例はConcurrentSkipListMap。
Mapの主な具象クラス。
クラス名 | JDK | 説明 | インスタンスを生成する例 | |
---|---|---|---|---|
java.util |
HashMap |
1.2 | キーのハッシュ値を使って探索するMap。 コンストラクターで初期サイズを指定可能。 →HashMapの構造 |
Map<String, String> map = new HashMap<String, String>(10); |
// Java19 [2022-09-21]Map<String, String> map = HashMap.newHashMap(10); |
||||
java.util |
LinkedHashMap |
1.4 | 並び順(追加した順序やアクセス順)が保持されるHashMap。 →LinkedHashMapの例 |
Map<String, String> map = new LinkedHashMap<String, String>(10); |
// Java19 [2022-09-21]Map<String, String> map = LinkedHashMap.newLinkedHashMap(10); |
||||
java.util |
TreeMap |
1.2 | キーの大小順で並ぶMap。(NavigableMap) →TreeMapの例 |
Map<String, String> map = new TreeMap<String, String>(); |
java.util |
EnumMap |
1.5 | キーを列挙値で保持するMap。 →EnumMapの使用例 |
enum MyEnum { 〜 } |
java.util |
IdentityHashMap |
1.4 | キーの参照を使って判別するMap。 例えばキーに "abc" とnew String("abc") を使った場合、HashMapでは同一と見なされるが、IdentityHashMapでは別物になる。 |
|
java.util |
Hashtable |
1.0 | 古いライブラリー以外で使うことは無い。 位置づけとしてはsynchronizedされたHashMapだが、その目的ならConcurrentHashMapの方が良い。 |
|
java.util |
Properties |
1.0 | プロパティーを保持する為のクラス。 キーと値のペアを保持したプロパティーファイルを扱うことも出来る。 古いクラスなのでHashtableを継承している。 →Propertiesの使用例 |
Properties properties = new Properties(); |
java.util |
WeakHashMap |
1.2 | キーを弱参照で保持するMap。 →WeakHashMapの説明 |
// Java19 [2022-09-21]Map<String, String> map = WeakHashMap.newWeakHashMap(10); |
java.util.Collections |
EmptyMap |
1.2 | 空の不変Map。 | Map map = Collections.EMPTY_MAP; // JDK1.4 |
Map<String, String> map = Collections.emptyMap(); |
||||
EmptyNavigableMap |
1.8 | 空の不変NavigableMap(SortedMap)。 | SortedMap<String, String> map = Collections.emptySortedMap(); |
|
NavigableMap<String, String> map = Collections.emptyNavigableMap(); |
||||
java.util.Collections |
SingletonMap |
1.2 | キーが1つの不変Map。 | Map<String, String> map = Collections.singletonMap("key", "value"); |
java.util.Collections |
UnmodifiableMap |
1.2 | 不変Map。 元となるマップを指定すると、値取得専用のマップになって返ってくる。 このUnmodifiableMapは要素の追加・削除・変更は出来ない。 元となったマップを変更すると、その内容は反映される。 |
Map<String, String> map = 〜; |
UnmodifiableSortedMap |
1.2 | 不変SortedMap。 | SortedMap<String, String> map = 〜; |
|
UnmodifiableNavigableMap |
1.8 | 不変NavigableMap。 | NavigableMap<String, String> map = 〜; |
|
java.util. |
AbstractImmutableMap |
9 | 不変Map。[2018-06-02] Map.ofやMap.ofEntriesメソッドで返されるMap。 |
Map<String, String> map = Map.of("key",
"value"); |
java.util.Collections |
CheckedMap |
1.5 | 動的型保証されるMap。 不正な型のキーや値を入れたらClassCastExceptionを発生させてくれる。 |
|
CheckedSortedMap |
1.5 | 動的型保証されるSortedMap。 | ||
CheckedNavigableMap |
1.8 | 動的型保証されるNavigableMap。 | ||
java.util.Collections |
SynchronizedMap |
1.2 | 同期化(synchronized)されるMap。 | |
SynchronizedSortedMap |
1.2 | 同期化(synchronized)されるSortedMap。 | ||
SynchronizedNavigableMap |
1.8 | 同期化(synchronized)されるNavigableMap。 | ||
java.util.concurrent |
ConcurrentHashMap |
1.5 | マルチスレッドセーフなMap(HashMap相当)。 →MTセーフなコレクション |
|
java.util.concurrent |
ConcurrentSkipListMap |
1.6 | マルチスレッドセーフなNavigableMap(TreeMap相当)。 →MTセーフなコレクション |
配列だと初期値を列挙する初期化構文がある(ScalaのMapにも初期値を指定する方法がある)が、JavaのMapでは構文としては用意されていない。
方法 | JDK | 例 | 説明 |
---|---|---|---|
通常 | Map<String, String> map = new HashMap<>(10); |
普通にputする方法。 | |
初期化子 | Map<String, String> map = new HashMap<String, String>()
{{ |
インスタンス初期化子を使う方法。 実務的には使用すべきではないだろう。 |
|
Stream | 1.8 | Map<String, String> map = Stream.of("a:foo", "b:bar", "c:zzz"). |
Streamを使う方法。 強引過ぎる^^; |
of | 9 | Map<String, String> map = Map.of("a", "foo", "b",
"bar", "c", "zzz"); |
Map.ofメソッドは不変マップを返す。[2017-09-24] キーや要素にnullを指定するとNullPointerExceptionが発生する。 また、キーが重複しているとIllegalArgumentExceptionが発生する。 |
ofEntries | 9 | import
static java.util.Map.entry; |
Map.ofEntriesメソッドはキーと値のペアを並べる。[2017-09-24] Map.entryメソッドでキーと値のペアを作成する。 |
他のマップ から作る場合 |
Map<String, String> map2 = new HashMap<>(map); |
||
10 | Map<String, String> map2 =
Map.copyOf(map); |
Map.copyOfメソッドは不変マップを返す。[2018-06-02] |
Map関連の主なメソッド。
クラス | メソッド | JDK | 説明 | 例 | |
---|---|---|---|---|---|
コーディング | 結果 | ||||
java.util |
V put(K key, V value) |
指定されたキーで値を保持する。 変更前に保持していた値が返る。 |
Map<String, String> map1 = new LinkedHashMap<>(); |
{a=foo, b=bar, c=zzz} |
|
java.util |
V putIfAbsent(K
key, V value) |
1.8 | 指定されたキーで値を保持していない(あるいはnullを保持している)場合に、新しい値を保持する。 変更前に保持していた値が返る。 (変更しなかった場合は、保持していた値がそのまま返る) →replace()・computeIfAbsent() |
Map<String, String> map2 = new LinkedHashMap<>(map1); |
{a=foo, b=bar, c=zzz, d=hage} |
java.util |
V putFirst(K k, V v) |
21 | Mapの先頭に追加する。(SequencedMapのみ) | var map2 =
new LinkedHashMap<>(map1); |
{z=hoge, a=foo, b=bar, c=zzz} |
java.util |
V putLast(K k, V v) |
21 | Mapの末尾に追加する。(SequencedMapのみ) | var map2 =
new LinkedHashMap<>(map1); |
{a=foo, b=bar, c=zzz, z=hoge} |
java.util |
void putAll(Map<K, V> map) |
渡されたマップの全要素を自分のマップに追加する。 | Map<String, String> map2 = new LinkedHashMap<>(); |
{a=foo, b=bar, c=zzz} |
|
java.util |
V get(Object key) |
指定されたキーの値を取得する。 無かった場合はnullを返す。 |
String b = map1.get("b"); |
bar |
|
java.util |
V getOrDefault(Object key,
V defaultValue) |
1.8 | 指定されたキーの値を取得する。 無かった場合は指定されたデフォルト値を返す。 |
String b = map1.getOrDefault("b", "hoge"); |
bar |
java.util |
Map.Entry<K,V>
firstEntry() |
21 | Mapの先頭のEntry(キーと値のペア)を返す。(SequencedMapのみ) | var map2 =
new LinkedHashMap<>(map1); |
a=foo |
java.util |
Map.Entry<K,V> lastEntry() |
21 | Mapの末尾のEntry(キーと値のペア)を返す。(SequencedMapのみ) | var map2 =
new LinkedHashMap<>(map1); |
c=zzz |
java.util |
Set<K> keySet() |
キーの一覧(Set)を取得する。 | for(String key : map1.keySet()){ |
a |
|
java.util |
Collection<V> values() |
値の一覧(Collection)を取得する。 | for (String value : map1.values()) { |
foo |
|
java.util |
Set<Map.Entry<K, V>> entrySet() |
Entry(キーと値のペア)の一覧(Set)を取得する。 | for (Entry<String, String> entry : map1.entrySet()) { |
a: foo |
|
java.util |
void forEach((K, V) -> void action) |
1.8 | 全要素を順次処理する。 引数として、処理を行う関数を渡す。 |
map1.forEach((key, value) -> System.out.printf("%s: %s%n", key,
value)); |
a: foo |
java.util |
SequencedSet<K>
sequencedKeySet() |
21 | キーの一覧(SequencedSet)を取得する。(SequencedMapのみ) | var map2 =
new LinkedHashMap<>(map1); |
[a, b, c] |
java.util |
SequencedCollection<V>
sequencedValues() |
21 | 値の一覧(SequencedCollection)を取得する。(SequencedMapのみ) | var map2 =
new LinkedHashMap<>(map1); |
[foo, bar, zzz] |
java.util |
SequencedSet<Map.Entry<K, V>>
sequencedEntrySet() |
21 | Entry(キーと値のペア)の一覧(SequencedSet)を取得する。(SequencedMapのみ) | var map2 =
new LinkedHashMap<>(map1); |
[a=foo, b=bar, c=zzz] |
java.util |
int size() |
保持しているキーの個数を返す。 | int size = map1.size(); |
3 |
|
java.util |
boolean isEmpty() |
空Map(要素を保持していない)の場合、trueを返す。 | boolean empty = map1.isEmpty(); |
false |
|
java.util |
boolean containsKey(Object key) |
指定されたキーを保持している場合、trueを返す。 | boolean exists = map1.containsKey("b"); |
true |
|
java.util |
boolean containsValue(Object value) |
指定された値を保持している場合、trueを返す。 | boolean exists = map1.containsValue("bar"); |
true |
|
java.util |
void clear() |
全要素を削除する。 | map2.clear(); |
{} |
|
java.util |
V remove(Object key) |
キーを削除する。 削除前に保持していた値が返る。 |
Map<String, String> map2 = new LinkedHashMap<>(map1); |
{a=foo, c=zzz} |
|
java.util |
Map.Entry<K,V>
pollFirstEntry() |
21 | Mapの先頭のEntry(キーと値のペア)を削除して返す。(SequencedMapのみ) | var map2 =
new LinkedHashMap<>(map1); |
a=foo |
java.util |
Map.Entry<K,V>
pollLastEntry() |
21 | Mapの末尾のEntry(キーと値のペア)を削除して返す。(SequencedMapのみ) | var map2 =
new LinkedHashMap<>(map1); |
c=zzz |
java.util |
boolean remove(Object key,
Object value) |
1.8 | 保持していた値と指定した値が等しい場合にキーを削除する。 実際に削除した場合はtrueが返る。 |
Map<String, String> map2 = new LinkedHashMap<>(map1); |
{a=foo, c=zzz} |
java.util |
boolean replace(K key,
V oldValue,
V newValue) |
1.8 | 指定されたキーで保持していた値がoldValueに等しかった場合にnewValueに置き換える。 実際に置き換えた場合はtrueが返る。 |
Map<String, String> map2 = new LinkedHashMap<>(map1); |
{a=foo, b=BAR, c=zzz} |
java.util |
boolean replace(K key,
V value) |
1.8 | 指定されたキーで値を保持していた場合だけ指定された値に置き換える。 置換前の値を返す。 →putIfAbsent() |
Map<String, String> map2 = new LinkedHashMap<>(map1); |
{a=foo, b=hoge, c=zzz} |
java.util |
void replaceAll((K, V) -> V function) |
1.8 | 値を置き換える。 引数として、新しい値を返す関数を渡す。 |
Map<String, String> map2 = new LinkedHashMap<>(map1); |
{a=af, b=bb, c=cz} |
java.util |
V computeIfAbsent(K key, |
1.8 | 指定されたキーで値を保持していない(あるいはnullを保持している)場合に、functionの結果を保持する。 functionがnullを返したら何もしない。 最終的に保持している値を返す。 |
Map<String, String> map2 = new LinkedHashMap<>(map1); |
{a=foo, b=bar, c=zzz, d=D} |
Map<String, List<String>> map = new TreeMap<>(); |
{a=[a1]} |
||||
java.util |
V computeIfPresent(K key, |
1.8 | 指定されたキーで値を保持している場合に、functionの結果に置換する。 functionがnullを返したら値を削除(remove)する。 最終的に保持している値を返す。 |
Map<String, String> map2 = new LinkedHashMap<>(map1); |
{a=foo, b=bb, c=zzz} |
java.util |
V compute(K key, |
1.8 | functionの結果を保持する。 functionがnullを返したら値を削除(remove)する(最初から値が無ければ何もしない)。 最終的に保持している値を返す。 |
Map<String, String> map2 = new LinkedHashMap<>(map1); |
{a=foo, b=bbar, c=zzz, d=dnull} |
java.util |
V merge(K key,
V value, (V, V) -> V function) |
1.8 | 指定されたキーで値を保持していない場合は、valueを保持する。 保持している場合は、その値とvalueを引数とする関数の結果に置換する。 最終的に保持している値を返す。 |
Map<String, String> map2 = new LinkedHashMap<>(map1); |
{a=foo, b=bar+hoge, c=zzz, d=hage} |
java.util |
SequencedMap<K, V>
reversed() |
21 | 逆順のMapを返す。(SequencedMapのみ) | var map2 =
new LinkedHashMap<>(map1); |
{c=zzz, b=bar, a=foo} |
クラス | メソッド | JDK | 説明 | 例 | |
---|---|---|---|---|---|
コーディング | 結果 | ||||
java.util.Map |
K getKey() |
Entryからキーを取得する。 | entry.getKey() |
||
java.util.Map |
V getValue() |
Entryから値を取得する。 | entry.getValue() |
||
java.util.Map |
V setValue(V) |
Entryに値をセットする。 (Entryの元のMapに反映される) |
entry.setValue("123") |
||
java.util.Map |
Comparator |
1.8 | EntryのキーのComparatorを返す。 | entry.stream().sorted(Entry.comparingByKey()).forEach(System.out::println); |
A=2 |
java.util.Map |
Comparator |
1.8 | EntryのキーのComparatorを返す。 | entry.stream().sorted(Entry.comparingByKey(Comparator.reverseOrder())) |
z=1 |
java.util.Map |
Comparator |
1.8 | Entryの値のComparatorを返す。 | entry.stream().sorted(Entry.comparingByValue()).forEach(System.out::println); |
z=1 |
java.util.Map |
Comparator |
1.8 | Entryの値のComparatorを返す。 | entry.stream().sorted(Entry.comparingByValue(Comparator.reverseOrder())).forEach(System.out::println); |
a=3 |
クラスやメソッドによっては、java.lang.UnsupportedOperationExceptionを返す実装もある。
例えばUnmodifiableMapは不変マップなので、値を変更するputやremoveメソッドを呼び出すとUnsupportedOperationExceptionが発生する。
Mapは、キーを指定して値を取得する操作が効率的に行えるようになっている。
処理 | JDK | 例 | 備考 |
---|---|---|---|
値の設定 |
Map<String, String> map = new LinkedHashMap<>(); map.put("a", "foo"); map.put("b", "bar"); map.put("c", "zzz"); |
[/2014-07-06] | |
値の取得 | String value = map.get("b"); |
[2007-03-26] | |
要素が存在しない場合に初期値を使用 | 1.7以前 | String getValue(Map<String, String> map, String key) { String value; if (map.containsKey(key)) { value = map.get(key); } else { value = "defaultValue"; } return value; } |
この方法は、containsKey()とget()で同じ探索が二重に行われることになり、効率が悪い。 (大抵の場合は値が存在しているだろうから) |
1.7以前 | String getValue(Map<String, String> map, String key) { String value = map.get(key); if (value == null && !map.containsKey(key)) { value = "defaultValue"; } return value; } |
containsKey()呼び出しをget()がnullだった場合だけ行うことにより、大抵のケースでは1回の探索で済む。 | |
1.7以前 | String getValue(Map<String, String> map, String key) { String value = map.get(key); if (value == null) { value = "defaultValue"; } return value; } |
値にnullを許容しない(nullが入っているのと値が無いのを同列に見なせる)場合はget()だけで済む。 | |
1.8 | String getValue(Map<String, String> map, String key) { String value = map.getOrDefault(key, "defaultValue"); return value; } |
||
要素が存在しない場合に初期値を設定 | 1.7以前 | void add(Map<String, List<String>> map, String key, String value) { List<String> list = map.get(key); if (list == null) { list = new ArrayList<>(); map.put(key, list); } list.add(value); } |
[2011-04-10] |
1.8 | void add(Map<String, List<String>> map, String key, String value) { List<String> list = map.computeIfAbsent(key, k -> new ArrayList<>()); list.add(value); } |
||
1.8 | void add(Map<String, List<String>> map, String key, String value) { List<String> list = new ArrayList<>(); list.add(value); map.merge(key, list, (oldList, newList) -> { oldList.addAll(newList); return oldList; }); } |
無理やりmergeを使ってみたが、強引な上に効率も悪そうorz |
Mapでは、保持している値(要素)を直接順次処理していくことが出来ない。(JDK1.8では出来るようになった→forEachの利用)
キーのSetや値のCollectionを取得し、それに対して処理を行う。
基本的には、処理される順序に保証は無い。
ただし、一部のクラス(LinkedHashMapやTreeMap・EnumMap等)では並び順が保証される。
Map#keySet()を使うと、キーの一覧を取得することが出来る。[2007-03-26]
キーが取れるのでMap#get()で値を取得することも出来るが、値だけが必要ならvalues()、キーと値の両方を使うならenrtySet()を使う方が良い。
(get()を呼び出すと、そこで値の探索処理が動いてしまうから)
Iteratorパターン | JDK1.4 | for (Iterator i = map.keySet().iterator(); i.hasNext();) { キー型 キー = (キー型) i.next(); 値の型 変数 = (値の型) map.get(キー); 〜 } |
|
---|---|---|---|
JDK1.5 | for (Iterator<キー型> i = map.keySet().iterator(); i.hasNext();) { キー型 キー = i.next(); 値の型 変数 = map.get(キー); 〜 } |
JDK1.5ではジェネリクスが使えるようになった。 | |
for each構文 | JDK1.5 | for (キー型 キー : map.keySet()) { 値の型 変数 = map.get(キー); 〜 } |
SetはIterableを継承しているので、for each構文を使うことが出来る。 |
Map#values()を使うと、値の一覧を取得することが出来る。[2007-03-26]
Iteratorパターン | JDK1.4 | for (Iterator i = map.values().iterator(); i.hasNext();) { 値の型 変数 = (値の型) i.next(); 〜 } |
|
---|---|---|---|
JDK1.5 | for (Iterator<値の型> i = map.values().iterator(); i.hasNext();) { 値の型 変数 = i.next(); 〜 } |
JDK1.5ではジェネリクスが使えるようになった。 | |
for each構文 | JDK1.5 | for (値の型 変数 : map.values()) { 〜 } |
CollectionはIterableを継承しているので、for each構文を使うことが出来る。 |
Map#entrySet()を使うと、キーと値の一覧を取得することが出来る。[2010-02-13]
import java.util.Map.Entry;
Iteratorパターン | JDK1.4 | for (Iterator i = map.entrySet().iterator(); i.hasNext();) { Entry entry = (Entry) i.next(); キー型 キー = (キー型) entry.getKey(); 値の型 変数 = (値の型) entry.getValue(); 〜 } |
|
---|---|---|---|
JDK1.5 | for (Iterator<Entry<キー型, 値の型>> i = map.entrySet().iterator(); i.hasNext();) { Entry<キー型, 値の型> entry = i.next(); キー型 キー = entry.getKey(); 値の型 変数 = entry.getValue(); 〜 } |
JDK1.5ではジェネリクスが使えるようになった。 | |
for each構文 | JDK1.5 | for (Entry<キー型, 値の型> entry : map.entrySet()) { キー型 キー = entry.getKey(); 値の型 変数 = entry.getValue(); 〜 } |
SetはIterableを継承しているので、for each構文を使うことが出来る。 |
forEachメソッド | JDK1.8 | map.forEach((キー, 変数) -> { 〜 }); |
forEachメソッドを使い、ラムダ式で処理を記述する方法。[2014-07-06] |
値の型を変換する例。[2018-02-04]
valuesメソッドは値一覧を取得できるだけで変換は出来ない。
entrySetメソッドでキーと値の両方を取得し、値だけを変換する。
for each構文 | JDK1.5 | Map<K, V1> map = 〜; Map<K, V2> toMap = new HashMap<K, V2>(map.size()); for (Entry<K, V1> entry : map.entrySet()) { V2 value = convert(entry.getValue()); toMap.put(entry.getKey(), value); } |
|
---|---|---|---|
JDK1.8 |
import java.util.AbstractMap.SimpleImmutableEntry; Map<K, V2> toMap = map.entrySet().stream() .map(entry -> new SimpleImmutableEntry<>(entry.getKey(), convert(entry.getValue()))) .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); |
ScalaだとmapValuesというメソッドがあるが、 Java8には無いのでStreamのmapメソッドを使う。 |
|
JDK1.8 |
import static jp.t2v.lab.syntax.MapStreamSyntax.*; Map<K, V2> toMap = map.entrySet().stream() .map(values(MapExample::convert)) .collect(entryToMap()); |
gakuzzzzさんのvalues, entryToMapメソッドを使うとすっきり書ける。 |
MapのキーをListに変換する方法。[2017-01-19]
List<String> keyList = new ArrayList<>(map.keySet());
Mapの値をListに変換する方法。
List<String> valueList = new ArrayList<>(map.values());
Mapから直接Streamを生成することは出来ない。
(ScalaのMapだと、「Mapは“キーと値のペア”の一覧(Seq)」という位置づけでもある為、直接Stream(キーと値の一覧)に変換することが出来る。
しかしJavaのMapはそういう考え方ではない)
entrySet()やkeySet()・values()で他のコレクションに変換し、そこからStreamを生成する。
StreamからMapを生成するにはCollectors.toMapを使用する。
Stream<String> stream = Stream.of("a:foo", "b:bar", "c:zzz");
Map<String, String> map = stream.collect(Collectors.toMap(s -> s.split(":")[0], s -> s.split(":")[1]));
toMapの引数には、「Streamの要素を元にしてキーを生成する関数」と「Streamの要素を元にして値を生成する関数」を渡す。
HashMapは、キーの検索にハッシュコードを利用して効率を良くしたマップ。[2007-12-22]
→HashMap内でのハッシュコードの使われ方
キーにはequals()とhashCode()が定義されていれば
どんなオブジェクト(クラス)でも使えるが、不変であることが望ましい(というか、不変でないとまずそう)。
(StringやInteger・Longといったラッパークラスは不変なので
キーとして使うのに何ら問題ない)
キーが可変(マップに入れた後で値を変更することによって equals()やhashCode()の結果が変わる)だと、以下のような問題が起きる可能性がある。
//可変なキーとするクラス class Key { private String key; public Key(String k) { this.key = k; } public void setKey(String k) { this.key = k; } @Override public boolean equals(Object obj) { //等しいかどうかは、保持している文字列で比較する if (obj instanceof Key) { Key k = (Key) obj; return key.equals(k.key); } return super.equals(obj); } @Override public int hashCode() { //ハッシュ値は、保持している文字列のものを使用する return key.hashCode(); } }
Key k1 = new Key("abc");
Key k2 = new Key("abc");
HashMap<Key, String> map = new HashMap<>();
map.put(k1, "data");
System.out.println("変更前:" + k1.equals(k2) + "/" + (k1.hashCode() == k2.hashCode()));
System.out.println("k1:" + map.get(k1));
System.out.println("k2:" + map.get(k2));
k1.setKey("def"); //キーの中身を変更
System.out.println("変更後:" + k1.equals(k2) + "/" + (k1.hashCode() == k2.hashCode()));
System.out.println("k1:" + map.get(k1));
System.out.println("k2:" + map.get(k2));
実行結果:
変更前:true/true k1:data k2:data 変更後:false/false k1:null k2:null
k1とk2は別インスタンスだが中身は同じなので、変更前はイコール(equals()がtrue)だし、ハッシュコードも等しい。
したがって、ハッシュマップからはk1でもk2でも値を取得することが出来る。
一方、マップに保持しているキーであるk1の内容を変えると、マップ内にk1のデータが残っているにも関わらず、k1で取得できない。
また、k2でも取得できない。
k2はk1と不一致(equals()がfalse)になったので取れなくなっても仕方ない。
k1でも取れなくなったのは、ハッシュ値が変わった為。(ハッシュ値の算出には保持している文字列を使っていたから)
ハッシュマップではキーのハッシュ値を元にハッシュマップ内のテーブル位置を決定する為、ハッシュ値が変わるとテーブル位置の算出結果が元のテーブル位置と異なってしまう(可能性がある)ので、見つけられなくなってしまう。
また、そこが偶然一致したとしても、テーブル位置内にある候補と比較する際にもハッシュ値そのものが使われているので、不一致になる可能性は高い。
試しにハッシュコードのメソッドを以下のように変えてやると、変更後もハッシュ値が変わらなくなるので取得できるようになる。
public int hashCode() { //デフォルトのハッシュ値を返す return super.hashCode(); }
変更前:true/false k1:data k2:null 変更後:false/false k1:data k2:null
しかし見ての通り、キー値の変更前でもk2でデータを取得できなくなってしまった。ハッシュ値が一致しないのだから不思議ではない(運が良ければハッシュ値が一致して取得できただろうが、プログラムが運に頼ってどーする!むしろデバッグ(バグの再現性)の観点からは、一致したら超アンラッキー)。
そして『equals()がtrueの時は同一のハッシュコードを返さなければならない』のがhashCode()の決まりなので、そうでないのは仕様違反!
equals()もデフォルトのもの(インスタンスが等しければtrue)にすれば仕様は満たせるが、他インスタンスは必ず不一致になってしまい、ハッシュマップからは絶対に値が取れなくなる。
結局、マップのキーには不変の値を使った方が安全・安心ということ。
Java8では、HashMapのキーに使うクラスはComparableを実装しておく方が良いらしい。[2015-06-20]
LinkedHashMapは、並び順が保証されているMap。
デフォルトではキーを追加した順番になるが、アクセス順にすることも出来る。
例 | 備考 | ||
---|---|---|---|
コーディング | 実行結果 | ||
追加順 (デフォルト) |
Map<String, String> map = new LinkedHashMap<>(4); map.put("z", "1"); map.put("a", "9"); map.put("b", "2"); map.put("a", "8"); System.out.println(map); |
{z=1, a=8, b=2} |
デフォルトでは、最初にキーが追加された順序となる。 |
アクセス順 | Map<String, String> map = new LinkedHashMap<>(4, 0.75f, true); map.put("z", "1"); map.put("a", "9"); map.put("b", "2"); map.put("a", "8"); System.out.println(map); |
{z=1, b=2, a=8} |
コンストラクターのaccessOrderをtrueにするとアクセス順になる。 最新(最近・最後に)アクセスしたキーが最も後ろになる。 (アクセスとは、putやgetのこと) |
追加順 (上限あり) |
@SuppressWarnings("serial") Map<String, Integer> map = new LinkedHashMap<String, Integer>() { private static final int MAX_ENTRIES = 4; @Override protected boolean removeEldestEntry(Map.Entry<String, Integer> eldest) { return size() > MAX_ENTRIES; } }; for (int i = 0; i < 10; i++) { String key = String.format("%c", 'a' + i); int value = i + 1; map.put(key, value); } System.out.println(map); |
{g=7, h=8, i=9, j=10} |
removeEldestEntryメソッドをオーバーライドすることで、 putやputAllによって値が追加されたときに古いデータを削除することが出来る。 (removeEldestEntryメソッドはデフォルトではfalseを返しており、全く削除しない) 左記の例では要素を4つまで保持するようにしている。 |
アクセス順 (上限あり) |
@SuppressWarnings("serial") Map<String, Integer> map = new LinkedHashMap<String, Integer>(4, 0.75f. true) { private static final int MAX_ENTRIES = 4; @Override protected boolean removeEldestEntry(Map.Entry<String, Integer> eldest) { return size() > MAX_ENTRIES; } }; for (int i = 0; i < 10; i++) { String key = String.format("%c", 'a' + i); int value = i + 1; map.put(key, value); map.get("a"); // 常にaが最新アクセス } System.out.println(map); |
{h=8, i=9, j=10, a=1} |
accessOrderをtrueにしてremoveEldestEntryメソッドで要素数上限を指定するようにすると、 LRU(Least Recently Used:最も使われなかったもの)で削除する(つまり使われたものだけ残す)Mapを作ることが出来る。 これは、キャッシュ用のMapとして利用できる。 |
TreeMapは、キーの順序で並ぶMap。
例 | 備考 | ||
---|---|---|---|
コーディング | 実行結果 | ||
デフォルト | Map<String, String> map = new TreeMap<>(); map.put("z", "1"); map.put("a", "9"); map.put("b", "2"); map.put("a", "8"); System.out.println(map); |
{a=8, b=2, z=1} |
デフォルトでは、キーのクラスはComparableインターフェースを実装している必要がある。 このComparableに従った順序になる。 (Comparableインターフェースを使った順序を「辞書順」というらしい) |
Comparator指定 | Map<String, String> map = new TreeMap<>(Comparator.reverseOrder()); map.put("z", "1"); map.put("a", "9"); map.put("b", "2"); System.out.println(map); |
{z=1, b=2, a=9} |
コンストラクターにComparatorを渡すと、その順序になる。 |
普通、Mapのキーにはnullを使うことが出来るのだが、TreeMapの場合はnullが許される場合と許されない場合がある。[2016-12-29]
条件 | null可否 |
---|---|
デフォルト | nullキー不可 |
nullを許容するComparator | nullキー可 |
nullを許容しないComparator | nullキー不可 |
デフォルトの場合(TreeMapのコンストラクターでComparatorを指定していない場合)は、key1.compareTo(key2)
という比較を行うので、キーがnullだったらNullPointerExceptionが発生する。
TreeMapのコンストラクターでComparatorが指定されている場合は、そのComparatorでcompare(key1, key2)
という呼び出しが行われる。したがって、そのcompareメソッドがnullを許容する作りになっていれば、TreeMap自体もnullキーを許容することになる。
Map.ofメソッド(Map.ofEntriesメソッド)で作られるマップは不変マップである。[2017-09-24]
キーや値にnullを入れる事は出来ない。
不変マップなので、putやremove等の操作を行おうとするとUnsupportedOperationExceptionが発生する。
keySetやvaluesの並び順は不定。
Java9では、実体のクラスはImmutableCollectionsの内部クラスとして定義されている。
キー数が0個のときはMap0、1個のときはMap1、それ以外はMapNというクラスである。
MapNは、キーと値を保持するのに(単純な)一次元配列だけを使用するので、メモリー使用効率はHashMapより良いと思われる。
キーの比較にはhashCodeやequalsを使っている。
putするときは、最初にhashCodeを元にインデックスを決め、配列のそのエントリーが空であればその位置を使う。空でなければ空の位置が見つかるまでインデックスをずらしていく。ハッシュテーブルの伝統的な実装方式のひとつである(笑)
getするときは、最初にhashCodeを元にインデックスを決め、配列のそのエントリーがequalsで一致すればその位置の値を返す。一致しない場合は、一致するか空になるまでインデックスをずらしていく。空の場合は一致するキーが無い。
(配列のエントリー数はキー数の2倍なので、必ず空のエントリーがある)