【Java】Map的笔记

的意思

    • 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适用于键只有一种枚举类型的情况。内存效率很高。迭代器返回的顺序是保证的(按序数顺序)。

java.util.Hashtable是旧的集合框架,基本上不使用。

java.util.Properties也是旧的集合框架,是一个负面遗产。由于在许多Library中使用到,所以不能被完全舍弃。

其他类有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开始,我们可以轻松生成不可更改的空映射。这个选项更简洁。

只有一个要素的地图

    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开始,可以直接生成不可更改的映射。
但是,由于需要交替指定两种类型的参数,所以它不是可变参数方法。
有提供了10对初始值的方法进行重载定义。

将同期转化为地图

    • java.util.Collections#synchronizedMap()

 

    java.util.Collections#synchronizedSortedMap()

也许考虑使用java.util.concurrent.ConcurrentHashMap比同步化更好,意思稍有不同。

动态担保

    java.util.Collections#checkedMap()

使用LinkedHashMap而不是HashMap

在HashMap中,无法保证在使用迭代器等方式获取全部元素时的顺序。

因此,在单元测试验证或故障排除时,可能会遇到一些困难。

使用有序的LinkedHashMap比较多时会更加方便,因为它可以明确规定获取数据的顺序。

地图的初始化

匿名类和初始化器。

在初始化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();

优点是可以创建可编辑和不可编辑的地图。

缺点是无法使用钻石操作员。

匿名函数

如果您确实不想写两次泛型类型的话,似乎也有使用Lambda表达式的方法。

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");
});

Java 8u60以后的情况下,对于Lambda和Reflection。

据说在Java8u60或更高版本中,可以通过反射获取Lambda表达式的参数名称,这样就可以实现一种有趣的初始化方式。

在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中的情况下,请在偏好设置的Java/编译器中的”存储有关方法参数的信息(可通过反射使用)”选项上进行勾选,而不是使用-parameters参数。(或者在项目属性的Java编译器中找到相同的选项)

执行结果如下。

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 现在可以轻松生成不可更改的列表。

然而,由于参数的类型有两种情况,因此它并不是可变参数,而是通过参数的初始值来实现重载,范围为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()混淆。

广告
将在 10 秒后关闭
bannerAds