重学Java集合类(三)—— Map接口(上)

前言

Map接口储存一组成对的键-值对象,提供key(键)到value(值)的映射,Map中的key不要求有序,不允许重复。value同样不要求有序,但可以重复。

最常见的Map实现类是HashMap,他的储存方式是哈希表,优点是查询指定元素效率高。Map接口提供了将键映射到集合的对象,一个映射不能包含重复的键,每个键最多只能映射到一个值。

Map接口中同样提供了集合的常用方法,如clear()方法,isEmpty()方法,Size()方法等。

Map接口

public interface Map<K, V> {
    int size();
    boolean isEmpty();
    boolean containsKey(Object var1);
    boolean containsValue(Object var1);
    V get(Object var1);
    V put(K var1, V var2);
    V remove(Object var1);
    void putAll(Map<? extends K, ? extends V> var1);
    void clear();
    Set<K> keySet();
    Collection<V> values();
    Set<Map.Entry<K, V>> entrySet();
    boolean equals(Object var1);
    int hashCode();
    ……
}

常用实现类

Map
|------HashTable
|           |-------WeakHashTable
|------HashMap
|           |-------LinkedHashMap
|------TreeMap
|------WeakHashMap

HashMap

HashMap是基于哈希表的 Map 接口的实现,可以说是平时用途最广泛的Map实现类。从内部结构上来讲,传统的HashMap是一种“数组+链表”的结构,通过对象的hashcode定位数组中的位置,调用equal方法存取数据,JDK1.8中加入了红黑树处理逻辑。

内部成员

    //初始容量为16
    DEFAULT_INITIAL_CAPACITY = 1 << 4; 

    //最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30;

    //默认平衡因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    //桶的树化阈值,当桶中元素个数超过这个值时,需要使用红黑树节点替换链表节点
    static final int TREEIFY_THRESHOLD = 8;

    //树的链表还原阈值,当扩容时,桶中元素个数小于这个值,就会把树形的桶元素 还原(切分)为链表结构
    static final int UNTREEIFY_THRESHOLD = 6;

    //当哈希表中的容量大于这个值时,表中的桶才能进行树形化,否则桶内元素太多时会扩容,而不是树形化为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD
    static final int MIN_TREEIFY_CAPACITY = 64;

初始化

HashMap提供如下四种构造方法:

public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }

    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

HashMap初始化时需要两个参数:capacity和loadfactor,前者指HashMap的容量,初始化时会调用tableSizeFor方法,此方法的返回值为大于且最接近capacity的2的次幂,即最终桶的数量,也就是数组的实际长度。后者是平衡因子,是键值对数量除以数组的长度。

如果在初始化时不提供上述两个参数,HashMap便会使用默认值,比如初始容量是16,平衡因子是0.75。

关键方法

put方法

HashMap的特点在于快速存取元素,我们可以使用put方法添加元素,如下为put方法代码:

    public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

    static int indexFor(int h, int length) {
        return h & (length-1);
    }

具体步骤如下:

  • 1,调用hash(key)方法获取key的hashcode,再调用indexFor(hash, table.length)获取数组索引;
  • 2,判断table[i]是否为null,如果是null,直接新建节点添加,并转向4,如果table[i]不为null,转向3;
  • 3,遍历整个链表,判断链表元素是否和key一样,如果相同直接覆盖value,否则转向4,这里的相同指的是hashCode以及equals;
  • 4,先判断插入成功后实际存在的键值对数量size是否超多了最大容量threshold,如果超过,则扩大数组长度为原先的两倍;
  • 5,创建新结点,新节点的next指向原先数组相应链表的头结点。

需要注意的是,HashMap是先插入再扩容,这与有些Map实现类有所不同。同时,JDK1.8以后,在put实现逻辑中加入了树化的过程,读者可以另外查询相关资料信息。

get方法

HashMap的get方法实现逻辑如下:

public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }

    private V getForNullKey() {
        if (size == 0) {
            return null;
        }
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
    }

    final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }

        int hash = (key == null) ? 0 : hash(key);
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }

其实get方法的实现逻辑与put方法中寻找相应节点类似,从大体过程上来讲,分为两步:

  • 1,根据hashcode定位数组位置;
  • 2,遍历整个链表,使用equal方法判断key是否相同,相同则返回对应的value,若不存在,则返回null。

值得注意的是,HashMap允许存在key和value为null的情况,所以使用get方法返回值为null具有歧义,当需要判断是否存在指定key的键值对,请使用containsKey方法。

LinkedHashMap

LinkedHashMap是HashMap的子类,与HashMap有着同样的存储结构,但它加入了一个双向链表的头结点,将所有put到LinkedHashmap的节点一一串成了一个双向循环链表,此链接列表定义了迭代顺序,该迭代顺序可以是插入顺序或者是访问顺序。

内部成员

    private transient Entry<K,V> header;

    private final boolean accessOrder;

除了继承于HashMap的内部成员变量,LinkedHashMap额外多了两个成员变量。header是内部双向链表的头结点,accessOrder表示的是LinkedHashMap的迭代顺序,默认为false。当为false时表示按照插入顺序,当为true时表示按照访问顺序。后者经常用于LRU(最近最少使用)算法。

初始化

LinkedHashMap提供如下四种构造方法:

    public LinkedHashMap(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor);
        accessOrder = false;
    }

    public LinkedHashMap(int initialCapacity) {
        super(initialCapacity);
        accessOrder = false;
    }

    public LinkedHashMap() {
        super();
        accessOrder = false;
    }

    public LinkedHashMap(Map<? extends K, ? extends V> m) {
        super(m);
        accessOrder = false;
    }

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

可以看出,由于LinkedHashMap继承自HashMap,因此LinkedHashMap的初始化过程其实就是HashMap的初始化过程,最后设置accessOrder成员变量。

除此之外,LinkedHashMap内部的Entry< K, V >也继承自HashMap,并且增加了before和after两个引用,增加了双向链表的节点新增删除操作。

private static class Entry<K,V> extends HashMap.Entry<K,V> {
        Entry<K,V> before, after;

        Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
            super(hash, key, value, next);
        }

        private void remove() {
            before.after = after;
            after.before = before;
        }

        private void addBefore(Entry<K,V> existingEntry) {
            after  = existingEntry;
            before = existingEntry.before;
            before.after = this;
            after.before = this;
        }

        void recordAccess(HashMap<K,V> m) {
            LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
            if (lm.accessOrder) {
                lm.modCount++;
                remove();
                addBefore(lm.header);
            }
        }

        void recordRemoval(HashMap<K,V> m) {
            remove();
        }
    }

关键方法

LinkedHashMap继承于HashMap,保留了HashMap的put、get方法逻辑。由于内部维护了双向链表,在新增、删除和读取的过程中额外新增了链表操作。

put方法

在put方法中,当需要新增entry时,会调用addEntry(int, K, V, int)方法和createEntry(int, K, V, int),LinkedHashMap重写了该方法:

    void addEntry(int hash, K key, V value, int bucketIndex) {
        super.addEntry(hash, key, value, bucketIndex);

        // Remove eldest entry if instructed
        Entry<K,V> eldest = header.after;
        if (removeEldestEntry(eldest)) {
            removeEntryForKey(eldest.key);
        }
    }

    void createEntry(int hash, K key, V value, int bucketIndex) {
        HashMap.Entry<K,V> old = table[bucketIndex];
        Entry<K,V> e = new Entry<>(hash, key, value, old);
        table[bucketIndex] = e;
        e.addBefore(header);
        size++;
    }

LinkedHashMap在原先HashMap的基础上,按照新增的顺序将entry加入到双向链表中。

get方法

当调用get方法时,LinkedHashMap除了按照HashMap的逻辑获取value之外,会根据accessOrder的值,对内部的双向链表进行维护。

    public V get(Object key) {
        Entry<K,V> e = (Entry<K,V>)getEntry(key);
        if (e == null)
            return null;
        e.recordAccess(this);
        return e.value;
    }

    void recordAccess(HashMap<K,V> m) {
            LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
            if (lm.accessOrder) {
                lm.modCount++;
                remove();
                addBefore(lm.header);
            }
        }

当迭代顺序为访问顺序时,LinkedHashMap会将返回的entry置为双向链表的头结点。

remove方法

LinkedHashMap的remove方法跟HashMap差不多,只是需要额外维护内部的双向链表。

     void recordRemoval(HashMap<K,V> m) {
            remove();
        }

     private void remove() {
            before.after = after;
            after.before = before;
        }

总结

本文主要基于JDK1.7而阐述,JDK1.8对HashMap有诸多改动,后续会有相关文章列举。平时HashMap的时候,大多数都是知其然却不知其所以然,希望本文能帮助读者对HashMap有深入的了解。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值