分享

【Java 7源码分析第10篇

 Mis林桑 2017-04-03

可能我们经常在实际应用或面试中注重的是各个集合的特点,比如,Set集合不能存储重复元素,不能保持插入和大小顺序,Map集合存储key-value对等等。但是,如果你想学好Java,或者目标更高一点,想要精通Java,那仅掌握这些是远远不够的。其实集合中还有需多的知道需要我们去掌握和了解。

我们首先来看Map集合的实现而不是Set集合,因为Map和Set集合实现上非常的相似,Set中许多方法都是通过调用Map中的方法来实现功能的。为什么呢?因为Map可以说就是一个Set,Set集合不重复且无序、Map中的key也有这个特性。当我们将Map中的value看为key的附属时,获取到的所有key就可以组成一个Set集合。

来看一下两者实现的类图:

     

                                                         Set 集合框架图 


                                                        Map集合框架图 

先来看一下Map接口,源代码如下:

public interface Map { // Query Operations int size(); boolean isEmpty(); boolean containsKey(Object key); boolean containsValue(Object value); V get(Object key); // Modification Operations V put(K key, V value); V remove(Object key); // Bulk Operations /* The behavior of this operation is undefined if the specified map is modified while the operation is in progress. */ void putAll(Map m); void clear(); // Views Set keySet();// 由于Map集合的key不能重复,key之间无顺序,所以Map集合中的所有key就可以组成一个Set集合 Collection values(); Set<>> entrySet(); interface Entry { K getKey(); V getValue(); V setValue(V value); boolean equals(Object o); int hashCode(); } // Comparison and hashing boolean equals(Object o); int hashCode();}

接口中有一个values方法,通过调用这个方法就可以返回Map集合中所有的value值;有一个keySet()方法,调用后可以得到所有Map中的 key值;调用entrySet()方法得到所有的Map中key-value对,以Set集合的形式存储。为了能够更好的表示这个key-value值,接口中还定义了一个Entry接口,并且在这个接口中定义了一些操作key和value的方法。

public class HashMap extends AbstractMap implements Map, Cloneable, Serializable{ // The default initial capacity - MUST be a power of two. static final int DEFAULT_INITIAL_CAPACITY = 16; static final int MAXIMUM_CAPACITY = 1 < 30;="" static="" final="" float="" default_load_factor="0.75f;//指定负载因子" the="" table,="" resized="" as="" necessary.="" length="" must="" always="" be="" a="" power="" of="" two.="" 使用entry数组来存储key-value,类似于arraylist用object[]来存储集合元素="" transient="" entry[]="" table;="" transient="" int="" size;="" the="" next="" size="" value="" at="" which="" to="" resize="" (capacity="" *="" load="" factor).="" hashmap所能容的mapping的极限="" int="" threshold;="" *="" 负载因子:="" */="" final="" float="" loadfactor;="" transient="" int="">
如上定义了一些重要的变量,其中的loadFactor是负载因子,增大值时可以减少Hash表(也就是Entry数组)所占用的内存空间,但会增加查询数据时时间的开销,而查询是最频繁的操作,减小值时会提高数据查询的性能,但是会增大Hash表所占用的内存空间,所以一般是默认的0.75。
threshold表示HashMap所能容纳的key-value对极限,如果存储的size数大于了threshold,则需要扩容了。
hashmap提供了几个构造函数,如下:
// 健HashMap的实际容量肯定大于等于initialCapacity,当这个值恰好为2的n次方时正好等于 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)="" 计算出大于initialcapacity的最小的2的n次方值="" capacity=""><= 1;="" this.loadfactor="loadFactor;" threshold="(int)(capacity" *="" loadfactor);//设置容量极限="" table="new" entry[capacity];="" init();="" }="" public="" hashmap(int="" initialcapacity)="" {="" this(initialcapacity,="" default_load_factor);="" }="" public="" hashmap()="" {="" this.loadfactor="DEFAULT_LOAD_FACTOR;" threshold="(int)(DEFAULT_INITIAL_CAPACITY" *="" default_load_factor);="" table="new" entry[default_initial_capacity];="" init();="" }="" public=""> m) { this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR); putAllForCreate(m); }
由第一个构造函数可以看出,其实际的capacity一般是大于我们指定的initialCapacity,除非initialCapacity正好是2的n次方值。接着就来说一下HashMap的实现原理吧。在这个类中开始处理定义了一个transient Entry[] 数组,这个Entry的实现如下:
private static class Entry implements Map.Entry { int hash; K key; V value; Entry next; protected Entry(int hash, K key, V value, Entry next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } protected Object clone() { return new Entry<>(hash, key, value,(next==null ? null : (Entry) next.clone())); } // Map.Entry Ops public K getKey() { return key; } public V getValue() { return value; } public V setValue(V value) { if (value == null) throw new NullPointerException(); V oldValue = this.value; this.value = value; return oldValue; } public boolean equals(Object o) { if (!(o instanceof Map.Entry)) return false; Map.Entry e = (Map.Entry)o; return (key==null ? e.getKey()==null : key.equals(e.getKey())) && (value==null ? e.getValue()==null : value.equals(e.getValue())); } public int hashCode() { return hash ^ (value==null ? 0 : value.hashCode()); } public String toString() { return key.toString()+'='+value.toString(); } }

了解了key-value存储的基本结构后,就可以考虑如何存储的问题了。HashMap顾名思义就是使用哈希表来存储的,哈希表为解决冲突,采用了开放地址法和链地址法来解决问题。Java中HashMap采用了链地址法。 链地址法,简单来说,就是数组加链表的结合。在每个数组元素上都一个链表结构,当数据被hash后,得到数组下标,把数据放在对应下标元素的链表上。当程序试图将多个 key-value 放入 HashMap 中时,以如下代码片段为例:
HashMap map = new HashMap(); map.put('语文' , 80.0); map.put('数学' , 89.0); map.put('英语' , 78.2);
HashMap 采用一种所谓的“Hash 算法”来决定每个元素的存储位置。
当程序执行 map.put('语文' , 80.0); 时,系统将调用'语文'的 hashCode() 方法得到其 hashCode 值——每个 Java 对象都有 hashCode() 方法,都可通过该方法获得它的 hashCode 值。得到这个对象的 hashCode 值之后,系统会根据该 hashCode 值来决定该元素的存储位置。

我们可以看 HashMap 类的 put(K key , V value) 方法的源代码:

// 当向HashMap中添加mapping时,由key的hashCode值决定Entry对象的存储位置,当两个 key的hashCode相同时,// 通过equals()方法比较,返回false产生Entry链,true时采用覆盖行为 public V put(K key, V value) { if (key == null) return putForNullKey(value); int hash = hash(key.hashCode()); int i = indexFor(hash, table.length); for (Entry 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; } 
当系统决定存储 HashMap 中的 key-value 对时,完全没有考虑 Entry 中的 value,仅仅只是根据 key 来计算并决定每个 Entry 的存储位置。这也说明了前面的结论:我们完全可以把 Map 集合中的 value 当成 key 的附属,当系统决定了 key 的存储位置之后,value 随之保存在那里即可。
   上面方法提供了一个根据 hashCode() 返回值来计算 Hash 码的方法:并且会调用 indexFor() 方法来计算该对象应该保存在 table 数组的哪个索引,源代码如下:
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); }    static int indexFor(int h, int length) {        return h & (length-1);    }

length是table.length,所以数组的长度总是 2 的 n 次方,通过h&(length-1)计算后,保证计算得到的索引值位于 table 数组的索引之内,计算的函数并不总是这样的,好的计算函数应该还会让索引值尽量平均分布到数组中。

        当调用put()方法向 HashMap 中添加 key-value 对,由其 key 的 hashCode() 返回值决定该 key-value 对(就是Entry 对象)的存储位置。当两个 Entry 对象的 key 的 hashCode() 返回值相同时,将由 key 通过 eqauls() 比较值决定是采用覆盖行为(返回 true),还是产生 Entry 链(返回 false),而且新添加的 Entry 位于 Entry 链的头部,这是通过调用addEntry()方法来完成的,源码如下:   

void addEntry(int hash, K key, V value, int bucketIndex){ Entry e = table[bucketIndex]; // 获取指定 bucketIndex 索引处的 Entry table[bucketIndex] = new Entry(hash, key, value, e); // 将新创建的 Entry 放入 bucketIndex 索引处,并让新的 Entry 指向原来的 Entry // 如果 Map 中的 key-value 对的数量超过了极限 if (size++ >= threshold) resize(2 * table.length); // 把 table 对象的长度扩充到 2 倍}

程序总是将新添加的 Entry 对象放入 table数组的 bucketIndex 索引处——如果 bucketIndex 索引处已经有了一个 Entry 对象,那新添加的 Entry 对象指向原有的 Entry 对象(产生一个 Entry 链),如果 bucketIndex 索引处没有 Entry 对象, e 变量是 null,也就是新放入的 Entry 对象指向 null,也就是没有产生 Entry 链。

接下来看一下HashMap是如何读取的。 get()方法的源代码如下:

public V get(Object key) { if (key == null) return getForNullKey(); int hash = hash(key.hashCode()); // 搜索该Entry链的下一个Entry,有多个Entry链时必须顺序遍历,降低了索引的速度 // 如果Entry链过长,说明发生“Hash”冲突比较频繁,需要采用新的算法或增大空间 for (Entry 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 的每个 bucket 里存储的 Entry 只是单个 Entry 时的 HashMap 具有最好的性能:当程序通过 key 取出对应 value 时,只要先计算出该 key 的 hashCode() 返回值,在根据该 hashCode 返回值找出该 key 在 table 数组中的索引,然后循环遍历查找 hash值相同,key值相同的value。

下面来看一下HashMap是如何实现如下的三个方法的,源代码如下:

public Set keySet() { Set ks = keySet; return (ks != null ? ks : (keySet = new KeySet())); } public Collection values() { Collection vs = values; return (vs != null ? vs : (values = new Values())); } public Set<>> entrySet() { return entrySet0(); } private Set<>> entrySet0() { Set<>> es = entrySet; return es != null ? es : (entrySet = new EntrySet()); }
分别得到了KeySet、Values和EntrySet私有类的实例,那么他们是怎么从HashMap中取出这些值的呢?其实这里会涉及到非常多的类和方法,大概框架如下所示:



如上类中最重要的就是HashEntry类的实现,如下:

private abstract class HashIterator implements Iterator { Entry next; // next entry to return int expectedModCount; // For fast-fail int index; // current slot Entry current; // current entry HashIterator() { expectedModCount = modCount; if (size > 0) { // advance to first entry Entry[] t = table; while (index < t.length="" &&="" (next="t[index++])" =="null);//" 将index指向第一个table不为null的位置="" }="" }="" public="" final="" boolean="" hasnext()="" {="" return="" next="" !="null;" }="" final=""> nextEntry() {// 遍历Entry链 if (modCount != expectedModCount) throw new ConcurrentModificationException(); Entry e = next; if (e == null) throw new NoSuchElementException(); if ((next = e.next) == null) { Entry[] t = table; while (index < t.length="" &&="" (next="t[index++])" =="null);" }="" current="e;" return="" e;="" }="" public="" void="" remove()="" {="" if="" (current="=" null)="" throw="" new="" illegalstateexception();="" if="" (modcount="" !="expectedModCount)" throw="" new="" concurrentmodificationexception();="" object="" k="current.key;" current="null;" hashmap.this.removeentryforkey(k);="" expectedmodcount="modCount;" }="">

和ArrayList一样,在获取到HashMap的Iterator对象后,就不可以使用ArrayList进行添加或删除的操作了,否则会出现异常。看一下几个重要的变量,如图示。





























本文转载自mazhimazh博客,版权归mazhimazh所有

猜你感兴趣:

  • 本文相关:
  • 深度优先搜索的用法——求数组部分和
  • Linux基本概念(二)
  • Android源码分析—属性动画的工作原理
  • 策略模式--GOF的23个之一
  • 【10】coco2d-x CCTextFieldTTF最简单的方法实现密码登陆“*”
  • Android ART机制分析
  • Clojure 学习入门(13)—— binding
  • shell内部命令使用详解
  • OpenCV之序统计滤波
  • c++学习笔记(12.继承与多态)
    • 本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
      转藏 分享 献花(0

      0条评论

      发表

      请遵守用户 评论公约

      类似文章 更多