目的
- Javaのコレクションフレームワークの1つである
Map
について、整理してみます。 - 各クラスやメソッドの詳細な説明はしません。
主要なクラス
- java.util.HashMap
-
- いわゆるハッシュマップ、ハッシュテーブル
- キーとなるオブジェクトは hashCode() と equals() が正しく実装されている必要がある
- java.util.LinkedHashMap
-
- 基本的には HashMap と同じ
- イテレータで返す順番が保証されている(基本は挿入順)
- 順番は「挿入順」のほかに「アクセス順」にできる。LRU方式のキャッシュを作ることもできます。
参考:LRUキャッシュの実装
- java.util.TreeMap
-
- ツリー構造を持つマップ
- キーとなるオブジェクトは java.lang.Comparable を実装しているか、別途 java.util.Comparator を明示的に指定する必要がある。また、その結果と equals() が矛盾しないこと。
- イテレータで返す順番が保証されている(ソート順)
- java.util.EnumMap
-
- キーが1種類の enum 型の場合に利用できる。メモリ効率が良い。
- イテレータで返す順番が保証されている(ordinal順)
- java.util.Hashtable
-
- 古いコレクションフレームワーク
- 基本使わないこと
- java.util.Properties
-
- 古いコレクションフレームワーク
- 負の遺産
- いろいろなライブラリで使われているので捨てるに捨てられない
- その他クラス
-
- java.util.concurrent.ConcurrentHashMap
- java.util.IdentityHashMap
- java.util.WeakHashMap
- いろいろ
便利なユーティリティ
基本は java.util.Collections
にまとまっています。
空のマップ
java.util.Collections#emptyMap()
空のマップが必要な場合には、無駄に new HashMap()
でインスタンスを生成しないで、イミュータブルなオブジェクトを使いまわすほうが良いかと。
変更不可なので注意。
java.util.Map#of()
Java 9 からは、変更不可の空マップを簡単に生成できるようになりました。
こちらのほうが短いですね。
要素が1つだけのマップ
java.util.Collections#singletonMap()
変更不可なので注意。
java.util.Map#of(K, V)
Java9 からは、変更不可の1要素マップを簡単に生成できるようになりました。
こちらのほうが短いですね。
変更不可マップ化
-
java.util.Collections#unmodifiableMap()
-
java.util.Collections#unmodifiableSortedMap()
-
java.util.Map#of(K, V, K, V, ...)
Java 9 からは変更不可マップを直接生成できるようになりました。
ただし、2種類の型の引数を交互に指定する必要があるため、可変引数メソッドにはなっていません。
初期値が10組までのメソッドがオーバロードで定義されています。
同期マップ化
java.util.Collections#synchronizedMap()
java.util.Collections#synchronizedSortedMap()
少し意味は違いますが、同期化するよりも java.util.concurrent.ConcurrentHashMap
が使えないかを検討したほうが良いかもしれません。
動的型保証
java.util.Collections#checkedMap()
HashMap
より LinkedHashMap
を使う
HashMap
ではイテレータなどで全件取得する際に取得できる順番は保証されていません。
そのため、単体テストの検証や、トラブルシュート時の再現テストで少々困ることもあります。
仕様で取得順番が明確化されている LinkedHashMap
を使うほうが便利なことが多いと思われます。
Map
の初期化
匿名クラスとイニシャライザ
Mapを初期化する場合に、以下のような匿名クラスとイニシャライザを組み合わせたテクニックが使われることがあります。
// 使い方
Map<String, String> map = new HashMap<String, String>() {
{
put("one", "1st");
put("two", "2nd");
put("three", "3rd");
}
};
この方法には大小いくつかの問題点があると思います。
- 初期化ごとに匿名クラスが作られる(影響:小)
- ダイヤモンドオペレータが使用できない(影響:極小)
- HashMap などはシリアライズ可能だが、serialVersionUID が定義されていない(影響:小)
- インスタンスメソッド内などで生成した場合、アウタークラスのインスタンスが暗黙のうちに保持される(影響:大)
特に最後のアウタークラスのインスタンスを抱え込んでしまうことで、以下のような問題が起きることがあります(クラススタティックなスコープで生成すれば大丈夫)。
- メモリリーク(必要なのはMapだけなのに、アウタークラスのインスタンスもずっと保持してしまう)
- シリアライズ失敗(Mapをシリアライズしたいが、アウタークラスがシリアライズ不可能の場合)
- シリアライズサイズが無駄に大きくなる(アウタークラスもシリアライズ可能な場合)
インスタンススコープで以下のようにMapを生成したとします。
import java.util.HashMap;
import java.util.Map;
public class Test {
public void test() {
Map<String, String> map = new HashMap<String, String>() {
{
put("one", "1st");
put("two", "2nd");
put("three", "3rd");
}
};
}
}
匿名クラスの逆コンパイル結果は以下のようになります。アウタークラス(ここでは Test クラス)のインスタンスを保持していることがわかります。
import java.util.HashMap;
class Test$1 extends HashMap
{
final Test this$0;
Test$1()
{
this$0 = Test.this;
super();
put("one", "1st");
put("two", "2nd");
put("three", "3rd");
}
}
ビルダーパターン
以下のようなビルダークラスを使って生成する方法も良く使われます。
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Mapビルダー.<br>
* 各メソッドはメソッドチェーンで呼び出すことを想定.<br>
*/
public class MapBuilder<K, V> {
/** 生成したMap */
private final Map<K, V> map;
/** Mapビルダーを生成 */
public MapBuilder() {
map = new LinkedHashMap<>();
}
/** Mapに値を登録 */
public MapBuilder<K, V> put(K key, V value) {
map.put(key, value);
return this;
}
/** 編集可能なMapを返す */
public Map<K, V> toMap() {
return map;
}
/** 編集不可なMapを返す */
public Map<K, V> toConst() {
return Collections.unmodifiableMap(map);
}
}
// 使い方
Map<String, String> map = new MapBuilder<String, String>()
.put("one", "1st")
.put("two", "2nd")
.put("three", "3rd")
.toConst();
メリットは編集可能なマップも、編集不能なマップも作れること。
デメリットはダイヤモンドオペレータが使えないこと。
ラムダ式
どうしても総称型を2回書きたくない場合には、ラムダ式を使う方法もありそうです。
public static <K, V> Map<K, V> toMap(Consumer<Map<K, V>> initializer) {
Map<K, V> map = new LinkedHashMap<>();
initializer.accept(map);
return map;
}
// 使い方
Map<String, String> map = toMap(m -> {
m.put("one", "1st");
m.put("two", "2nd");
m.put("three", "3rd");
});
ラムダ+リフレクション(Java8u60 以降の場合)
Java8u60 以降であれば、ラムダ式の引数名がリフレクションで取得可能になっているらしく、面白い初期化の仕方ができるみたいです。
参考:Java 8u60で、Mapの初期化とかがすごい楽になってる件
関係しそうな部分だけ切り出して実行してみました。
import java.io.Serializable;
import java.lang.invoke.SerializedLambda;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
/** ラムダ式の変数名を取得してマップを定義するテスト */
public class LambdaNameMap {
/** テスト実行 */
public static void main(String[] args) throws Exception {
System.out.println(System.getProperty("java.version"));
// こんな感じでマップが作れる
Map<String, String> map = toConstMap(
one -> "1st",
two -> "2nd",
three -> "3rd");
System.out.println(map);
}
/** 名前付きの値のためのインタフェース */
public interface NamedValue<T> extends Function<String, T>, Serializable {
default T value() {
return apply(name());
}
default String name() {
return getName(this);
}
}
/** 変更不能マップを生成 */
@SafeVarargs
public static <T> Map<String, T> toConstMap(NamedValue<T>... namedValues) {
Map<String, T> map = Arrays.stream(namedValues)
.collect(Collectors.toMap(namedValue -> namedValue.name(), namedValue->namedValue.value()));
return Collections.unmodifiableMap(map);
}
/** ラムダ式から変数の名前を取得 */
private static <T> String getName(Function<String, T> func) {
SerializedLambda lambdaInfo = toSerializedLambda(func);
Method method = getMethod(lambdaInfo, String.class);
Parameter parameter = method.getParameters()[0];
if (!parameter.isNamePresent()) {
throw new IllegalStateException("no name");
}
return parameter.getName();
}
/** SerializedLambda から指定のシグネチャのメソッドを取得 */
private static Method getMethod(SerializedLambda lambdaInfo, Class<?>... argClasses) {
try {
// 実装クラス名
String implClassName = lambdaInfo.getImplClass().replace('/', '.');
Class<?> implClass = Class.forName(implClassName);
// 実装メソッド名
String implMethodName = lambdaInfo.getImplMethodName();
Method implMethod = implClass.getDeclaredMethod(implMethodName, argClasses);
return implMethod;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/** ラムダ式から SerializedLambda を取得 */
private static SerializedLambda toSerializedLambda(Object lambda) {
try {
Method replaceMethod = lambda.getClass().getDeclaredMethod("writeReplace");
replaceMethod.setAccessible(true);
return (SerializedLambda) replaceMethod.invoke(lambda);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
コンパイル時には javac に -parameters
を付ける必要があります。
もしEclipse の場合には、-parameters
の代わりにプリファレンスのJava/Compilerの"Store inforation about method parameters (usable via reflection)" にチェックを入れてください。
(あるいはプロジェクトのプロパティの Java Compiler の同名の項目)
実行結果はこうなりました。
1.8.0_65
{one=1st, three=3rd, two=2nd}
非常に興味深いですが、デメリットもあるかと。
- キーは変数名に使えるものだけ
- 現時点では動作環境が少ない(Java9などに切り替わった後なら安心して使えるかもね)
- コンパイルオプションを変更しないと使えない
普通の初期化
大抵の場合には、変な技巧をこらさず愚直に初期化すると思います。
クラススタティックな変数の場合にはstaticイニシャライザ。
private static final Map<String, String> CONST_MAP;
static {
Map<String, string> map = new LinkedHashMap<>();
map.put("one", "1st");
map.put("two", "2nd");
map.put("three", "3rd");
CONST_MAP = Collections.unmodifiableMap(map);
}
インスタンスな変数の場合にはイニシャライザかコンストラクタ。
private final Map<String, String> map;
{
map = new LinkedHashMap<>();
map.put("one", "1st");
map.put("two", "2nd");
map.put("three", "3rd");
}
ローカル変数なら生成後に順次。
Map<String, string> map = new LinkedHashMap<>();
map.put("one", "1st");
map.put("two", "2nd");
map.put("three", "3rd");
あるいは、マップを生成するメソッドを別途用意する方法も良く使われると思います。
private static final Map<String, String> CONST_MAP = createMap();
private static Map<String, String> createMap() {
Map<String, string> map = new LinkedHashMap<>();
map.put("one", "1st");
map.put("two", "2nd");
map.put("three", "3rd");
return Collections.unmodifiableMap(map);
}
Java 9 以降の初期化(変更不可リスト)
前述のように、Java 9 からは簡単に変更不可リストを生成できるようになりました。
ただし、引数の型が 2 種類(の場合も)あるため、可変引数にはなっておらず、引数が初期値 0~10 組までのオーバロードで実装されています。
private static final Map<String, String> CONST_MAP = Map.of(
"one", "1st",
"two", "2nd",
"three", "3rd");
もう一つ Map#ofEntries
を使う生成方法もあります。こちらは Map.Entry
の可変引数になっているため、個数に制限がありません。
import java.util.Map;
import static java.util.Map.entry;
// ...
private static final Map<String, String> CONST_MAP = Map.ofEntries(
entry("one", "1st"),
entry("two", "2nd"),
entry("three", "3rd"));
Hashtable
の contains()
メソッド
Map
インタフェースには containsKey()
, containsValue()
メソッドがあります。
ところが、古い Hashtable
やその子クラスの Properties
にはこのほかに contains()
メソッドもあります。動作的には containsValue()
と同じものです。
名前が似ているので containsKey()
と間違えないようにご注意を。