1. Map 和 Set 是什么?
1.1 概念
在Java中,Map
和Set
都是接口,是一种专门用来进行搜索的容器或者数据结构,其搜索的效率与其具体的实例化子类有关。Map
的实例化子类有TreeMap
、HashMap
等,Set
的实例化子类有TreeSet
,HashSet
等
它们的模型:一般把搜索的数据称为关键字(Key),和关键字对应的称为值(Value),将其称之为Key-value
的键值对,所以模型会有两种,纯 key 模型
、Key-Value 模型
。
Map中存储的是key-value的键值对,Set中只存储了Key,并且每个key都是唯一的,不能重复。
2.Map 接口的使用
在Java中,Map
接口的实现类有:TreeMap、HashMap
。
Map
中提供了一些方法的规范,下面是一些常用的:
方法 | 描述 | |
| 从Map中删除所有的键值对。 | |
| 判断Map中是否包含指定的键。 | |
| 判断Map中是否包含指定的值。 | |
| 返回Map中包含的所有键值对的Set集合。 | |
| 返回与指定键相关联的值。如果Map中不包含该键,则返回null。 | |
| 判断Map是否为空。 | |
| 返回Map中包含的所有键的Set集合。 | |
| 将指定的键值对添加到Map中。如果Map中已经包含了该键,则使用新值替换旧值,并返回旧值。 | |
| 将指定Map中的所有键值都添加到当前Map中。 | |
| 从Map中删除与指定键相关联的键值对。如果Map中不包含该键,则返回null。 | |
| 返回Map中键值对的数量。 | |
| 返回Map中包含的所有值的Collection集合。 | |
| 返回 key 对应的 value,key 不存在,返回默认值 |
- Map是一个接口,不能直接实例化对象,如果要实例化对象只能实例化其实现类TreeMap或者HashMap。
- Map中存放键值对的Key是唯一的,value是可以重复的。
- Map中的Key可以全部分离出来,存储到Set中来进行访问(因为Key不能重复)。
- Map中的value可以全部分离出来,存储在Collection的任何一个子集合中(value可能有重复)。
- Map中键值对的Key不能直接修改,value可以修改,如果要修改key,只能先将该key删除掉,然后再来进行重新插入。
- TreeMap中的key不能为null,HashMap中的key可以为null。
2.1 TreeMap
在Java中,TreeMap
是基于红黑树实现的一种有序Map
数据结构。红黑树是一种自平衡的二叉搜索树,它可以保证插入、删除、查找等操作的最坏情况时间复杂度为O(log n)
,在TreeMap
中key
不能为null
。 (二叉搜索树 的基本操作 - 掘金 (juejin.cn))
(1)TreeMap 的常见构造方法
构造方法 | 描述 |
| 创建一个空的TreeMap。 |
| 创建一个空的TreeMap,使用指定的比较器对键进行排序。 |
| 创建一个包含指定Map中的所有键值对的TreeMap。 |
由于TreeMap
底层是用搜索树实现的,所以TreeMap
里元素的 key
必须是可比较的(根据key
来比较,生成搜索树),不然要报错,这时候就需要传一个比较器。
class Student{
String name;
int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student{" + "name=" + name + ", age=" + age + '}';
}
}
public class Main {
public static void main(String[] args) {
//因为 Student 不可比较,所以传入一个比较器
Map<Student,Integer> map = new TreeMap<>(new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
//比较名字
return o1.name.compareTo(o2.name);
}
});
map.put(new Student("小明",20),1);
map.put(new Student("张三",20),2);
map.put(new Student("王五",20),3);
map.put(new Student("李四",20),4);
System.out.println(map);//TreeMap重写了toString()方法,可以直接打印。
}
}
结果:{Student{name=小明, age=20}=1, Student{name=张三, age=20}=2, Student{name=李四, age=20}=4, Student{name=王五, age=20}=3}
复制代码
还有另一种方式,就是 Student
实现Comparable
接口重写compareTo()
方法。
class Student implements Comparable<Student>{
String name;
int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student{" + "name=" + name + ", age=" + age + '}';
}
@Override
public int compareTo(Student o) {
return this.name.compareTo(o.name);
}
}
复制代码
(2)TreeMap 方法的演示
TreeMap
的常见方法都是重写了Map
接口的方法,这里演示几个比较有趣的方法。
- getOrDefault() 方法。
public static void main(String[] args) {
Map<String,Integer> map = new TreeMap<>();
map.put("小明",20);
map.put("张三",19);
map.put("王五",18);
//返回 key 对应的 value,key 不存在,返回默认值
int n = map.getOrDefault("李四",1000);
System.out.println(n);
}
结果:1000
复制代码
- keySet() 方法 与 values() 方法
public static void main(String[] args) {
Map<String,Integer> map = new TreeMap<>();
map.put("小明",20);
map.put("张三",19);
map.put("王五",20);
//返回所有 key 的不重复集合
Set<String> set = map.keySet();
System.out.println(set);
//返回所有 value 的可重复集合
Collection<Integer> collection = map.values();
System.out.println(collection);
}
结果:
[小明, 张三, 王五]
[20, 19, 20]
复制代码
(4)Map 的遍历问题:Map.Entry
Map.Entry<K, V>
是Map
内部实现的用来存放<key, value>
键值对映射关系的内部类,这就好比链表中的Node
一样,该内部类中主要提供了<key, value>
的获取,value
的设置以及Key
的比较方式。
方法 | 解释 |
| 返回 entry 中的 key |
| 返回 entry 中的 value |
| 将键值对中的value替换为指定value |
当我们遍历Map
时,需要注意的是Map
中存储的是键值对(Entry)
,因此我们需要先获取Map
中的所有Entry
,再逐个遍历每个Entry
的键和值。
public static void main(String[] args) {
Map<String,Integer> map = new TreeMap<>();
map.put("小明",20);
map.put("张三",19);
map.put("王五",20);
//遍历 Map, Map.Entry<String,Integer> 表示 Map 中的键值对,就像链表中的 Node 一样
Set<Map.Entry<String,Integer>> set = map.entrySet();//将 Map 里的所有键值对存入一个 Set 里面
//遍历 Set
for(Map.Entry<String,Integer> entry: set){
String key = entry.getKey();
//将“小明”的 value 改为 30
if(entry.getKey().equals("小明")){
entry.setValue(30);
}
int value = entry.getValue();
System.out.println("Key: " + key +" "+ "Value: " + value);
}
}
结果:
Key: 小明 Value: 30
Key: 张三 Value: 19
Key: 王五 Value: 20
复制代码
为什么需要将所有键值对存入set
里面,再对set
遍历?为什么不能直接对Map
进行for-each
遍历?
因为Map
接口没有直接实现Iterable
接口,因此无法直接使用for-each
语句遍历Map
中的元素。
2.2 HashMap
Java中HashMap
的底层是一个哈希桶(开散列表),什么是哈希桶?
哈希桶:一个数组,每个数组元素都是一个单向链表或红黑树,这些链表或树的头结点组成了一个桶数组,用来存储键值对,也可以叫做哈希表。(哈希表是什么?哈希冲突又是什么?又如何解决哈希冲突? - 掘金 (juejin.cn))
(1)构造方法
构造方法 | 描述 |
| 构造一个空的 |
| 构造具有指定初始容量的 |
| 构造具有指定初始容量和负载因子的 |
| 构造一个新的 |
HashMap
的使用与TreeMap
都是相同的,比如遍历等,下面是使用HashMap
的注意点:
- HashMap中的元素必须是能比较出是否相同的,自定义类型需要重写equals和 hashCode方法,对是否能比较大小没有要求。
class Student{
String name;
int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student{" + "name=" + name + ", age=" + age + '}';
}
}
public class Main {
public static void main1(String[] args) {
Map<Student,Integer> map = new HashMap<>();
map.put(new Student("小明",19),1);
map.put(new Student("张三",18),2);
map.put(new Student("王五",20),3);
map.put(new Student("王五",20),4);
System.out.println(map);
}
}
复制代码
结果:
我们期望的是王五只出现一次,但是这里出现了两次,为什么?因为Student
没有比较是否相等的能力,它需要重写equals
和 hashCode
方法。
class Student{
String name;
int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student{" + "name=" + name + ", age=" + age + '}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return age == student.age && name.equals(student.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
public class Main {
public static void main(String[] args) {
HashMap<Student,Integer> map = new HashMap<>();
map.put(new Student("小明",19),1);
map.put(new Student("张三",18),2);
map.put(new Student("王五",20),3);
map.put(new Student("王五",20),4);
System.out.println(map);
}
}
结果:
{Student{name=小明, age=19}=1, Student{name=张三, age=18}=2, Student{name=王五, age=20}=4}
复制代码
在HashMap
源码中,重写的hashCode()
方法是用来计算哈希值进而确定key的位置,equals()
方法是用来比较两个对象是否相等的。
- 顺序问题:
HashMap
中键值对的顺序是不确定的,而TreeMap
中默认按键的自然顺序或指定的比较器进行排序。 - 扩容机制:如果
new
的时候没有传参数,那么当第一次put()
的时候,才会为HashMap
分配内存,大小为 16;如果new
的时候传了参数n
,那么会分配最接近(大于等于)你给的参数的2次幂的值,比如:n = 19,那么会分配2525 = 32 的大小。 - 在 JDK 8 中,当一个桶中的元素数量超过了 8 个,并且哈希表的容量大于等于 64,这个桶就会被转换成红黑树。
2.4 TreeMap 与 HashMap 的区别
Map底层结构 | TreeMap | HashMap |
底层结构 | 红黑树 | 哈希桶 |
插入/删除/查找时间 复杂度 | �(���2�)O(log2N) | �(1)O(1) |
是否有序 | 关于Key有序 | 无序 |
线程安全 | 不安全 | 不安全 |
插入/删除/查找区别 | 需要进行元素比较 | 通过哈希函数计算哈希地址 |
比较与重写 | key必须能够比较,否则会抛出 ClassCastException异常 | 自定义类型需要重写equals和 hashCode方法 |
应用场景 | 需要Key有序场景下 | Key是否有序不关心,需要更高的 时间性能 |
3. Set 接口的使用
Set接口提供的方法
方法 | 描述 |
| 将指定元素添加到集合中(如果尚未存在)。 |
| 从集合中移除指定元素。 |
| 如果集合中包含指定元素,则返回 true。 |
| 如果集合不包含任何元素,则返回 true。 |
| 返回集合中的元素数量。 |
| 从集合中移除所有元素。 |
| 返回一个包含集合中所有元素的数组。 |
| 将集合中的所有元素转换为指定类型的数组。 |
| 如果集合中包含指定集合中的所有元素,则返回 true。 |
| 将指定集合中的所有元素添加到集合中(如果尚未存在)。 |
| 仅保留集合中包含在指定集合中的元素(删除所有其他元素)。 |
| 从集合中移除指定集合中包含的所有元素。 |
由于Set接口实现了lterable
,所以TreeSet、HashSet
的遍历直接用for-each
即可。
- Set是继承自Collection的一个接口类。
- Set中只存储了key,并且要求key一定要唯一。
- Set的底层是使用Map来实现的,其使用key与Object的一个默认对象作为键值对插入到Map中的。
- Set最大的功能就是对集合中的元素进行去重。
- TreeSet中不能插入null的key,HashSet能插入null的key。
3.1 TreeSet
TreeSet
底层是什么呢?它的底层其实就是TreeMap
!
现在有以下代码
public static void main(String[] args) {
Set<String> set = new TreeSet<>();
set.add("小明");
}
复制代码
我们点进源码中的 add():
也就是说,当我new TreeSet()
的时候,它会 new TreeMap()
然后赋给 m;但是Set
明明不是键值对的形式来存储数据的呀,底层怎么会是Map
呢?从上面的图中看到put()
方法中第二个参数是PRESENT
,而PRESENT
的值是new Object()
,换言之,无论add多少,第二个参数永远都是一个Object
类,这就相当于忽略了value
值。
3.2 HashSet
HashSet
的底层是HashMap
,HashSet
的一些注意事项与HashMap
相同,比如它们的key
可以为null
,自定义类型要重写equals
与hashCode
方法等,这里就不赘述了。
3.3 TreeSet 与 HashSet 的区别
Set底层结构 | TreeSet | HashSet |
底层结构 | 红黑树 | 哈希桶 |
插入/删除/查找时间 复杂度 | �(���2�)O(log2N) | �(1)O(1) |
是否有序 | 关于Key有序 | 不一定有序 |
线程安全 | 不安全 | 不安全 |
插入/删除/查找区别 | 需要进行元素比较 | 通过哈希函数计算哈希地址 |
比较与覆写 | key必须能够比较,否则会抛出 ClassCastException异常 | 自定义类型需要覆写equals和 hashCode方法 |
应用场景 | 需要Key有序场景下 | Key是否有序不关心,需要更高的时间性能 |