Map

Map

         将键映射到值的对象。对于一个映射来说键不能重复,一个键不能对应到多个值上。

         一般情况下通过equal来判别键是否重复,但是也存在特殊的Map使用“==”来判别是否重复,如IdentityHashMap。

         Map有3中collection视图。①键集②值集③键-值映射关系集。由于实现机制的不同,有些实现类可以保证视图的迭代顺序,如TreeMap,有些无法保证,如HashMap。

         这里有一点需要注意一下。值从类型上可以分为2种,一种是基本类型的封装类,这个就不说了;另外一种是普通的值对象,由于对于值对象使用的是它的引用关系,所以这里的值对象是可以修改它的状态,使用时尤其是涉及到多个值对象修改时。

         对于用可变对象作为Map的键的情况,尽量不要对可变对象做可以修改equals返回值的操作,否则的话会发生很不好的事情。

Map.Entry

         键值映射关系集(Entry)是Map中很重要的一个组成部分,属于Map的封闭接口。它的功能类似于列表中的迭代器,get、containKey、containValues、remove等操作都和它息息相关,Map的键集和值集也是通过它来生成。

AbstractMap

         AbstractMap是Map的骨干实现,提供Map实现的功能,如size、get、containKey、containValues、remove等操作,以及键集(keySet)、值集(values)的构建。这些功能主要由Map.Enry来实现。

public Set<K> keySet() {
        if (keySet == null) {
            keySet = new AbstractSet<K>() {
                public Iterator<K> iterator() {
                    return new Iterator<K>() {
                        privateIterator<Entry<K,V>> i = entrySet().iterator();
 
                        public boolean hasNext() {
                            return i.hasNext();
                        }
 
                        public K next() {
                            return i.next().getKey();
                        }
 
                        public void remove() {
                            i.remove();
                        }
                    };
                }
           ……
            };
        }
        return keySet;
}


值集的实现与上述代码类似。

如果我们要实现不可修改的Map时,只需要扩展此类并提供 entrySet 方法的实现即可,该方法将返回映射的映射关系 Set 视图。

public abstract Set<Entry<K,V>>entrySet();

当然如果要实现可修改的Map,我们还需要重写put方法,并根据实际情况重写remove方法。如果还有其他的需求,我们可以重写别的相关方法。

HashMap

public class HashMap<K,V>extends AbstractMap<K,V>implements Map<K,V>, Cloneable,Serializable{
……
}


    HashMap是我们最常用到的Map了,基于哈希表实现,提供所有的映射操作,支持null键和null值,不保证映射顺序。与HashTable很类似,区别在于不支持同步操作和对null的处理上。

    HashMap的数据结构是以Entry数组为主,利用哈希算法来将键对象的哈希值转换为数组的index值。

    static int hash(int h) {
        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h>>> 12);
        return h ^ (h >>> 7) ^ (h>>> 4);
}


Hash值的计算方法,HashTable采用的计算方式与这个略有不同。

如果不同的值对象对应到了同一个index上该怎么办呢?这里Entry实现时,采用了链式的结构,每一个Entry可通过next找到后续的Entry。

    Entry1-->Entry2-->EntryN

      比较有意思的一点就是,新增的值会放在Entry链的头上,而不是尾部。不太清除为什么这么设计,见Entry的构造器

        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
       }


    保存时,是将键值映射关系作为整体保存的。读取的时候,首先通过键的hashCode计算哈希值,并通过数组大小一起计算得到对应的index值,对得到的Entry链逐个判别,每个entry.hash是否等于计算出的哈希值,entry.key是否等于查询的key。

    public V get(Object key) {
        if (key == null)
            return getForNullKey();
        int hash = hash(key.hashCode());
        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.equals(k)))
                return e.value;
        }
        return null;
}


其余的操作类似,可以通过源码查看。

在HashMap中有2个参数影响其性能:初始容量和加载因子。容量是哈希表中桶的数量,初始容量只是哈希表在创建时的容量。加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,通过调用 rehash 方法将容量翻倍。

默认加载因子 (.75) 在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本,因为存的太满的话,Entry链上的数据就会明显增多。这个就需要根据实际情况来看了,对于大数据量来说,设置一个较大的初始容量是很重要的。

LinkedHashMap

public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>{
……
}


    Map接口的哈希表和连接表实现,具有可预知的迭代顺序。与HashMap的不同点在于维护了一个双向链表,在增加删除元素时,在维护其存储结构的同时也在维护一个双向链表,该链表记录了加载顺序。

private static class Entry<K,V>extends HashMap.Entry<K,V> {
        Entry<K,V> before, after;
 
      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;
        }
……
}


可见链表也是使用Entry来实现的,但是在HashMap的Entry的基础上增加了前后Entry信息。其实我们发现Entry连包含了所有的元素信息,迭代器也是通过它来实现的。

注意,这里的after和before主要是给双向链表使用的,而保存时数据结构还是依赖于单向链表的,和HashMap一样。

LinkedHashMap还有另外一种迭代顺序--最近最少使用(LRU),注意变量 accessOrder 默认为false,意味着迭代按照加载顺序来。我们可以将它设置为true,将按照访问情况迭代,从近期访问最少到近期访问最多的顺序,可以用来创建LRU缓存。

IndentityHashMap

public class IdentityHashMap<K,V> extendsAbstractMap<K,V> implements Map<K,V>, java.io.Serializable, Cloneable{
    ……
}


         IndentityHashMap也是采用键的hash值来进行散列的Map,也可以支持null,但是与HashMap不同的是它在判别键是否重复时比较键(和值)时使用引用相等性代替对象相等性,及不是通过equals的而是通过“==”来进行的。

         它不是通用的Map实现,一般情况下都是使用equals来对对象进行比较,它是为特殊情况使用的。

         它为简单的线性探头哈希表,数组交替保持键和值。从源代码中可以看到,首先通过键的哈希值找到数组index,如果满足条件就将值保存在index+1上,不行就后移2位。

    public V put(K key, V value) {
        Object k = maskNull(key);
        Object[] tab = table;
        int len = tab.length;
        int i = hash(k, len);
 
        Object item;
        while ( (item = tab[i]) != null) {
            if (item == k) {
                V oldValue = (V) tab[i + 1];
                tab[i + 1] = value;
                return oldValue;
            }
            i = nextKeyIndex(i, len);
        }
 
        modCount++;
        tab[i] = k;
        tab[i + 1] = value;
        if (++size >= threshold)
            resize(len);
        return null;
}


我们知道HashMap使用了桶和链来共同实现,这里只使用了一个Object数组。可见在空间上IndentityHashMap有更大的需求,由于所有操作都是在数组上进行的,查询效率应该也会高于HashMap。

但是删除一个元素是很麻烦的,删除后需要判别后续的元素是否需要前移,这个花费也是不小的。

由于初始的数组最大值,如果超过最大值则会对数组扩容,这个代价是很高的。但过大的会影响到迭代效率,需要平衡处理。

WeakHashMap

public class WeakHashMap<K,V> extendsAbstractMap<K,V> implements Map<K,V> {
    ……
}


    从结构上看同hashMap很相像,但是会维护一个弱引用队列

private final ReferenceQueue<Object> queue = new ReferenceQueue<>();


    在维护每一个Entry时,都会将key加载入弱引用队列中。Entry只是保留一个对键值的弱引用。但可以值可不是弱引用,而是一个强引用。但WeakHashMap不会主动释放失效的弱引用,而是

在每次对Map进行操作时,都会调用

    private void expungeStaleEntries() {
        for (Object x; (x = queue.poll()) != null; ) {
            synchronized (queue) {
                @SuppressWarnings("unchecked")
                    Entry<K,V> e =(Entry<K,V>) x;
                int i = indexFor(e.hash, table.length);
 
                Entry<K,V> prev = table[i];
                Entry<K,V> p = prev;
                while (p != null) {
                    Entry<K,V> next = p.next;
                    if (p == e) {
                        if (prev == e)
                            table[i] = next;
                        else
                            prev.next = next;
                        // Must not null out e.next;
                        // stale entries may be in use by a HashIterator
                        e.value = null; // Help GC
                        size--;
                        break;
                    }
                    prev = p;
                    p = next;
                }
            }
        }
}


将弱引用队列中对应的数据清除。

在 WeakHashMap中,当某个键不再正常使用时,将自动移除其条目。更精确地说,对于一个给定的键,其映射的存在并不阻止垃圾回收器对该键的丢弃,这就使该键成为可终止的,被终止,然后被回收。丢弃某个键时,其条目从映射中有效地移除,因此,该类的行为与其他的 Map 实现有所不同。

WeakHashMap中的值对象由普通的强引用保持。因此应该小心谨慎,确保值对象不会直接或间接地强引用其自身的键,因为这会阻止键的丢弃。注意,值对象可以通过WeakHashMap本身间接引用其对应的键;这就是说,某个值对象可能强引用某个其他的键对象,而与该键对象相关联的值对象转而强引用第一个值对象的键。处理此问题的一种方法是,在插入前将值自身包装在 WeakReferences中,如:m.put(key, newWeakReference(value)),然后,分别用get进行解包。

EnumMap

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


    EnumMap是一个比较特殊的Map,它只允许Enum元素作为它的键,否则的话就会抛出ClassCastException异常。不允许使用null键,但是允许使用null值。

    所有基本操作都在固定时间内执行。虽然并不保证,但它们很可能比其 HashMap 副本更快。

    其数据结构,由于在初始化时已通过

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


获取到了键集,所以我们在保存时主要处理值数组,值数组的大小等于已知键集的大小。且值对应的位置有枚举(ordinal)键的序号确定。Entry尽需要保存index信息。

SortedMap

         保证按照键的升序或是自然排序的映射,我们可以也可以自定其他的排序方式,这个有接口提供的comparator方法提供支持,我们可以按照实际需要来实现不同的比较方法。对有序映射的集合视图(由 entrySetkeySet 和 values 方法返回)进行迭代时,此顺序就会反映出来。

    由于使用了比较器,所以就要求所有的键都必须实现Comparable接口,而且键之间应该是可以比较的。

       注意,如果有序映射正确实现了 Map 接口,则有序映射所保持的顺序(无论是否明确提供了比较器)都必须保持相等一致性。这也是因为 Map 接口是按照 equals 操作定义的,但有序映射使用它的compareTo(或 compare)方法对所有键进行比较,因此从有序映射的观点来看,此方法认为相等的两个键就是相等的。即使顺序没有保持相等一致性,树映射的行为仍然是 定义良好的,只不过没有遵守 Map 接口的常规协定。

NavigableMap

         该接口是在java1.6版本后增加的。

         其中对键集(Key)的操作有:

  • ceilingKey(key):用来获取大于或者等于给定的key的第一个键,如果没有的话就返回null。
  • floorKey(key):用来获取小于或者等于给定的key的第一个键,如果没有的话就返回null。
  • higherKey(key):用来获取大于给定的key的第一个键,如果没有的话就返回null。
  • lowerKey(key):用来获取小于给定的key的第一个键,如果没有的话就返回null。

对键值映射关系集(Entry)的操作有:

  • ceilingEntry(key):用于获取大于或等于给定key的第一个实体,如果没有则返回null。
  • firstEntry():用于获取Map的第一个实体,如果没有则返回null。
  • floorEntry(key):用于获取小于或等于给定的第一个实体key,如果没有则返回null。
  • higherEntry():用于获取大于给定的key的第一个实体,如果没有则返回null。
  • lastEntry():用于获取Map最后一个实体,如果没有则返回null。
  • lowerEntry(key):用于获取小于给定key的第一个实体,如果没有则返回null。

这里有两个单步从Map获取和删除实体的方法。提供了一个简单的不用使用迭代器而遍历所有Map元素的方法。下面是具体的介绍:

  • Map.Entry<K,V> pollFirstEntry():获取Map第一个键的实体并且从Map中移除该实体,如果Map为空则返回null。
  • Map.Entry<K,V> pollLastEntry():获取Map最后一个键的实体并且从Map移除该实体,如果Map为空则返回null。
TreeMap

public class TreeMap<K,V> extendsAbstractMap<K,V> implements NavigableMap<K,V>, Cloneable, java.io.Serializable{
    ……
}


    基于红黑树的实现。此类保证了映射按照升序顺序排列关键字,根据使用的构造方法不同,可能会按照键的类的自然顺序进行排序,或者按照创建时所提供的比较器进行排序。

       红黑树是一种自平衡的二叉树。每个节点是一个五元组:color(颜色),key(数据),left(左孩子),right(右孩子)和p(父节点)。其中可以保证left<self<right。具有以下特性:

性质1. 节点是红色或黑色

性质2. 根是黑色

性质3. 所有叶子都是黑色(叶子是NIL节点)

性质4. 如果一个节点是红的,则它的两个儿子都是黑的

性质5. 从任一节点到其叶子的所有简单路径都包含相同数目的黑色节点。

由于红黑树比较复杂,这里就避重就轻不讨论它的结构和操作,这样TreeMap中很多结构操作就先当做是透明的,以后详细研究红黑树时可以再将这里缺失的部分不全,这里主要精力还是放在TreeMap上,其实也没剩下什么。

         TreeMap中红黑树的节点的数据包含两部分,键和值。这里使用Entry实现。创建一个初始的root,按照红黑树增加、删除节点的方式逐步操作,构建最终的数据结构。

         由于TreeMap在插入或删除数据时需要对存储结构进行旋转操作,造成了效率上会低于HashMap。但是HashMap无法胜任需要对键集排序的情况,这时我们只能依靠TreeMap。但是其他的情况还是建议使用HashMap,呵呵

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值