文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Java Map 和 Set 全面解析

2024-11-28 14:35

关注

详解Map集合

Map接口定义概览

Map即映射集,是线上就是将对应的key映射到对应value上,由此构成一个数学上的映射的概念,该适合存储不可重复键值对类型的元素,key不可重复,value可重复,我们可以通过key找到对应的value:

An object that maps keys to values. A map cannot contain duplicate keys; each key can map to at most one value.

HashMap(重点)

JDK1.8的HashMap默认是由数组+链表/红黑树组成,通过key算得hash寻址从而定位到Map底层数组的索引位置。在进行put操作时,若冲突时使用拉链法解决冲突,如下面这段代码所示,当相同索引位置存储的是链表时,它会进行for循环定位到相同hash值的索引位置的尾节点进行追加。当链表长度大于8且数组长度大于64的情况下,链表会进行树化变成红黑树,减少元素搜索时间。

注意 : 若长度小于64链表长度大于8只会HashMap底层数组的扩容操作,对此我们给出Map进行put操作时将元素添加到链表结尾的代码段,可以看到当链表元素大于等于8时会尝试调用treeifyBin进行树化操作:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
 //......
 for (int binCount = 0; ; ++binCount) {
     //遍历找到空位尝试插入
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //如果链表元素加上本次插入元素大于8( TREEIFY_THRESHOLD )时,则尝试进行树化操作
                        if (binCount >= TREEIFY_THRESHOLD - 1) 
                            treeifyBin(tab, hash);
                        break;
                    }
                   //.......
                }
                //......
}

对应我们步入treeifyBin即可直接印证我们的逻辑,当底层数组空间小于64时只会进行数组扩容,而非针对当前bucket的树化操作:

final void treeifyBin(Node[] tab, int hash) {
        int n, index; Node e;
        //若当前数组空间小于64则直接会进行数组空间扩容
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {//反之进行树化操作
            TreeNode hd = null, tl = null;
            do {
               //......
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }

详解Hashtable核心添加操作

Hashtable底层也是由数组+链表(主要用于解决冲突)组成的,操作时都会上锁,可以保证线程安全,底层数组是 Hashtable 的主体,在插入发生冲突时,会通过拉链法即将节点追加到同索引位置的节点后面:

public synchronized V put(K key, V value) {
        //......

       //插入元素寻址操作
        Entry tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        //修改操作时,通过遍历对应index位置的链表完成覆盖操作
        @SuppressWarnings("unchecked")
        Entry entry = (Entry)tab[index];
        for(; entry != null ; entry = entry.next) {
            if ((entry.hash == hash) && entry.key.equals(key)) {
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }
  //发生冲突时,会将节点追加到同索引位置的节点后面
        addEntry(hash, key, value, index);
        return null;

详解Set集合

Set基本核心概念

Set集合不可包重复的元素,即如果两个元素在equals方法下判定为相等就只能存储其中一个,这意味着该集合也最多包含一个null的元素,它常用于一些需要进行去重的场景。

//HashSet底层复用了Map的put方法,value统一使用PRESENT对象
private static final Object PRESENT = new Object();

public boolean add(E e) {
  // 返回null说明当前插入时并没有覆盖相同元素
        return map.put(e, PRESENT)==null;
    }

Set有两种我们比较熟悉的实现:

//HashSet底层复用了Map的put方法,value统一使用PRESENT对象
private static final Object PRESENT = new Object();

public boolean add(E e) {
  // 返回null说明当前插入时并没有覆盖相同元素
        return map.put(e, PRESENT)==null;
    }

A Red-Black tree based NavigableMap implementation. The map is sorted according to the natural ordering of its keys, or by a Comparator provided at map creation time, depending on which constructor is used.

对应的从源码中我们也可以看出TreeSet底层是对TreeMap的复用:

public TreeSet() {
        this(new TreeMap());
    }

HashMap 和 Hashtable 的区别(重点)

针对该问题,笔者建议从以下5个角度进行探讨:

(1) 从线程安全角度:HashMap 操作是线程不安全、Hashtable 是线程安全,这一点我们已经在上文中源码进行了相应的介绍,这里就不多做赘述了。

(2) 从底层数据结构角度:HashMap 初始情况是数组+链表,特定情况下会变数组+红黑树,Hashtable 则是数组+链表。

(3) 从保存数值角度:HashMap 允许null键或null值,而Hashtable则不允许null的value,这一点我们可以直接查看的Hashtable的put方法:

public synchronized V put(K key, V value) {
        // 如果value为空则抛出空指针异常
        if (value == null) {
            throw new NullPointerException();
        }
  //......
    }

(4) 从初始容量角度考虑:HashMap默认16,对此我们可以通过直接查看源码的定义印证这一点:

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

同时HashMap进行扩容时都是基于当前容量*2,这一点我们可以直接通过resize印证:

  final Node[] resize() {
       //......
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //基于原有数组容量*2得到新的数组空间完成map底层数组扩容
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
   Node[] newTab = (Node[])new Node[newCap];
//......
}

Hashtable 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1:

//初始化容量为11
 public Hashtable() {
        this(11, 0.75f);
    }

 //扩容方法
   protected void rehash() {
        int oldCapacity = table.length;
        Entry[] oldMap = table;

        //扩容的容量基于原有空间的2倍+1
        int newCapacity = (oldCapacity << 1) + 1;
        //......
}

(5) 从性能角度考虑:Hashtable 针对冲突总是通过拉链法解决问题,长此以往可能会导致节点查询复杂度退化为O(n)相较于HashMap在达到空间阈值时通过红黑树进行bucket优化来说性能表现会逊色很多。

HashMap 和 HashSet有什么区别

HashSet 聚合了HashMap ,通俗来说就是将HashMap 的key作为自己的值存储来使用:

//HashSet底层本质是通过内部聚合的map完成元素插入操作
public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

HashMap 和 TreeMap 有什么区别

类图如下,TreeMap 底层是有序树,所以对于需要查找最大值或者最小值等场景,TreeMap 相比HashMap更有优势。因为他继承了NavigableMap接口和SortedMap 接口。

如下源码所示,我们需要拿最大值或者最小值可以用这种方式或者最大值或者最小值:

 Object o = new Object();
        //创建并添加元素
        TreeMap treeMap = new TreeMap<>();
        treeMap.put(3213, o);
        treeMap.put(434, o);
        treeMap.put(432, o);
        treeMap.put(2, o);
        treeMap.put(432, o);
        treeMap.put(31, o);
        //顺序打印
        System.out.println(treeMap);
        //拿到第一个key
        System.out.println(treeMap.firstKey());
        //拿到最后一个key
        System.out.println(treeMap.lastEntry());

输出结果:

{2=231, 31=231, 432=231, 434=231, 3213=231}
2
3213=231

HashSet实现去重插入的底层工作机制了解嘛?

当你把对象加入HashSet时其底层执行步骤为:

对此我们给出HashSet底层的去重的实现,本质上就算map的putVal方法:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
       //......
       //bucet不存在元素则直接插入
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node e; K k;
            //若存在元素,且hash、key、equals相等则覆盖元素并返回旧元素
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                  //......
                    //若存在元素,且hash、key、equals相等则覆盖元素并返回旧元素
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //返回旧有元素的值
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                //......
                return oldValue;
            }
        }
        //......
        return null;
    }

HashSet、LinkedHashSet 和 TreeSet 使用场景

来源:写代码的SharkChili内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯