Collections(七)Map和Set下篇

本文深入探讨了Java集合框架中的Map和Set实现,包括LinkedHashMap、TreeMap、EnumMap、WeakHashMap、IdentityHashMap以及HashSet、TreeSet和LinkedHashSet的原理与应用场景。详细解释了LinkedHashMap如何通过链表保持元素顺序,TreeMap如何使用红黑树实现排序,EnumMap针对枚举类型的优化,WeakHashMap的弱引用特性,以及IdentityHashMap的引用相等判断。同时介绍了Set如何基于Map实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文接着上篇继续探讨更多的Map实现和Set实现。

LinkedHashMap

LinkedHashMap是一个有序的HashMap,它 继承了HashMap类,并且通过内部维护一个链表来保证迭代时有序

static class Entry<K,V> extends HashMap.Node<K,V> {
    Entry<K,V> before, after;
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}

LinkedHashMap.Entry<K,V> head;

LinkedHashMap.Entry<K,V> tail;

关于LinkedHashMap,它的构造器有个有趣的参数accessOrder,这个参数代码有序模式,默认是false,表示按照插入顺序(insertion-order), 如果传入true,则是按照访问顺(access-order):

public LinkedHashMap(int initialCapacity,
                     float loadFactor,
                     boolean accessOrder) {
    super(initialCapacity, loadFactor);
    this.accessOrder = accessOrder;
}

访问顺序的意思是每次插入元素都会在链表尾部插入,因为这是最近访问的元素,如果是获取元素,则将获取的元素移到链表尾部,链表尾部是最近访问,如果我们限定LinkedList大小,很容易实现一个 LRU缓存算法

package com.deepoove.datastructure;

import java.util.LinkedHashMap;

public class LRUCache implements Cache {

    private LinkedHashMap<String, Object> cache;
    private int size;
    
    public LRUCache(int capacity) {
        size = capacity;
        cache = new LinkedHashMap<>(capacity, 0.75f, true);
    }

    @Override
    public void put(String key, Object value) {
        synchronized (this) {
            if (cache.size() >= size){
                cache.remove(cache.keySet().iterator().next());
            }
            cache.put(key, value);
        }
    }

    @Override
    public Object get(String key) {
        return cache.get(key);
    }
    
    @Override
    public String toString() {
        return cache.toString();
    }
}

这里的元素没有保存访问次数,改进的LRU算法可以将访问次数也作为排序的一个条件。

LinkedHashMap还提供了 removeEldestEntry一种方法,可以重写该方法,这个方法的返回值是个boolean值,以便在将新映射添加到Map时强制执行自动删除过时映射的策略,这使得实现自定义缓存变得非常容易。

TreeMap

TreeMap是一个按照顺序排序的结构,关于顺序性(Comparable和Comparator),在第一篇概览中介绍过了。

TreeMap间接实现了SortedMap接口,实现基础是一个红黑树, 正如HashMap中对恶化的链表的优化。每个节点的数据结构如下:

static final class Entry<K,V> implements Map.Entry<K,V> {
    K key;
    V value;
    Entry<K,V> left;
    Entry<K,V> right;
    Entry<K,V> parent;
    boolean color = BLACK;
}

性能

从实现角度来说,如果不希望保证顺序,那应该选用HashMap,因为LinkedHashMap还额外维护了一个链表,HashMap查找一个元素最快是O(1),性能最差的情况下会是O(logN)。

HashMap的性能取决于以下三点:

  • 负载因子:默认值0.75是个空间和时间上最优的考量,不建议修改
  • 容量:我们应当根据预期的存储元素个数来选择合适的初始容量,从而避免重新计算Hash去扩容
  • 哈希函数:默认是通hashcode方法通过计算来实现的

接下来会介绍几个专用的Map,这些都是一些优化这三点或者特殊场景的实现。

Map特殊用途实现

EnumMap

如果我们希望一个枚举类型映射一个对象,代码可能是这样的:

HashMap<DAY, String> map = new HashMap<DAY, String>();
map.put(DAY.MONDAY, "星期一");

JDK提供了一个特殊的Map实现EnumMap来做这样的事情,顾名思义,它并不是一个基于哈希表的实现(名称上没有hash单词):

// DAY是一个枚举类
EnumMap<DAY, String> eummap = new EnumMap<>(DAY.class);
eummap.put(DAY.MONDAY, "星期一");

那么这两者之间有什么差异,为何要新实现一个EnumMap呢?
这是由于枚举的特殊性,每个枚举值都有个整型常量值,它是枚举声明的序数ordinal所以我们无需哈希函数,或者说哈希函数就是

f(x)=ordinal=index

因为枚举常量值的个数一定的,所以根本无需扩容,它的容量就是枚举常量的个数,所有不需要大材小用使用哈希去实现枚举对象的映射关系。

EnumMap是一个基于固定长度数组Object[] vals为实现的Map,长度为枚举常量的个数,下标是枚举的整型值,当使用枚举作为Key时,我们应当使用这个类来代替HashMap。

WeakHashMap

WeakHashMap是一个Key为弱引用的Map,当这个Key没有强引用的时候,就有可能被垃圾回收器回收。

WeakReferenc类是对一个对象的装饰,继承Reference类,提供了get方法获取这个对象,WeakHashMap的Entry继承了这WeakReferenc。

private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
    V value;
    final int hash;
    Entry<K,V> next;

    Entry(Object key, V value,
          ReferenceQueue<Object> queue,
          int hash, Entry<K,V> next) {
        super(key, queue);
        this.value = value;
        this.hash  = hash;
        this.next  = next;
    }

    @SuppressWarnings("unchecked")
    public K getKey() {
        return (K) WeakHashMap.unmaskNull(get());
    }

    public boolean equals(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry<?,?> e = (Map.Entry<?,?>)o;
        K k1 = getKey();
        Object k2 = e.getKey();
        if (k1 == k2 || (k1 != null && k1.equals(k2))) {
            V v1 = getValue();
            Object v2 = e.getValue();
            if (v1 == v2 || (v1 != null && v1.equals(v2)))
                return true;
        }
        return false;
    }
    // 略
}

源码中可以看到,每个key都会调用super(key, queue);构造为弱引用,在getKey方法中,通过java.lang.ref.Reference.get()方法获取对象。

IdentityHashMap

IdentityHashMap是一个哈希函数特别的基于哈希表的Map结构,它的Key是通过引用相等(reference-equality)来判断的,而我们熟知的HashMap的key是通过对象相等(object-equality)来判断。

我们看看它计算hash的算法:

private static int hash(Object x, int length) {
    int h = System.identityHashCode(x);
    // Multiply by -127, and left-shift to use least bit as part of hash
    return ((h << 1) - (h << 8)) & (length - 1);
}

System.identityHashCode会返回这个对象的hashcode,无论这个对象的类是否实现了hashCode方法,即当且仅当Key1=Key2,它们才会被认为是相同的关键字,IdentityHashMap可以利用在深拷贝的实现上。

Map并发实现

并发包下提供了接口ConcurrentMap和基于哈希表的高并发实现ConcurrentHashMap,具体会在并发章节中阐述。

Set

Set通用实有:HashSet,TreeSet和LinkedHashSet,和Map的命名很相似,Set接口继承于Collection接口,而Collection和Map接口是相对独立的,为什么要把Map和Set放在同一个章节里介绍呢?

因为JDK做了一个巧妙的实现,这三种Set实现底层都是基于对应的Map实现的,Set的集合就是Map中Key的集合(不允许重复),Map重的value统一设置成了一个固定对象new Object():

private transient HashMap<E,Object> map;

// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();

public HashSet() {
    map = new HashMap<>();
}

public HashSet(Collection<? extends E> c) {
    map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
    addAll(c);
}

关于Set实现原理请参见Map的实现原理,Set也提供了高性能的枚举实现EnumSet,在并发包下,提供了写时复制的类CopyOnWriteArraySet。

转载于:https://my.oschina.net/LeBronJames/blog/3101538

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值