S-JIS[2014-07-06/2018-02-04] 変更履歴

java.util.Map

Javaコレクションのひとつであるマップについて。


概要

マップは、キーとなるオブジェクト(よく使われるのは文字列や列挙型)に対し、それに該当する値を保持するコレクション。
他言語のマップ(ディクショナリー・連想配列)


マップは、java.util.Mapインターフェースで表される。

Mapは(ListSetとは異なり)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);
java.util LinkedHashMap 1.4 並び順(追加した順序やアクセス順)が保持されるHashMap。
LinkedHashMapの例
Map<String, String> map = new LinkedHashMap<String, String>(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 { 〜 }
Map<MyEnum, String> map = new EnumMap<MyEnum, String>(MyEnum.class);
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の説明
 
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 = 〜;
Map<String, String> umap = Collections.unmodifiableMap(map);
UnmodifiableSortedMap 1.2 不変SortedMap。 SortedMap<String, String> map = 〜;
SortedMap<String, String> umap = Collections.unmodifiableSortedMap(map);
UnmodifiableNavigableMap 1.8 不変NavigableMap。 NavigableMap<String, String> map = 〜;
NavigableMap<String, String> umap = Collections.unmodifiableNavigableMap(map);
java.util.
ImmutableCollections
AbstractImmutableMap
Map0, Map1, MapN
9 不変Map[2018-06-02]
Map.ofMap.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);
map.put("a", "foo");
map.put("b", "bar");
map.put("c", "zzz");
普通にputする方法。
初期化子   Map<String, String> map = new HashMap<String, String>() {{
  put("a", "foo");
  put("b", "bar");
  put("c", "zzz");
}};
インスタンス初期化子を使う方法。
実務的には使用すべきではないだろう。
Stream 1.8 Map<String, String> map = Stream.of("a:foo", "b:bar", "c:zzz").
  collect(Collectors.toMap(s -> s.split(":")[0], s -> s.split(":")[1]));
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<String, String> map = Map.ofEntries(entry("a", "foo"), entry("b", "bar"), entry("c", "zzz"));
ofEntries.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
Map<K, V>
V put K key,
V value
  指定されたキーで値を保持する。
変更前に保持していた値が返る。
Map<String, String> map1 = new LinkedHashMap<>();
map1.put("a", "foo");
map1.put("b", "bar");
map1.put("c", "zzz");
{a=foo, b=bar, c=zzz}
java.util
Map<K, V>
V putIfAbsent K key,
V value
1.8 指定されたキーで値を保持していない(あるいはnullを保持している)場合に、新しい値を保持する。
変更前に保持していた値が返る。
(変更しなかった場合は、保持していた値がそのまま返る)
replace()computeIfAbsent()
Map<String, String> map2 = new LinkedHashMap<>(map1);
map2.putIfAbsent("b", "hoge");
map2.putIfAbsent("d", "hage");
{a=foo, b=bar, c=zzz, d=hage}
java.util
Map<K, V>
void putAll Map<K, V> map   渡されたマップの全要素を自分のマップに追加する。 Map<String, String> map2 = new LinkedHashMap<>();
map2.putAll(map1);
{a=foo, b=bar, c=zzz}
java.util
Map<K, V>
V get Object key   指定されたキーの値を取得する。
無かった場合はnullを返す。
String b = map1.get("b");
String d = map1.get("d");
bar
null
java.util
Map<K, V>
V getOrDefault Object key,
V defaultValue
1.8 指定されたキーの値を取得する。
無かった場合は指定されたデフォルト値を返す。
String b = map1.getOrDefault("b", "hoge");
String d = map1.getOrDefault("d", "hoge");
bar
hoge
java.util
Map<K, V>
Set<K> keySet     キーの一覧(Set)を取得する。 for(String key : map1.keySet()){
  System.out.println(key);
}
a
b
c
java.util
Map<K, V>
Collection<V> values     値の一覧(Collection)を取得する。 for (String value : map1.values()) {
  System.out.println(value);
}
foo
bar
zzz
java.util
Map<K, V>
Set<Map.Entry<K, V>> entrySet     キーと値(のペア)の一覧(Set)を取得する。 for (Entry<String, String> entry : map1.entrySet()) {
  System.out.printf("%s: %s%n", entry.getKey(), entry.getValue());
}
a: foo
b: bar
c: zzz
java.util
Map<K, V>
void forEach (K, V) -> void action 1.8 全要素を順次処理する。
引数として、処理を行う関数を渡す。
map1.forEach((key, value) -> System.out.printf("%s: %s%n", key, value)); a: foo
b: bar
c: zzz
java.util
Map<K, V>
int size     保持しているキーの個数を返す。 int size = map1.size(); 3
java.util
Map<K, V>
boolean isEmpty     空Map(要素を保持していない)の場合、trueを返す。 boolean empty = map1.isEmpty(); false
java.util
Map<K, V>
boolean containsKey Object key   指定されたキーを保持している場合、trueを返す。 boolean exists = map1.containsKey("b"); true
java.util
Map<K, V>
boolean containsValue Object value   指定された値を保持している場合、trueを返す。 boolean exists = map1.containsValue("bar"); true
java.util
Map<K, V>
void clear     全要素を削除する。 map2.clear(); {}
java.util
Map<K, V>
V remove Object key   キーを削除する。
削除前に保持していた値が返る。
Map<String, String> map2 = new LinkedHashMap<>(map1);
map2.remove("b");
{a=foo, c=zzz}
java.util
Map<K, V>
boolean remove Object key,
Object value
1.8 保持していた値と指定した値が等しい場合にキーを削除する。
実際に削除した場合はtrueが返る。
Map<String, String> map2 = new LinkedHashMap<>(map1);
map2.remove("a", "aaa");
map2.remove("b", "bar");
 
{a=foo, c=zzz}
java.util
Map<K, V>
boolean replace K key,
V oldValue,
V newValue
1.8 指定されたキーで保持していた値がoldValueに等しかった場合にnewValueに置き換える。
実際に置き換えた場合はtrueが返る。
Map<String, String> map2 = new LinkedHashMap<>(map1);
map2.replace("a", "aaa", "AAA");
map2.replace("b", "bar", "BAR");
{a=foo, b=BAR, c=zzz}
java.util
Map<K, V>
boolean replace K key,
V value
1.8 指定されたキーで値を保持していた場合だけ指定された値に置き換える。
置換前の値を返す。
putIfAbsent()
Map<String, String> map2 = new LinkedHashMap<>(map1);
map2.replace("b", "hoge");
map2.replace("d", "hage");
{a=foo, b=hoge, c=zzz}
java.util
Map<K, V>
void replaceAll (K, V) -> V function 1.8 値を置き換える。
引数として、新しい値を返す関数を渡す。
Map<String, String> map2 = new LinkedHashMap<>(map1);
map2.replaceAll((key, value) -> key + value.charAt(0));
{a=af, b=bb, c=cz}
java.util
Map<K, V>
V computeIfAbsent K key,
(K) -> V function
1.8 指定されたキーで値を保持していない(あるいはnullを保持している)場合に、functionの結果を保持する。
functionがnullを返したら何もしない。
最終的に保持している値を返す。
Map<String, String> map2 = new LinkedHashMap<>(map1);
map2.computeIfAbsent("b", key -> key.toUpperCase());
map2.computeIfAbsent("d", key -> key.toUpperCase());
{a=foo, b=bar, c=zzz, d=D}
Map<String, List<String>> map = new TreeMap<>();
{
  List<String> list = map.computeIfAbsent("a", key -> new ArrayList<>());
  list.add("a1");
  System.out.println(map);
}
{
  List<String> list = map.computeIfAbsent("a", key -> new ArrayList<>());
  list.add("a2");
  System.out.println(map);
}
{
  List<String> list = map.computeIfAbsent("b", key -> new ArrayList<>());
  list.add("b1");
  System.out.println(map);
}
{a=[a1]}

{a=[a1, a2]}

{a=[a1, a2], b=[b1]}
java.util
Map<K, V>
V computeIfPresent K key,
(K, V) -> V function
1.8 指定されたキーで値を保持している場合に、functionの結果に置換する。
functionがnullを返したら値を削除(remove)する。
最終的に保持している値を返す。
Map<String, String> map2 = new LinkedHashMap<>(map1);
map2.computeIfPresent("b", (key, value) -> key + value.charAt(0));
map2.computeIfPresent("d", (key, value) -> key + value.charAt(0));
{a=foo, b=bb, c=zzz}
java.util
Map<K, V>
V compute K key,
(K, V) -> V function
1.8 functionの結果を保持する。
functionがnullを返したら値を削除(remove)する(最初から値が無ければ何もしない)。
最終的に保持している値を返す。
Map<String, String> map2 = new LinkedHashMap<>(map1);
map2.compute("b", (key, value) -> key + value);
map2.compute("d", (key, value) -> key + value);
{a=foo, b=bbar, c=zzz, d=dnull}
java.util
Map<K, V>
V merge K key,
V value
(V, V) -> V function
1.8 指定されたキーで値を保持していない場合は、valueを保持する。
保持している場合は、その値とvalueを引数とする関数の結果に置換する。
最終的に保持している値を返す。
Map<String, String> map2 = new LinkedHashMap<>(map1);
map2.merge("b", "hoge", (oldValue, newValue) -> oldValue + "+" + newValue);
map2.merge("d", "hage", (oldValue, newValue) -> oldValue + "+" + newValue);
{a=foo, b=bar+hoge, c=zzz, d=hage}
java.util.Map
Entry<K, V>
K getKey     Entryからキーを取得する。 entry.getKey()  
java.util.Map
Entry<K, V>
V getValue     Entryから値を取得する。 entry.getValue()  
java.util.Map
Entry<K, V>
V setValue V   Entryに値をセットする。
(Entryの元のMapに反映される)
entry.setValue("123")  
java.util.Map
Entry<K, V>
Comparator
<Endtry<K, V>>
comparingByKey   1.8 EntryのキーのComparatorを返す。 entry.stream().sorted(Entry.comparingByKey()).forEach(System.out::println); A=2
a=3
z=1
java.util.Map
Entry<K, V>
Comparator
<Endtry<K, V>>
comparingByKey Comparator<K> 1.8 EntryのキーのComparatorを返す。 entry.stream().sorted(Entry.comparingByKey(Comparator.reverseOrder()))
.forEach(System.out::println);
z=1
a=3
A=2
java.util.Map
Entry<K, V>
Comparator
<Endtry<K, V>>
comparingByValue   1.8 Entryの値のComparatorを返す。 entry.stream().sorted(Entry.comparingByValue()).forEach(System.out::println); z=1
A=2
a=3
java.util.Map
Entry<K, V>
Comparator
<Endtry<K, V>>
comparingByValue Comparator<V> 1.8 Entryの値のComparatorを返す。 entry.stream().sorted(Entry.comparingByValue(Comparator.reverseOrder())).forEach(System.out::println); a=3
A=2
z=1

クラスやメソッドによっては、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を取得し、それに対して処理を行う。

基本的には、処理される順序に保証は無い。
ただし、一部のクラス(LinkedHashMapTreeMapEnumMap等)では並び順が保証される。


keySet

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構文を使うことが出来る。

values

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構文を使うことが出来る。

値の型を変換する例


entrySet

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メソッドを使うとすっきり書ける。

Listへの変換

MapのキーをListに変換する方法。[2017-01-19]

	List<String> keyList = new ArrayList<>(map.keySet());

Mapの値をListに変換する方法。

	List<String> valueList = new ArrayList<>(map.values());

Streamとの変換

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の要素を元にしてを生成する関数」を渡す。


java.util.HashMap

HashMapは、キーの検索にハッシュコードを利用して効率を良くしたマップ。[2007-12-22]
HashMap内でのハッシュコードの使われ方

キーにはequals()hashCode()が定義されていれば どんなオブジェクト(クラス)でも使えるが、不変であることが望ましい(というか、不変でないとまずそう)。
StringInteger・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]


java.util.LinkedHashMap

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として利用できる。


java.util.TreeMap

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を渡すと、その順序になる。

TreeMapのキーのnull

普通、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.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倍なので、必ず空のエントリーがある)


コレクションへ戻る / Java目次へ戻る / 新機能へ戻る / 技術メモへ戻る
メールの送信先:ひしだま