文章目录
1.概述
在日常生活和编程中,我们常常需要处理一种特殊的关系:一一对应的关系,如 IP 地址与主机名,身份证号与个人信息,系统用户名与系统用户对象等。这种关系被称为映射,在 Java 中,我们使用 Map<K, V>
接口来表达和处理这种关系。
java.util.Map<K, V>
是一个在 Java 集合框架中用于存储键值对的接口。键和值是一一对应的,键用于唯一标识一个元素,值则是与该键关联的数据。通过键,我们可以非常快速地找到其对应的值,这就是映射的核心特性。
相比于 Collection<E>
接口,Map<K, V>
接口有着根本的区别。Collection<E>
接口的实现类,如 List<E>
和 Set<E>
,存储的是一组独立的元素。元素之间没有特定的关系,只是单纯地放在一起。而 Map<K, V>
接口则不同,它的元素以键值对的形式存在,每个元素由一个键和一个值组成,二者之间存在着一种映射关系。基于此,我们通常将 Collection<E>
的实现类称为单列集合,而将 Map<K, V>
的实现类称为双列集合。
在 Map
接口中,每个键都必须是唯一的,即不允许重复的键存在。然而,对于值则没有这个限制,相同的值可以与不同的键关联。另外,每个键只能映射到一个值,但这个值可以是单一的值,也可以是一个数组或集合,这为存储复杂的数据结构提供了可能。
2.常用 API
方法名 | 描述 |
---|---|
put(K key, V value) | 将指定的键值对添加到 Map 中 |
get(Object key) | 返回指定键所映射的值 |
remove(Object key) | 从 Map 中移除指定键及其对应的值 |
clear() | 移除 Map 中的所有键值对 |
containsKey(Object key) | 判断 Map 是否包含指定的键 |
containsValue(Object value) | 判断 Map 是否包含指定的值 |
keySet() | 返回 Map 中所有键的集合 |
values() | 返回 Map 中所有值的集合 |
entrySet() | 返回 Map 中所有键值对的集合 |
size() | 返回 Map 中键值对的数量 |
isEmpty() | 判断 Map 是否为空 |
equals(Object o) | 判断 Map 是否与指定对象相等 |
hashCode() | 返回 Map 的哈希码值 |
TIP:
使用 put 方法时,若指定的键(key)在集合中不存在,则没有这个键对应的值,返回 null,并把指定的键值添加到集合中; 若指定的键 (key) 在集合中存在,则返回值为集合中键对应的值(该值为替换前的值),并把指定键所对应的值,替换成指定的新值。
下面是一个简单的使用案例:
public class MapTest {
public static void main(String[] args) {
// 创建HashMap集合
Map<String,String> map = new HashMap<>();
// 添加元素
map.put("name","张三");
map.put("age","18");
map.put("eamil", "XXXX@qq.com");
// 输出集合
System.out.println(map); // {name=张三, eamil=XXXX@qq, age=18}
// 根据key获取value
System.out.println(map.get("name")); // 张三
// 根据key删除元素
map.remove("age");
// 输出集合
System.out.println(map); // {name=张三, eamil=XXXX@qq}
// 判断是否包含某个key
System.out.println(map.containsKey("name")); // true
// 判断是否包含某个value
System.out.println(map.containsValue("张三")); // true
// 获取所有key
System.out.println(map.keySet()); // [name, eamil]
// 获取所有value
System.out.println(map.values()); // [张三, XXXX@qq]
// 获取所有key-value
System.out.println(map.entrySet()); // [name=张三, eamil=XXXX@qq]
// 返回集合大小
System.out.println(map.size()); // 2
// 判断集合是否为空
System.out.println(map.isEmpty()); // false
// 清空集合
map.clear();
// 判断集合是否为空
System.out.println(map.isEmpty()); // true
}
}
3.遍历 Map 集合
遍历集合是我们在日常编程中经常需要进行的操作。对于 Collection
接口的实现类,如 List
和 Set
,我们通常使用 foreach 循环或 Iterator
对象进行遍历。然而,对于 Map
接口,情况则有所不同。
由于 Map
接口没有继承 java.lang.Iterable<T>
接口,也没有实现 Iterator iterator()
方法,所以我们不能直接使用 foreach 循环或 Iterator
对象遍历 Map
。不过,我们仍有其他方法可以有效地遍历 Map
。
Map
接口声名如下:
public interface Map<K, V> {
// ......
}
一种方法是分别遍历所有的键和值。这种方法的实现非常简单,我们可以直接调用 Map
的 keySet()
方法和 values()
方法,分别获取所有的键和值,然后再使用 foreach 循环或 Iterator
对象遍历。
单独遍历所有的键:
for (K key : map.keySet()) {
// 处理键
}
map.keySet().forEach(key -> System.out.println(key));
单独遍历所有的值:
for (V value : map.values()) {
// 处理值
}
map.values().forEach(value -> System.out.println(value));
另一种方法是成对遍历,即同时遍历键和值。这种方法需要使用 Map
接口的内部接口 Map.Entry<K, V>
。Map.Entry<K, V>
表示了一个键值对,即 Map
的一个元素。
在 Map
中,每个元素都是一个 Map.Entry<K, V>
对象,键和值都被存储在这个对象中。因此,我们可以通过遍历所有的 Map.Entry<K, V>
对象,同时获取键和值。
成对遍历所有的键和值:
for (Map.Entry<K, V> entry : map.entrySet()) {
K key = entry.getKey();
V value = entry.getValue();
// 处理键和值
}
map.entrySet().forEach(entry -> System.out.println(entry.getKey() + "=" + entry.getValue()));
下面是一个完整的简单使用实例:
public static void main(String[] args) {
Map<String,String> map = new HashMap<>();
map.put("name","张三");
map.put("age","18");
map.put("eamil", "xxx@qq.com");
// 单独遍历key
map.keySet().forEach(key -> System.out.println(key));
// 单独遍历value
map.values().forEach(value -> System.out.println(value));
// 遍历key-value
map.entrySet().forEach(entry -> System.out.println(entry.getKey() + "=" + entry.getValue()));
}
4.HashMap 和 Hashtable
HashMap
和 Hashtable
是 Java 中两个经典的基于哈希表的数据结构。它们都实现了 Map
接口,可以存储键值对,而且在内部都使用了哈希表以达到快速查找的效果。虽然在表面上它们看起来相似,但在实现细节和用法上,它们有着重要的不同。
HashMap
类图如下:
Hashtable
类图如下:
首先,无论是 HashMap
还是 Hashtable
,它们判断两个键是否相等的标准都是:两个键的 hashCode
方法返回值相等,并且它们的 equals
方法返回 true
。这是因为哈希表通过哈希函数(在 Java 中是对象的 hashCode
方法)将键映射到内部的数组中,然后通过键的 equals
方法来处理哈希冲突。因此,如果你计划使用自定义对象作为键,你必须正确地重写 hashCode
和 equals
方法,以确保对象在哈希表中能被正确地存储和查找。
尽管 HashMap
和 Hashtable
在判断键相等的方式上相同,但它们在其他方面存在显著的不同。最重要的差异在于并发控制和对 null 的处理。
Hashtable
是线程安全的,它使用了同步机制来保证多线程环境下的正确性。如果你在多线程环境下使用 Hashtable
,你可以不用担心并发修改的问题。然而,这种线程安全是有代价的,那就是性能开销。另一方面,Hashtable
不允许使用 null 作为键或值,如果试图插入 null 键或 null 值,Hashtable
会抛出NullPointerException
。
下面是一个简单的使用案例:
public static void main(String[] args) {
// 创建Hashtable集合
Hashtable<String, String> map = new Hashtable<>();
// 添加元素
map.put("name", "张三");
map.put("age", "18");
// map.put(null,null); // key和value都不可以为null
// 输出集合
System.out.println(map); // {name=张三, age=18}
}
与之相反,HashMap
是线程不安全的。如果你在多线程环境下使用 HashMap
,并且没有正确地同步,那么可能会遇到并发问题。然而,因为没有同步机制,HashMap
的性能通常比 Hashtable
要好。另外,HashMap
允许使用 null 键和 null 值,这使得它在某些情况下更为灵活。
下面是一个简单的使用案例:
public static void main(String[] args) {
// 创建HashMap集合
HashMap<String, String> map = new HashMap<>();
// 添加元素
map.put("name", "张三");
map.put("age", "18");
map.put(null,null); // key和value都可以为null
// 输出集合
System.out.println(map); // {null=null,name=张三, age=18}
}
5.LinkedHashMap
LinkedHashMap
是 HashMap
的一个子类,它在 HashMap
的基础上提供了顺序迭代的能力。这个顺序通常就是元素被插入到映射中的顺序,也被称为插入顺序。此外,LinkedHashMap
还提供了一种称为访问顺序的迭代方式。
LinkedHashMap
的类图如下:
LinkedHashMap
与 HashMap
的主要不同之处在于其内部数据结构。在 HashMap
中,每个键值对(即 Map.Entry
对象)都被插入到内部的哈希表中。而在 LinkedHashMap
中,除了哈希表之外,还维护了一个双向链表。每个 Map.Entry
对象都被插入到这个链表的尾部,形成一个按插入顺序排序的键值对链表。
由于这个双向链表的存在,我们可以通过 LinkedHashMap
的迭代器顺序访问每个键值对,这个顺序反映了键值对的插入顺序或访问顺序。插入顺序意味着键值对的迭代顺序就是它们被插入到 LinkedHashMap
中的顺序。而访问顺序则意味着键值对的迭代顺序是它们最后一次被访问(通过 get
或 put
操作)的顺序。
创建 LinkedHashMap
时,我们可以选择使用插入顺序或访问顺序。默认情况下,LinkedHashMap
使用插入顺序。如果我们想要使用访问顺序,只需在创建 LinkedHashMap
时将第三个参数设置为 true
即可。
下面是一个简单使用示例:
public static void main(String[] args) {
// 创建LinkedHashMap集合(默认情况下按照元素添加的顺序排序)
Map<String, String> map = new LinkedHashMap<>();
// 添加元素
map.put("name", "张三");
map.put("age", "18");
map.put(null,null); // key和value都可以为null
// 输出集合
System.out.println(map); // {name=张三, age=18, null=null}
}
public static void main(String[] args) {
/*
* 创建LinkedHashMap集合(默认情况下按照元素添加的顺序排序)
* 当我们不想使用默认的排序方式时,可以使用LinkedHashMap的构造方法
* LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)
* 三个参数:
* initialCapacity:初始容量,指的是集合初始化时容量
* loadFactor:加载因子,指的是集合自动扩容的程度
* accessOrder:排序方式,true表示按照访问顺序排序,false表示按照添加顺序排序
*
* 说明:按照访问顺序排序,指的是按照元素的get方法或put方法访问顺序排序,即最近访问的元素放
* 在最后,最先访问的元素放在最前。也就是说,当我们每次访问一个元素时,该元素都会被放到最后,
* 这样就实现了按照访问顺序排序。
*/
Map<String, String> map = new LinkedHashMap<>(16,0.75f,true);
// 添加元素
map.put("name", "张三");
map.put("age", "18");
map.put(null,null); // key和value都可以为null
// 输出集合
System.out.println(map); // {name=张三, age=18, null=null}
// 通过get方法访问元素
map.get("name");
map.get("age");
// 再次输出集合
System.out.println(map); // {null=null, name=张三, age=18}
}
6.TreeMap
TreeMap
是一个强大的 Java 集合类,它实现了 NavigableMap
接口并提供了一种高效的方式来存储键值对,并按照键进行排序。它的内部结构基于一种称为红黑树的自平衡二叉查找树,这使得它在保持键的有序性的同时,能保证关键操作(如插入、删除和查找)的时间复杂度接近于O(log n),其中n是映射中的条目数。
其类图如下:
TreeMap
的排序取决于键的自然顺序,或者在创建 TreeMap
实例时提供的 Comparator
。自然顺序指的是存储在 TreeMap
中的键所属类的 Comparable
接口的实现,这需要该类的 compareTo
方法定义了一个全序关系。如果键的类没有实现 Comparable
接口,或者你想要在 TreeMap
中使用不同于自然顺序的排序,那么你可以在创建 TreeMap
时提供一个自定义的 Comparator
。
除了基本的 Map
操作,TreeMap
还提供了一些额外的方法,来利用键的有序性。例如,firstKey
方法可以返回当前映射中的最小键,lastKey
方法可以返回当前映射中的最大键。lowerKey
方法和 higherKey
方法则可以返回给定键的前一个和后一个键。此外,TreeMap
还提供了 subMap
、headMap
和 tailMap
方法,可以返回当前映射的一部分视图。
下面是一个简单使用示例:
public static void main(String[] args) {
// 创建TreeMap集合
TreeMap<String, String> map = new TreeMap<>();
// 添加元素
map.put("name", "张三");
map.put("age", "18");
map.put("sex", "男");
map.put("school", "清华大学");
// String 实现了Comparable接口,所以TreeMap可以按照 Unicode 码排序
map.entrySet().forEach(entry -> System.out.println(entry.getKey() + "=" + entry.getValue()));
}
public static void main(String[] args) {
// 创建LinkedHashMap集合,指定排序规则
Map<String, String> map = new TreeMap<>(new Comparator<String>(){
@Override
public int compare(String o1, String o2) {
// 按照字符串长度比较(升序)
return o1.length() - o2.length();
}
});
// 添加元素
map.put("name", "张三");
map.put("age", "18");
map.put("sex", "男");
map.put("school", "清华大学");
// 遍历集合
Set<Entry<String, String>> entries = map.entrySet();
for (java.util.Map.Entry<String, String> entry : entries) {
System.out.println(entry.getKey() + "=" + entry.getValue());
}
}
7.Properties
Properties
类是 Hashtable
子类的一个特殊实例,提供了对持久性键值对存储的支持,其中键和值都是字符串类型。这种结构常常被用于配置文件的读写,尤其是在需要保存和加载程序设置或配置数据的场合。
其类图如下:
Properties
类的一个重要特性是其与流的交互能力。这种能力通过 load
和 store
方法来实现。load
方法可以从输入流(如文件输入流)中读取属性列表,而 store
方法则可以将属性列表写入到输出流。这两个方法使得 Properties
对象可以轻松地保存到文件或从文件中加载,非常适合持久化设置和配置数据。
在操作 Properties
对象时,推荐使用 setProperty
和 getProperty
方法。setProperty
方法用于插入或修改键值对,它接受两个字符串参数,分别代表键和值。getProperty
方法则用于根据键来获取对应的值,它接受一个字符串参数代表键,如果键存在则返回对应的值,否则返回 null
。
除此之外,Properties
类还提供了一些其他有用的方法。例如,propertyNames
方法可以返回所有键的枚举,stringPropertyNames
方法则可以返回所有键的字符串集合。list
方法可以将属性列表打印到指定的输出流,这对于调试和日志记录非常有用。
下面是一些简单使用案例:
public static void main(String[] args) {
// 使用 System.getProperties() 方法获取系统属性集
Properties properties = System.getProperties();
// 获取系统属性集中的编码
String encoding = properties.getProperty("file.encoding");
// 输出编码
System.out.println(encoding);
}
public static void main(String[] args) {
// 创建Properties集合
Properties properties = new Properties();
// 添加元素
properties.setProperty("name", "张三");
properties.setProperty("age", "18");
// 输出集合
System.out.println(properties); // {name=张三, age=18}
}
8.Set 集合与 Map 集合的关系
在 Java 的集合框架中,Set
接口的各个实现其实是建立在 Map
接口实现的基础上的。这是由于 Set
和 Map
之间的基本关系:Set
是不包含重复元素的集合,而 Map
则是键值对的集合,且键是唯一的。因此,我们可以将 Set
视为只有键的 Map
。
具体来说,HashSet
、TreeSet
和 LinkedHashSet
都是 Set
接口的主要实现,它们分别基于 HashMap
、TreeMap
和 LinkedHashMap
构建。
HashSet
是最常用的 Set
实现,它的内部就是一个 HashMap
实例。在这个 HashMap
中,HashSet
的元素作为键,值则是一个固定的 Object 对象,因为对于 Set
来说,我们只关心键,不关心值。
关键部分源码如下:
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable{
private static final Object PRESENT = new Object();
public HashSet() {
map = new HashMap<>();
}
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
}
TreeSet
的内部实现是 TreeMap
,它是一个有序的 Set
,能确保元素按照自然顺序或自定义的顺序进行排序。和 HashSet
类似,TreeSet
中的元素实际上是存储在 TreeMap
的键中的,而对应的值则是一个固定的 Object 对象。
关键部分源码如下:
public class TreeSet<E> extends AbstractSet<E> implements NavigableSet<E>, Cloneable, java.io.Serializable{
private static final Object PRESENT = new Object();
public TreeSet() {
this(new TreeMap<>());
}
public boolean add(E e) {
return m.put(e, PRESENT)==null;
}
}
LinkedHashSet
则是基于 LinkedHashMap
来实现的。它不仅能保证元素的唯一性,还可以记住元素的插入顺序。和其他两种 Set
实现一样,LinkedHashSet
的元素也是存储在 LinkedHashMap
的键中的。
关键部分源码如下:
public class LinkedHashSet<E> extends HashSet<E> implements Set<E>, Cloneable, java.io.Serializable {
public LinkedHashSet() {
super(16, .75f, true);
}
}
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable{
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
}
9.总结
下面是一个简单的总结表格:
特性 | HashMap | Hashtable | LinkedHashMap | TreeMap | Properties |
---|---|---|---|---|---|
Key 的顺序 | 无序 | 无序 | 插入顺序(或访问顺序) | 有序(自然排序、定制排序) | 无序 |
允许 null 值 | 是 | 否 | 是 | 是 | 是 |
线程安全 | 否 | 是 | 否 | 否 | 是 |
性能 | 高 | 低 | 较高 | 低 | 中等 |
基于 | Hash表 | Hash表 | Hash表+链表 | 红黑树 | Hash表 |
特殊功能 | 无 | 无 | 记录插入顺序 | 排序 | 存储字符串 |