Java 集合 - Map 接口

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", "[email protected]");

        // 输出集合
        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 接口的实现类,如 ListSet,我们通常使用 foreach 循环或 Iterator 对象进行遍历。然而,对于 Map 接口,情况则有所不同。

由于 Map 接口没有继承 java.lang.Iterable<T> 接口,也没有实现 Iterator iterator() 方法,所以我们不能直接使用 foreach 循环或 Iterator 对象遍历 Map。不过,我们仍有其他方法可以有效地遍历 Map

Map 接口声名如下:

public interface Map<K, V> {
    
    
    // ......
}

一种方法是分别遍历所有的键和值。这种方法的实现非常简单,我们可以直接调用 MapkeySet() 方法和 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", "[email protected]");

    // 单独遍历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

HashMapHashtable 是 Java 中两个经典的基于哈希表的数据结构。它们都实现了 Map 接口,可以存储键值对,而且在内部都使用了哈希表以达到快速查找的效果。虽然在表面上它们看起来相似,但在实现细节和用法上,它们有着重要的不同。

HashMap 类图如下:

Hashtable 类图如下:

首先,无论是 HashMap 还是 Hashtable,它们判断两个键是否相等的标准都是:两个键的 hashCode 方法返回值相等,并且它们的 equals 方法返回 true。这是因为哈希表通过哈希函数(在 Java 中是对象的 hashCode 方法)将键映射到内部的数组中,然后通过键的 equals 方法来处理哈希冲突。因此,如果你计划使用自定义对象作为键,你必须正确地重写 hashCodeequals 方法,以确保对象在哈希表中能被正确地存储和查找。

尽管 HashMapHashtable 在判断键相等的方式上相同,但它们在其他方面存在显著的不同。最重要的差异在于并发控制和对 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

LinkedHashMapHashMap 的一个子类,它在 HashMap 的基础上提供了顺序迭代的能力。这个顺序通常就是元素被插入到映射中的顺序,也被称为插入顺序。此外,LinkedHashMap 还提供了一种称为访问顺序的迭代方式

LinkedHashMap 的类图如下:

LinkedHashMapHashMap 的主要不同之处在于其内部数据结构。在 HashMap 中,每个键值对(即 Map.Entry 对象)都被插入到内部的哈希表中。而LinkedHashMap 中,除了哈希表之外,还维护了一个双向链表。每个 Map.Entry 对象都被插入到这个链表的尾部,形成一个按插入顺序排序的键值对链表

由于这个双向链表的存在,我们可以通过 LinkedHashMap 的迭代器顺序访问每个键值对,这个顺序反映了键值对的插入顺序或访问顺序。插入顺序意味着键值对的迭代顺序就是它们被插入到 LinkedHashMap 中的顺序。而访问顺序则意味着键值对的迭代顺序是它们最后一次被访问(通过 getput 操作)的顺序。

创建 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 还提供了 subMapheadMaptailMap 方法,可以返回当前映射的一部分视图。

下面是一个简单使用示例:

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 类的一个重要特性是其与流的交互能力。这种能力通过 loadstore 方法来实现。load 方法可以从输入流(如文件输入流)中读取属性列表,而 store 方法则可以将属性列表写入到输出流。这两个方法使得 Properties 对象可以轻松地保存到文件或从文件中加载,非常适合持久化设置和配置数据。

在操作 Properties 对象时,推荐使用 setPropertygetProperty 方法。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 接口实现的基础上的。这是由于 SetMap 之间的基本关系:Set 是不包含重复元素的集合,而 Map 则是键值对的集合,且键是唯一的。因此,我们可以将 Set 视为只有键的 Map

具体来说,HashSetTreeSetLinkedHashSet 都是 Set 接口的主要实现,它们分别基于 HashMapTreeMapLinkedHashMap 构建。

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表
特殊功能 记录插入顺序 排序 存储字符串

猜你喜欢

转载自blog.csdn.net/ly1347889755/article/details/130905261