论坛首页 Java企业应用论坛

边读边写【2】 ----java 集合包之深入Map

浏览 4807 次
精华帖 (0) :: 良好帖 (0) :: 新手帖 (4) :: 隐藏帖 (0)
作者 正文
   发表时间:2012-04-09   最后修改:2012-04-09
Collection 中还有一个Set 但是常用的Set 都是基于HashMap 实现的,因此先看完HashMap 更好理解一点。Listhttp://jiuyuehe.iteye.com/blog/1480165


Map 接口
map 都是基于 k-v 对的实现。
常用的有 HashMap 跟 TreeMap 。


HashMap  api 中有这么一段话来描述它:
基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

此实现假定哈希函数将元素适当地分布在各桶之间,可为基本操作(get 和 put)提供稳定的性能。迭代 collection 视图所需的时间与 HashMap 实例的“容量”(桶的数量)及其大小(键-值映射关系数)成比例。所以,如果迭代性能很重要,则不要将初始容量设置得太高(或将加载因子设置得太低)。


static final int DEFAULT_INITIAL_CAPACITY = 16;  // init capacity
 static final int MAXIMUM_CAPACITY = 1 << 30;    // max_size;
 static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默认加载因子
 transient Entry[] table;                        //装k-v 对
 transient int size;            
  int threshold;                                 //阀值
  final float loadFactor;                        //加载因子

                          //初始化
   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);

        // Find a power of 2 >= initialCapacity
        int capacity = 1;
        while (capacity < initialCapacity)
            capacity <<= 1;

        this.loadFactor = loadFactor;
        threshold = (int)(capacity * loadFactor); //阀值 用来rehash 的标志
        table = new Entry[capacity];
        init();
    }
     
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
        table = new Entry[DEFAULT_INITIAL_CAPACITY];
        init();
    }




put 操作。
put 一个值的时候,key 是允许为空的。如果有,更新value 如果没有创建一个,如果大小超过阀值了,rehash 一下 重新设定阀值。key  不是null 的时候,更具key 本身对象的hashCode,再做hash操作
 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);
    }

在与数组长度-1 进行&运算。得到index。这里就可能产生hash冲突
解决办法是找到此处的value ,如果有,则更新,如果没有创造一个。



  public V put(K key, V value) {
        if (key == null)  //允许为空
            return putForNullKey(value);
          // 给key hash 找到对应的位置i
        int hash = hash(key.hashCode());
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
         //解决hash 冲突
            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;
    }

  /**
     * Offloaded version of put for null keys
     * key 为null 时 put
      */
    private V putForNullKey(V value) {
          //默认从第一个元素开始找,找到将value 更新,返回oldValue
          //找不到 新增一个 Entry 
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
     //默认桶的index 是0;
        addEntry(0, null, value, 0);
        return null;
    }
// 新增一个Entry
    void addEntry(int hash, K key, V value, int bucketIndex) {
	Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
         //大小大于阀值的时候,rehash.
        if (size++ >= threshold)
            resize(2 * table.length);
    }



get 操作
他的流程跟put 操作基本一直。基于entry.next 进行遍历。

  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;
    }

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


remove 操作

一句话就是将当前对象的前一个对象的next 指向后一个对象
final Entry<K,V> removeEntryForKey(Object key) {
        int hash = (key == null) ? 0 : hash(key.hashCode());
        int i = indexFor(hash, table.length);
        Entry<K,V> prev = table[i];
        Entry<K,V> e = prev;

        while (e != null) {
            Entry<K,V> next = e.next;
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k)))) {
                modCount++;
                size--;
                if (prev == e)
                    table[i] = next;
                else
                    prev.next = next;
                e.recordRemoval(this);
                return e;
            }
            prev = e;
            e = next;
        }

        return e;
    }



HashMap 总结
HashMap 采用 k-v 对构成对象存储,无容量限制,基于key hash 寻找entry对象存放的数组位置,对于hash 冲突采用链表的方式来解决,插入元素时要扩容,并且重新计算hash.
HashMap 是非线性安全的。如果在并发场景中不保持同步的话很可能会死循环,cpu100%.最好的办法是用ConcurrentHashMap。
jdk 中这么说明:
注意,此实现不是同步的。如果多个线程同时访问一个哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须 保持外部同步。(结构上的修改是指添加或删除一个或多个映射关系的任何操作;仅改变与实例已经包含的键关联的值不是结构上的修改。)这一般通过对自然封装该映射的对象进行同步操作来完成。如果不存在这样的对象,则应该使用 Collections.synchronizedMap 方法来“包装”该映射。最好在创建时完成这一操作,以防止对映射进行意外的非同步访问,如下所示:

   Map m = Collections.synchronizedMap(new HashMap(...));由所有此类的“collection 视图方法”所返回的迭代器都是快速失败 的:在迭代器创建之后,如果从结构上对映射进行修改,除非通过迭代器本身的 remove 方法,其他任何时间任何方式的修改,迭代器都将抛出 ConcurrentModificationException。因此,面对并发的修改,迭代器很快就会完全失败,而不冒在将来不确定的时间发生任意不确定行为的风险。

-----------------------------------分割线-----------------------------------------
TreeMap


是一个支持排序的Map实现,实现方式和HashMap 不相同。
  private final Comparator<? super K> comparator;

  private transient Entry<K,V> root = null;

   public TreeMap() {
        comparator = null;
    }


put 操作  都写在注释里面了
  public V put(K key, V value) {
        Entry<K,V> t = root;
        if (t == null) {//首先判断root s是否为null 是创建一个新的Entry 给root
	    // TBD:
	    // 5045147: (coll) Adding null to an empty TreeSet should
	    // throw NullPointerException
	    //
	    // compare(key, key); // type check
            root = new Entry<K,V>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        int cmp;
        Entry<K,V> parent;
        // split comparator and comparable paths
        Comparator<? super K> cpr = comparator;
        if (cpr != null) { //如果root!=null 判断cpr 是否null 即:是否传入了指定的comparator.
            do { //是 则基于红黑树遍历 比较key 是放于树的左边还是右边。
                parent = t;
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);  //找到key 直接替换。
            } while (t != null);
        }
        else {
            if (key == null)  //cpr =null 判断 key =null? 
                throw new NullPointerException();
            Comparable<? super K> k = (Comparable<? super K>) key;
            do {  //同上
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
           //全部未找到 ,new Entry 将上面找到的 元素设为 parent 
        Entry<K,V> e = new Entry<K,V>(key, value, parent);
        if (cmp < 0)  //比较 
            parent.left = e;
        else
            parent.right = e;
        fixAfterInsertion(e);  //红黑树 填值
        size++;
        modCount++;
        return null;
    }


get 操作
用的就是getEntry()
这是一个典型的红黑树查找。从跟对象开始往下找
  final Entry<K,V> getEntry(Object key) {
        // Offload comparator-based version for sake of performance
        if (comparator != null)
            return getEntryUsingComparator(key);
        if (key == null)
            throw new NullPointerException();
	Comparable<? super K> k = (Comparable<? super K>) key;
        Entry<K,V> p = root;  //根对象
        while (p != null) {  //一直往下找,找到为止。找不到回null
            int cmp = k.compareTo(p.key); 
            if (cmp < 0)
                p = p.left;
            else if (cmp > 0)
                p = p.right;
            else
                return p;
        }
        return null;
    }


remove 操作
  remove 是一个操作复杂的过程,他首先getEntry 就是上面的查找过程。然后调整红黑树的结构。[你说麻烦不。因此删除节点的代价是比较高的]
 public V remove(Object key) {
        Entry<K,V> p = getEntry(key);
        if (p == null)
            return null;

        V oldValue = p.value;
        deleteEntry(p);  //很麻烦的一个过程
        return oldValue;
    }



TreeMap 总结: 基于红黑树k-v 实现,无限制容量,非线程安全。

HashMap 与 TreeMap 的选择。
这2个东西其实是没有可比性的。
转一篇博客中的一段话: 针对Map 的选用说的十分的透彻
Map 选择 

也许您曾期望更复杂的考量,而这实际上是否显得太容易? 好的,让我们慢慢来。 首先,您应使用哪种 Map? 答案很简单: 不要为您的设计选择任何特定的 Map,除非实际的设计需要指定一个特殊类型的 Map。 设计时通常不需要选择具体的 Map 实现。 您可能知道自己需要一个 Map,但不知道使用哪种。 而这恰恰就是使用 Map 接口的意义所在。 直到需要时再选择 Map 实现 — 如果随处使用“Map”声明的变量,则更改应用程序中任何特殊 Map 的 Map 实现只需要更改一行,这是一种开销很少的调整选择。 是否要使用默认的 Map 实现? 我很快将谈到这个问题。 

同步 Map 

同步与否有何差别? (对于同步,您既可以使用同步的 Map,也可以使用 Collections.synchronizedMap() 将未同步的 Map 转换为同步的 Map。 后者使用“同步的包装器”)这是一个异常复杂的选择,完全取决于您如何根据多线程并发访问和更新使用 Map,同时还需要进行维护方面的考虑。 例如,如果您开始时未并发更新特定 Map,但它后来更改为并发更新,情况将如何? 在这种情况下,很容易在开始时使用一个未同步的 Map,并在后来向应用程序中添加并发更新线程时忘记将此未同步的 Map 更改为同步的 Map。 这将使您的应用程序容易崩溃(一种要确定和跟踪的最糟糕的错误)。 但如果默认为同步,则将因随之而来的可怕性能而序列化执行多线程应用程序。 看起来,我们需要某种决策树来帮助我们正确选择。 

Doug Lea 是纽约州立大学奥斯威戈分校计算机科学系的教授。 他创建了一组公共领域的程序包(统称 util.concurrent),该程序包包含许多可以简化高性能并行编程的实用程序类。 这些类中包含两个 Map,即 ConcurrentReaderHashMap 和 ConcurrentHashMap。 这些 Map 实现是线程安全的,并且不需要对并发访问或更新进行同步,同时还适用于大多数需要 Map 的情况。 它们还远比同步的 Map(如 Hashtable)或使用同步的包装器更具伸缩性,并且与 HashMap 相比,它们对性能的破坏很小。 util.concurrent 程序包构成了 JSR166 的基础;JSR166 已经开发了一个包含在 Java 1.5 版中的并发实用程序,而 Java 1.5 版将把这些 Map 包含在一个新的 java.util.concurrent 程序包中。 

所有这一切意味着您不需要一个决策树来决定是使用同步的 Map 还是使用非同步的 Map, 而只需使用 ConcurrentHashMap。 当然,在某些情况下,使用 ConcurrentHashMap 并不合适。 但这些情况很少见,并且应具体情况具体处理。 这就是监测的用途。 

这段话出自一下博客连接:
http://www.cnblogs.com/aviva/archive/2008/03/26/1122953.html





   发表时间:2012-04-10  
我以为你是对Map边读边写呢。。。

就进来看看

理解错了
0 请登录后投票
   发表时间:2012-04-10  
是否可以这样理解:一般情况下直接用HashMap就可以了,如果遇到了并发的问题,直接改成ConcurrentHashMap就可以。具体看监控结果。
LZ如果能分享一下监控的方式方法和最佳实践就更好了。
0 请登录后投票
   发表时间:2012-04-10  
xiaoyuqi00 写道
我以为你是对Map边读边写呢。。。

就进来看看

理解错了

呵呵
0 请登录后投票
   发表时间:2012-04-10  
JackyCheng2007 写道
是否可以这样理解:一般情况下直接用HashMap就可以了,如果遇到了并发的问题,直接改成ConcurrentHashMap就可以。具体看监控结果。
LZ如果能分享一下监控的方式方法和最佳实践就更好了。

好的,我接下来会分享一些并发包里面的东西,但是都是潜尝则止。希望您多多指教哈。
0 请登录后投票
论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics