详解Java HashMap

发布时间 2023-10-16 18:10:28作者: KRDecad3

HashMap介绍

HashMap遍历方式

HashMap的遍历,大体上可分为4类,而每种类型下又有不同的实现方式,总共的遍历方式可分为7种:

  1. 迭代器遍历:
    • 使用迭代器对EntrySet遍历;
    • 使用迭代器对KeySet遍历;
  2. foreach遍历:
    • 使用foreach对EntrySet遍历;
    • 使用foreach对KeySet遍历;
  3. lambda表达式遍历;
  4. streams API遍历:
    • Streams API单线程方式遍历;
    • Streams API多线程方式遍历。

下面列举出几种遍历方式,首先创建并初始化一个HashMap:

public class HashMapTest {
    public static void main(String[] args) {
        Map<Integer, String> map = new HashMap<>();
        map.put(1, "Java");
        map.put(2, "Python");
        map.put(3, "C++");
        map.put(4, "Rust");
        map.put(5, "Go");

        System.out.println("1. 迭代器遍历EntrySet");
        traverseByIterator1(map);
        System.out.println("2. 迭代器遍历KeySet");
        traverseByIterator2(map);
        System.out.println("3. foreach遍历EntrySet");
        traverseByForeach1(map);
        System.out.println("4. foreach遍历KeySet");
        traverseByForeach2(map);
        System.out.println("5. lambda表达式遍历");
        traverseByLambda(map);
        System.out.println("6. streams API单线程");
        traverseByStreams1(map);
        System.out.println("7. streams API多线程");
        traverseByStreams2(map);
    }
}

迭代器

EntrySet

public static void traverseByIterator1(Map map) {
	Iterator<Map.Entry<Integer, String>> iterator = map.entrySet().iterator();
	while (iterator.hasNext()) {
		Map.Entry<Integer, String> entry = iterator.next();
		System.out.println(entry.getKey() + " : " + entry.getValue());
	}
}

输出:

1. 迭代器遍历EntrySet
1 : Java
2 : Python
3 : C++
4 : Rust
5 : Go

KeySet

public static void traverseByIterator2(Map map) {
	Iterator<Integer> iterator = map.keySet().iterator();
	while (iterator.hasNext()) {
		Integer key = iterator.next();
		System.out.println(key + " : " + map.get(key));
	}
}

输出:

2. 迭代器遍历KeySet
1 : Java
2 : Python
3 : C++
4 : Rust
5 : Go

foreach

对于foreach遍历,内部也是通过创建迭代器来遍历

EntrySet

public static void traverseByForeach1(Map<Integer, String> map) {
	for (Map.Entry<Integer, String> entry : map.entrySet()) {
		System.out.println(entry.getKey() + " : " + entry.getValue());
	}
}

输出:

3. foreach遍历EntrySet
1 : Java
2 : Python
3 : C++
4 : Rust
5 : Go

KeySet

public static void traverseByForeach2(Map<Integer, String> map) {
	for (Integer key : map.keySet()) {
		System.out.println(key + " : " + map.get(key));
	}
}

输出:

4. foreach遍历KeySet
1 : Java
2 : Python
3 : C++
4 : Rust
5 : Go

lambda表达式

public static void traverseByLambda(Map map) {
	map.forEach((key, value) -> {
		System.out.println(key + " : " + value);
	});
}

输出:

5. lambda表达式遍历
1 : Java
2 : Python
3 : C++
4 : Rust
5 : Go

streams API

单线程

public static void traverseByStreams1(Map<Integer, String> map) {
	map.entrySet().stream().forEach((entry) -> {
		System.out.println(entry.getKey() + " : " + entry.getValue());
	});
}

输出:

6. streams API单线程
1 : Java
2 : Python
3 : C++
4 : Rust
5 : Go

多线程

public static void traverseByStreams2(Map<Integer, String> map) {
	map.entrySet().parallelStream().forEach((entry) -> {
		System.out.println(entry.getKey() + " : " + entry.getValue());
	});
}

输出:

7. streams API多线程
1 : Java
2 : Python
3 : C++
4 : Rust
5 : Go

遍历时删除元素

在上述几种遍历方式中,有些可以在遍历过程中安全删除元素,有些则会抛出ConcurrentModificationException异常,这是因为遍历过程中会比较modCount != expectedModCount,不相等就会抛出异常,具体分析请往下看。

迭代器

Iterator<Map.Entry<Integer, String>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
	Map.Entry<Integer, String> entry = iterator.next();
	if (entry.getKey() == 1) {
		iterator.remove();
	}
}

通过迭代器遍历,并使用迭代器的remove()方法可以正常删除元素。
成功的原因是,调用iterator.remove()方法最后会对expectedModCount值进行更新,这样就保证了迭代器调用next()获取下一个元素时,检查modCount == expectedModCount
image

foreach

for (Map.Entry<Integer, String> entry : map.entrySet()) {
	if (entry.getKey() == 2) {
		map.remove(entry.getKey());
	}
}

在foreach遍历的过程中调用Map的remove()方法会抛出ConcurrentModificationException异常。
通过查看源码得知,抛出异常的原因是删除元素之后,进行下一个元素的遍历时,比较变量modCountexpectedModCount不相等:
image
HashMap中的变量modCount记录了HashMap的修改次数,HashIterator中的变量expectedModCount在遍历前会初始化与modCount相等,当删除一个元素时,++modCount,之后迭代器通过next()获取下一个元素时,检查modCount != expectedModCount,就会抛出异常。
此外,这种检查到错误就抛出异常并停止程序后续执行的机制被称为fail-fast机制。

lambda

map.forEach((key, value) -> {
	if (key == 1) {
		map.remove(key);
	}
});

使用lambda表达式遍历时删除也会抛出ConcurrentModificationException。

可以通过removeIf()对key进行判断后删除。

map.keySet().removeIf(key -> key == 1);
map.forEach((key, value) -> {
	System.out.println(key + value);
});

sterams:

map.entrySet().stream().forEach((entry) -> {
	if (entry.getKey() == 1) {
		map.remove(entry.getKey());
	}
});

使用stream遍历删除同样抛出ConcurrentModificationException。
可以使用filter()过滤掉不需要的数据再遍历,但是这种方式不会真正删除hashmap中的元素。

map.entrySet().stream().filter(e -> 1 != e.getKey()).forEach((entry) -> {
	if (entry.getKey() == 1) {
		System.out.println(entry.getKey());
	}
});

HashMap扩容机制

HashMap线程安全问题