java集合(四)Map集合之EnumMap详解

一、EnumMap 概述

EnumMap 是一个用于存储 key 为枚举类型的 map,底层使用数组实现(K,V 双数组)。下面是其继承结构:

public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V>
    implements java.io.Serializable, Cloneable

从上面的继承结构上可以看出 EnumMap 的 key 必须是一个枚举类型,而 value 没有限制。

1.1 内部属性

    // key 类型
    private final Class<K> keyType;

    // key 数组
    private transient K[] keyUniverse;

    // value 数组
    private transient Object[] vals;

    // 键值对个数
    private transient int size = 0;

    // value 为 null 时对应的值
    private static final Object NULL = new Object() {
        public int hashCode() {
            return 0;
        }

        public String toString() {
            return "java.util.EnumMap.NULL";
        }
    };

与其他类型 map 不同的是 EnumMap 底层使用双数组来存储 key 与 value,key 数组会在构造函数中根据 keyType 进行初始化,下面我们会看到。当 EnmumMap 的 value 为 null 时会特殊处理为一个 Object 对象。

1.2 构造函数

EnumMap 共提供了 3 个构造函数,如下:

下面我们只来看其中一个指定类型的构造函数。

    public EnumMap(Class<K> keyType) {
        this.keyType = keyType;
        // 初始化 key 数组,getKeyUniverse 方法会计算出枚举元素的总数并初始化 key 数组
        keyUniverse = getKeyUniverse(keyType);
        // 初始化 value 数组大小
        vals = new Object[keyUniverse.length];
    }

在使用上述构造函数初始化 EnumMap 的时候必须指定枚举类型,上面我们已经说过,EnumMap 会在构造函数中初始化 key 数组,这个初始化动作是在 getKeyUniverse(keyType) 中完成的。

    private static <K extends Enum<K>> K[] getKeyUniverse(Class<K> keyType) {
        return SharedSecrets.getJavaLangAccess()
                                        .getEnumConstantsShared(keyType);
    }

一开始看上面的代码可能有点懵,这怎么就初始化了 key 数组呢?在 Java 中我们可以通过 JavaLangAccess 和 SharedSecrets 来获取 JVM 中对象实例,具体是怎么实现的,有兴趣的可以查相关的资料了解下。

我们以 debug 形式来验证下 key 数组是否会在构造函数中被初始化与赋值:

首先来声明一个枚举类型:

enum Season {
    SPRING("春天"), SUMMER("夏天"), FALL("秋天"), WINTER("冬天");

    private final String name;

    Season(String name) {
        this.name = name;
    }
}

测试类:

    public static void main(String[] args) throws Exception {
        EnumMap<Season, String> map = new EnumMap<>(Season.class);
    }

我们把断点打在其构造函数上就会看到 keyUniverse 数组被初始化了,且数组的元素顺序与在枚举类型中定义的顺序一致。如下图:

1.3 使用方式

    public static void main(String[] args) throws Exception {
        EnumMap<Season, String> map = new EnumMap<>(Season.class);
        map.put(Season.FALL, "硕果累累的秋天");
        map.put(Season.WINTER, "寒风凛冽的冬天");
        System.out.println(map.get(Season.FALL));
    }

二、相关源码分析

2.1 put 方法

    public V put(K key, V value) {
        // key 类型检查
        typeCheck(key);

        // 获得该 key 对应的位置
        int index = key.ordinal();
        // 在 vals 数组中获取 key 角标对应的 value
        Object oldValue = vals[index];
        // 覆盖或设置 value
        vals[index] = maskNull(value);
        // 如果 key 对应的位置 value 为 null,则表示新插入了键值对,size++,反之表示值覆盖 size 不变
        if (oldValue == null)
            size++;
        return unmaskNull(oldValue);
    }

在添加键值对的时候会先检查 key 的类型,如果 key 的类型不一致会抛出异常。

    private void typeCheck(K key) {
        Class<?> keyClass = key.getClass();
        if (keyClass != keyType && keyClass.getSuperclass() != keyType)
            throw new ClassCastException(keyClass + " != " + keyType);
    }

PS: keyType 在构造函数中已经被初始化了。

EnumMap 存储键值对时并不会根据 key 获取对应的哈希值,enum 本身已经提供了一个 ordinal() 方法,该方法会返回具体枚举元素在枚举类中的位置(从 0 开始),因此一个枚举元素从创建就已经有了一个唯一索引与其对应,这样就不存在哈希冲突的问题了。

如果添加的 value 为 null 会通过 maskNull 方法特殊处理,存储一个 Object 对象。

    private Object maskNull(Object value) {
        return (value == null ? NULL : value);
    }

如果值覆盖的话,put 方法会返回旧的 value 值,并特殊处理 value 为 null 的情况:

    private V unmaskNull(Object value) {
        return (V)(value == NULL ? null : value);
    }

EnmuMap 添加键值对并没有扩容操作,因为一个枚举类型到底有多少元素在代码运行阶段是确定的,在构造函数中已经对 key 数组进行了初始化与赋值,value 数组的大小也已经被确定。还有一个需要注意的问题,在上面的 put
方法中只对 value 进行了处理,并没有处理 key,原因就是 key 数组在构造函数中已经被赋值了。

2.2 remove 方法

     public V remove(Object key) {
        // key 类型错误的时候直接返回 null
        if (!isValidKey(key))
            return null;
        // 根据 key 计算出其在枚举中位置
        int index = ((Enum<?>)key).ordinal();
        // 获取对应的 value
        Object oldValue = vals[index];
        // value 置 null,下次 GC 回收
        vals[index] = null;
        // 如果对应的 value 不为 null,如果添加键值对的时候 value 为 null,则存储的是 NULL(Object)
        if (oldValue != null)
            size--;
        return unmaskNull(oldValue);
    }

在移除键值对的时候会先调用 isValidKey 方法对 key 进行一次检查:

    private boolean isValidKey(Object key) {
        // key 为 null 直接返回 false
        if (key == null)
            return false;

        // Cheaper than instanceof Enum followed by getDeclaringClass
        Class<?> keyClass = key.getClass();
        // key 类型检查
        return keyClass == keyType || keyClass.getSuperclass() == keyType;
    }

remove 方法相对来说比较简单,这里就不总结了。

2.3 a question

从上面的源码分析中我们知道,key 数组自从在构造函数中完成初始化之后就没有执行过增删改的操作,是不是意味着我们根据枚举类型创建一个 EnumMap 之后,就算不添加任何键值对,也能根据其迭代器获取所有的 key,因为 key 在构造函数中已经被赋值了。看下面的代码:

    public static void main(String[] args) throws Exception {
        EnumMap<Season, String> map = new EnumMap<>(Season.class);
        // 获取迭代器对象
        Iterator<Map.Entry<Season, String>> iterator = map.entrySet().iterator();

        while (iterator.hasNext()) {
            System.out.println(iterator.next().getKey());
        }
    }

结果是上面的代码并不会输出任何 key,原因就在于 EnumMap 的 hasNext() 方法中对 value 做了非空判断,如下:

        public boolean hasNext() {
            // 循环中会略过 value 数组中为 null 的情况
            while (index < vals.length && vals[index] == null)
                index++;
            return index != vals.length;
        }

尽管在构造函数中 key 数组已经被初始化,但是如果对应的 value 为 null,在迭代的时候也会被过滤掉。

EnumMap 相对来说比较简单,关于源码就介绍到这里。jdk1.8 更多的源码分析,请点击这里前往:jdk1.8 源码阅读

原文地址:https://www.cnblogs.com/yuexiaoyun/p/12189678.html

时间: 2024-10-23 22:42:11

java集合(四)Map集合之EnumMap详解的相关文章

Java ExecutorService四种线程池使用详解

1.引言 合理利用线程池能够带来三个好处.第一:降低资源消耗.通过重复利用已创建的线程降低线程创建和销毁造成的消耗.第二:提高响应速度.当任务到达时,任务可以不需要的等到线程创建就能立即执行.第三:提高线程的可管理性.线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控.但是要做到合理的利用线程池,必须对其原理了如指掌. 2.线程池使用 Executors提供的四种线程 1.newCachedThreadPool创建一个可缓存线程池

java集合HashMap、HashTable、HashSet详解

一.Set和Map关系 Set代表集合元素无序,集合元素不可重复的集合,Map代表一种由多个key-value组成的集合,map集合是set集合的扩展只是名称不同,对应如下 二.HashMap的工作原理 HashMap基于hashing原理,通过put()和get()方法储存和获取对象. put()方法: 它调用键对象的hashCode()方法来计算hashcode值,系统根据hashcode值决定该元素在bucket位置.如果两个对象key的hashcode返回值相同,那他们的存储位置相同,如

java集合(List集合与Map集合的数据转换)

List集合与Map集合的数据转换 实现List和Map数据的转换. 具体要求如下: 功能1:定义方法public void listToMap( ){ }将List中Student元素封装到Map中 1)使用构造方法Student(int id,String name,int age,String sex )创建多个学生信息并加入List 2) 遍历List,输出每个Student信息 3) 将List中数据放入Map,使用Student的id属性作为key,使用Student对象信息作为va

java集合框架--Map集合

1.Map集合的概述 Map集合是将键映射到值的对象.一个映射不能包含重复的键.每个键最多只能映射到一个值. 2.Map接口和Collection接口的不同? Map集合存储元素是成对出现的,Collection集合存储元素是单独出现的. Map集合的键是唯一的,值是可重复的. Collection集合的子接口Set是唯一的,List是可重复的. Map集合的数据结构值针对键有效,和值无关,而Collection接口的数据结构是针对元素有效. 3.Map集合示例及功能 package cn; i

java学习笔记—集合之Map集合

p.p1 { margin: 0.0px 0.0px 0.0px 0.0px; text-align: center; font: 12.0px Times } p.p2 { margin: 0.0px 0.0px 0.0px 0.0px; font: 12.0px "Songti SC" } p.p3 { margin: 0.0px 0.0px 0.0px 0.0px; font: 12.0px Times } p.p4 { margin: 0.0px 0.0px 0.0px 0.0

(1)集合 ---遍历map集合

Map接口     实现Map接口的类用来存储键(key)-值(value) 对.Map 接口的实现类有HashMap和TreeMap等.Map类中存储的键-值对通过键来标识,所以键值不能重复. HashMap: 线程不安全,效率高. 允许key或value为nullHashTable:线程安全,效率低. 不允许key或value为nullProperties : HashTable的子类,key和value都是string常用的方法: Object put(Object key, Object

Java基础13:反射与注解详解

Java基础13:反射与注解详解 什么是反射? 反射(Reflection)是Java 程序开发语言的特征之一,它允许运行中的 Java 程序获取自身的信息,并且可以操作类或对象的内部属性. Oracle官方对反射的解释是 Reflection enables Java code to discover information about the fields, methods and constructors of loaded classes, and to use reflected fi

Java的String和StringBuffer和StringBuilder详解

Java的String和StringBuffer和StringBuilder详解 作者:chszs,转载需注明.博客主页:http://blog.csdn.net/chszs 前言 最近发现团队成员在Java代码方面的质量不够高,准备写一些基础的文章,供大家参考. 一.定义 String是不可变字符序列. StringBuffer是可变的字符序列. StringBuilder也是可变的字符序列. 1.StringBuffer和StringBuilder的唯一区别 StringBuffer对象是线

Java进阶(三十二) HttpClient使用详解

Java进阶(三十二) HttpClient使用详解 Http协议的重要性相信不用我多说了,HttpClient相比传统JDK自带的URLConnection,增加了易用性和灵活性(具体区别,日后我们再讨论),它不仅是客户端发送Http请求变得容易,而且也方便了开发人员测试接口(基于Http协议的),即提高了开发的效率,也方便提高代码的健壮性.因此熟练掌握HttpClient是很重要的必修内容,掌握HttpClient后,相信对于Http协议的了解会更加深入. 一.简介 HttpClient是A

java笔记--反射进阶之总结与详解

一.反射进阶之动态设置类的私有域 "封装"是Java的三大特性之一,为了能更好保证其封装性,我们往往需要将域设置成私有的, 然后通过提供相对应的set和get方法来操作这个域.但是我们仍然可以用java的反射机制来 修改类的私有域,由于修改类的私有域会破坏Java"封装"的特性,故请慎重操作. 主要技术:     Field类提供有关类或接口的单个字段的信息,以及对它的动态访问权限.     访问的字段可能是一个类(静态)字段或实例字段.             常