Java 集合 - Map 接口

2023-11-03

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 接口的实现类,如 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", "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

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表
特殊功能 记录插入顺序 排序 存储字符串
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Java 集合 - Map 接口 的相关文章

随机推荐

  • Linux和AIX主机获取主机帐号创建时间和最后一次登录时间

    最近在获取Linux和Aix主机帐号创建时间和最后一次登录时间方法 跟大家分享下 如有不对多多指教 首先说一下对于Linux而言没有创建时间这么一说 只能说是更新时间 lastlog u test 最后一次登录 Linux 推荐 awk F
  • Mybatis-plus开启sql日志打印

    第一种 mybatis plus 设置 mybatis plus configuration log impl org apache ibatis logging stdout StdOutImpl 开启sql日志 log impl org
  • 第四篇:小程序之缓存策略

    缓存key分为内存缓存和本地缓存 如果是持久化的 需要缓存到本地中 但是 小程序中对于一些特殊结构的对象 如Map Set的缓存是不支持的 那如果非要缓存呢 该如何实现呢 且听我娓娓道来 点击我查看完整项目 一 内存缓存 java中 内存缓
  • 谷歌浏览器:拷贝为CURL的小技巧

    1 美图 2 背景 一个项目要写shell 要调用一个接口 这个接口很麻烦 传参很多 一个一个的弄很难 后来发现浏览器自带的小技巧 非常好用 拷贝的url是直接可以在命令行中执行的 curl http blog sina com cn s
  • 毕业设计-基于深度学习的加密及异常网络流量检测系统

    目录 前言 课题背景和意义 实现技术思路 一 相关理论与技术 三 基于流时空特征的加密流量识别模型 实现效果图样例 最后 前言 大四是整个大学期间最忙碌的时光 一边要忙着备考或实习为毕业后面临的就业升学做准备 一边要为毕业设计耗费大量精力
  • 路由交换-华为usg6000防火墙上配置内网外网通过公网ip访问http服务

    源nat是将私网地址转换为公网地址 实现内部网络访问外网 目的dnat是将对公网访问Ip转换为内网ip 实现外部网络访问内网资源 目的nat的实现有多种方式 一对一转换 带端口和不带端口的转换 最常用的就是使用带端口的一对多转换 即我们常说
  • Levinson-Durbin快速递推法功率谱估计(Python实现版)

    Levinson Durbin快速递推法功率谱估计是在Yule Walker方程法之上建立的 如果对于Yule Walker方程法不熟悉的话可以参考我的一篇博客 Yule Walker方程法参数化谱估计 Python实现版 声明 博客原本在
  • 文件上传漏洞upload-libs pass5

    文件上传漏洞upload libs pass4 首先查看源码 无法使用空格和大小写绕过 且黑名单过滤了 htaccess 查看提示 利用readme php文件 因为没有过滤ini文件 创建 text ini和一句话木马文件 内容为 aut
  • HIVE厂牌艺人_Labelwarts Vol. 2:洛杉矶天才厂牌 Odd Future Records 的开始到结束

    We re F kin Radical been F kin Awesome 我们太TMD激进 太TMD耀眼 Talked a lotta sh t so far words you re at a loss 说着一大堆胡话 让你们都不知所
  • 将ant design pro打包的JS分离出去

    通过analyze分析发现其实react dom并不算小 有100多kb 所以就想把它单独引用 于是就在config ts增加 externals react window React react dom window ReactDOM b
  • 利用python3 生成密码本

    一 思路 1 把密码中含有哪些字符串都放入一个迭代器中 2 确定生成的密码是几位数的 3 将生成的所有密码写入一个文件里面 二 代码 import itertools as its 迭代器 words 1234567890 生成密码本的位数
  • 3.2 Python图像的频域图像增强-高通和低通滤波器

    3 2 Python图像的频域图像增强 高通和低通滤波器 文章目录 3 2 Python图像的频域图像增强 高通和低通滤波器 1 算法原理 1 1理想滤波器 1 2巴特沃斯滤波器 1 3指数滤波器 2 代码 3 效果 1 算法原理 高通和低
  • Mongodb笔记六:排序与限制输出

    一 排序 db collectionname find sort key1 1 key 1 这里的1代表升序 1代表降序 如 对所有人按年龄升序排序 降序排序 二 索引 索引是特殊的数据结构 索引存储在一个易于遍历读取的数据集合中 索引是对
  • FFmpeg中RTSP客户端拉流测试代码

    之前在https blog csdn net fengbingchun article details 91355410中给出了通过LIVE555实现拉流的测试代码 这里通过FFmpeg来实现 代码量远小于LIVE555 实现模块在liba
  • 蓝桥杯每日一题——手算题·空间

    本题为填空题 只需要算出结果后 在代码中使用输出语句将所填结果输出即可 小蓝准备用 256MB 的内存空间开一个数组 数组的每个元素都是 3232 位 二进制整数 如果不考虑程序占用的空间和维护内存需要的辅助空间 请问 56MB 的空间可以
  • [阶段二] 4. MySQL的基本操作

    mysql的基本操作 数据插入 INSERT 语句可以向数据表写入数据 可以是一条记录 也可以是多条记录 INSERT INTO 数据表名称 字段1 字段2 VALUES 值1 值2 插入一条记录 INSERT INTO 数据表名称 字段1
  • 分析工具 nvprof简介

    nvprof 是一个可用于Linux Windows和OS X的命令行探查器 使用 nvprof myApp 运行我的应用程序 我可以快速看到它所使用的所有内核和内存副本的摘要 摘要将对同一内核的所有调用组合在一起 显示每个内核的总时间和总
  • 十六进制转二进制

    public static String hexToBinary String hex if hex null hex length 2 0 return null String bString String tmp for int i 0
  • Visual Studio(VS) 编程推荐字体和主题设置

    首先是字体 工具 gt 选项 gt 环境 gt 字体和颜色 具体图如下 选择Consolas的原因 Consolas算是最常见的编码字体了 在很多的编译软件都是这个字体 而且在这个字体下的中英文标点和半角圆角符号也能有比较明显的区别 至于字
  • Java 集合 - Map 接口

    文章目录 1 概述 2 常用 API 3 遍历 Map 集合 4 HashMap 和 Hashtable 5 LinkedHashMap 6 TreeMap 7 Properties 8 Set 集合与 Map 集合的关系 9 总结 1 概