文章目录
一、Map的继承结构
- Map集合以key和value的键值对形式存储。key和value存储的都是引用。
- Map集合中key起主导作用。value是附属在key上的。
- SequencedMap是Java21新增的。LinkedHashMap和TreeMap都是有序集合。(key是有序的)
- HashMap,Hashtable,Properties都是无序集合。(key是无序的)
- Map集合的key都是不可重复的。key重复的话,value会覆盖。
- HashSet集合底层是new了一个HashMap。往HashSet集合中存储元素实际上是将元素存储到HashMap集合的key部分。HashMap集合的key是无序不可重复的,因此HashSet集合就是无序不可重复的。HashMap集合底层是哈希表/散列表数据结构,因此HashSet底层也是哈希表/散列表。
- TreeSet集合底层是new了一个TreeMap。往TreeSet集合中存储元素实际上是将元素存储到TreeMap集合的key部分。TreeMap集合的key是不可重复但可排序的,因此TreeSet集合就是不可重复但可排序的。TreeMap集合底层是红黑树,因此TreeSet底层也是红黑树。它们的排序通过java.lang.Comparablejava.util.Comparator均可实现。
- LinkedHashSet集合底层是new了一个LinkedHashMap。LinkedHashMap集合只是为了保证元素的插入顺序,效率比HashSet低,底层采用的哈希表+双向链表实现。
- 根据源码可以看到向Set集合中add时,底层会向Map中put。value只是一个固定不变的常量,只是起到一个占位符的作用。主要是key。
- Hashtable和Properties的key和value都不能为null,TreeMap的key不能为null,TreeSet不能添加null。
二、Map接口的常用方法
第13个方法是Java 9中引入的一种方便的方式来创建Map实例。
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, Integer> scores = Map.of("Alice", 80, "Bob", 90, "Charlie", 75);
System.out.println(scores);
}
}
输出结果为:
{Alice=80, Bob=90, Charlie=75}
三、Map集合的遍历
1、keySet
import java.util.Set;
public class MapIterationTest {
public static void main(String[] args) {
Map<Integer, String> map = new HashMap<>();
map.put(1, "zhangsan");
map.put(2, "lisi");
map.put(3, "wangwu");
map.put(4, "zhaoliu");
}
}
使用迭代器
Set<Integer> keySet = map.keySet();
Iterator<Integer> iterator = keySet.iterator();
while (iterator.hasNext()) {
Integer next = iterator.next();
System.out.println(next + " = " +map.get(next ));
}
使用for-each
Set<Integer> keySet = map.keySet();
for (Integer key : keySet){
System.out.println(key + "="+map.get(key));
}
2、entrySet(效率较高)
Entry类中包含获取key和value的方法,不需要根据key去找value
Set<Map.Entry<Integer, String>> entries = map.entrySet();
Iterator<Map.Entry<Integer, String>> entryIterator = entries.iterator();
while (entryIterator.hasNext()) {
Map.Entry<Integer, String> entry = entryIterator.next();
Integer key = entry.getKey();
String value = entry.getValue();
System.out.println(key + "=" +value);
}
for(Map.Entry<Integer, String> entry:entries){
System.out.println(entry.getKey() + "= "+entry.getValue());
}
三、HashMap底层结构
1、重写equals和hashCode方法
map的底层数据结构是由哈希表和红黑树组成的,当自定义map的key为引用时,需要重写这个引用的toString和hashCode方法。
一个重要原理:如果equals方法返回结果为true,那么hashCode()方法返回的结果必须相同。这样才能保证key不是重复。存储HashMap集合key部分的元素,以及存储在HashSet集合中的元素,都需要同时重写hashCode+equals。
public class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return age == user.age &&
Objects.equals(name, user.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
新建两个相同的对象user1和user2,将user1和user2作为key放入map集合中,User类在不重写equals和hashCode方法的前提下,会将user1和user2都放入,但是这违背了HashMap集合key的唯一性。重写equals方法判断对象是否相同,重写hashCode方法保证相同的对象经过hash函数处理之后值是相同的,发生哈希碰撞,从而保证key的唯一性。
package com.rongrong.Map;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class HashMapTest {
public static void main(String[] args) {
User user = new User("rongrong",18);
User user1 = new User("rongrong",18);
Map<User,Integer> map = new HashMap();
map.put(user,123);
map.put(user1,456);
Set<Map.Entry<User, Integer>> entries = map.entrySet();
for (Map.Entry<User, Integer> entry:entries) {
System.out.println(entry.getKey() + "=" + entry.getValue());
}
}
}
这两个方法不重写,会调用Object类的equals和hashCode的方法,而Object类中的这两个方法根据引用的地址判断两个对象是否相同和求哈希值。
2、HashMap在Java8后的改进
3、HashMap初始化容量
4、HashMap的put方法执行过程
HashMap集合扩容成本较高,扩容之后,数组长度会改变,需要将集合中的元素,重新计算分配,让其散列分布均匀。HashMap中的put方法执行过程大体如下:
1、判断键值对数组table[i]是否为空(null)或者length=0,或者是的话就执行resize()方法进行扩容。
2、不是就根据键值key计算hash值得到插入的数组索引i。
3、判断table[i]==null,如果是true,直接新建节点进行添加。
4、如果table[i]!=null,判断table[i]处的key是否和要插入的key相同,相同则覆盖,不同则执行5
5、判断table[i]是否为treenode,即判断是否是红黑树,如果是红黑树,直接在树中插入键值对。
6、如果不是treenode,开始遍历链表,
- 判断链表长度是否大于8,如果大于8并且此时table的长度大于64,就转成红黑树,在树中执行插入操作;
- 如果链表长度大于8但是table长度小于64,执行resize()扩容方法。
- 如果不大于8,就在链表中执行插入;在遍历过程中判断key是否存在,存在就直接覆盖对应的value值。
7、插入成功后,就需要判断实际存在的键值对数量size是否超过了最大容量threshold,如果超过了,执行resize方法进行扩容。