Set和Map底层解析

先看下set的类图:

Set集合和Map集合的对应关系如下。

-

Set集合和Map集合的对应关系如下。

■ Set <-> Map

■ EnumSet <-> EnumMap

■ SortedSet <-> SortedMap

■ TreeSet <-> TreeMap

扫描二维码关注公众号,回复: 11378512 查看本文章

■ NavigableSet <-> NavigableMap

■ HashSet <-> HashMap

■ LinkedHashSet <-> LinkedHashMap

Map集合的key具有一个特征:所有key不能重复,key之间没有顺序。也就是说,如果将Map集合的所有key集中起来,那这些key就组成了一个Set集合。所以,发现Map集合提供了如下方法来返回所有key组成的Set集合。

class SimpleEntry<K , V>
          implements Map.Entry<K , V>, java.io.Serializable
      {
          private final K key;
          private V value;
          //定义如下2个构造器
          public SimpleEntry(K key, V value)
          {
              this.key =key;
              this.value=value;
          }
          public SimpleEntry(Map.Entry<? extends K
              , ? extends V> entry)
          {
              this.key =entry.getKey();
              this.value=entry.getValue();
          }
          //获取key
          public K getKey()
          {
              return key;
          }
          //获取value
          public V getValue()
          {
              return value;
          }
          //改变该key-value对的value值
          public V setValue(V value)
          {
              V oldValue=this.value;
              this.value=value;
              return oldValue;
          }
          //根据key比较2个SimpleEntry是否相等
          public boolean equals(Object o)
          {
              if (o == this)
              {
                  return true;
              }
              if (o.getClass() == SimpleEntry.class)
              {
                  SimpleEntry se=(SimpleEntry)o;
                  return se.getKey().equals(getKey());
              }
              return false;
          }
          //根据key计算hashCode
          public int hashCode()
          {
              return key  == null ? 0 :  key.hashCode();
          }
          public String toString()
          {
              return key+"="+value;
          }
      }
      //继承HashSet实现一个Map
      public class Set2Map<K , V>
            extends HashSet<SimpleEntry<K , V>>
      {
            //实现清空所有key-value对的方法
            public void clear()
            {
              super.clear();
            }
            //判断是否包含某个key
            public boolean containsKey(Object key)
            {
                return super.contains(
                    new SimpleEntry<K , V>(key ,null));
            }
            //判断是否包含某个value
            boolean containsValue(Object value)
            {
                for (SimpleEntry<K , V> se : this)
                {
                    if (se.getValue().equals(value))
                    {
                            return true;
                    }
                }
                return false;
          }
          //根据指定key取出对应的value
          public V get(Object key)
          {
              for (SimpleEntry<K , V> se : this)
              {
          if (se.getKey().equals(key))
          {
                      return se.getValue();
          }
              }
              return null;
          }
          //将指定key-value对放入集合中
          public V put(K key, V value)
          {
              add(new SimpleEntry<K , V>(key ,value));
              return value;
          }
          //将另一个Map的key-value对放入该Map中
          public void putAll(Map<? extends K,? extends V> m)
          {
              for (K key : m.keySet())
              {
            add(new SimpleEntry<K , V>(key , m.get(key)));
              }
          }
          //根据指定key删除指定key-value对
          public V removeEntry(Object key)
          {
              for (Iterator<SimpleEntry<K , V>> it=this.iterator()
                    ; it.hasNext() ; )
              {
          SimpleEntry<K , V> en=(SimpleEntry<K , V>)it.next();
          if (en.getKey().equals(key))
          {
                        V v=en.getValue();
                        it.remove();
                        return v;
          }
            }
            return null;
          }
          //获取该Map中包含多少个key-value对
          public int size()
          {
            return super.size();
          }
      }

HashSet和HashMap之间有很多相似之处。对于HashSet而言,系统采用Hash算法决定集合元素的存储位置,这样可以保证快速存、取集合元素;对于HashMap而言,系统将value当成key的附属,系统根据Hash算法来决定key的存储位置,这样可以保证快速存、取集合key,而value总是紧随key存储。

HashMap类的put(K key , V value)方法的源代码如下:

      public V put(K key, V value)
      {
            //如果key为null,调用putForNullKey方法进行处理
            if (key == null)
                return putForNullKey(value);
            //根据key的keyCode计算Hash值
            int hash=hash(key.hashCode());
            //搜索指定hash值在对应table中的索引
            int i=indexFor(hash, table.length);
            //如果i索引处的Entry不为null,通过循环不断遍历e元素的下一个元素
            for (Entry<K,V> e=tablei]; e != null; e=e.next)
            {
                Object k;
                //找到指定key与需要放入的key相等(hash值相同,通过equals比较放回true)
                if (e.hash == hash && ((k=e.key) == key || key.equals(k)))
                {
                    V oldValue=e.value;
                    e.value=value;
                    e.recordAccess(this);
                    return oldValue;
            }
          }
          //如果i索引处的Entry为null,表明此处还没有Entry
          modCount++;
          //将key、value添加到i索引处
          addEntry(hash, key, value, i);
          return null;
      }

从上面put方法的源代码可以看出,当程序试图将一个key-value对放入HashMap中时,先根据该key的hashCode()返回值决定该Entry的存储位置:如果两个Entry的key的hashCode()返回值相同,那它们的存储位置相同;如果这两个Entry的key通过equals比较返回true,新添加Entry的value将覆盖集合中原有Entry的value,但key不会覆盖;如果这两个Entry的key通过equals比较返回false,新添加的Entry将与集合中原有Entry形成Entry链,而且新添加的Entry位于Entry链的头部—具体说明请看addEntry()方法的说明。

当向HashMap中添加key-value对,由其key的hashCode()返回值决定该key-value对(就是Entry对象)的存储位置。当两个Entry对象的key的hashCode()返回值相同时,将由key通过equals()比较值决定是采用覆盖行为(返回true),还是产生Entry链(返回false)。

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

系统总是将新添加的Entry对象放入table数组的bucketIndex索引处。如果bucketIndex索引处已经有了一个Entry对象,新添加的Entry对象指向原有的Entry对象(产生一个Entry链);如果bucketIndex索引处没有Entry对象,也就是上面程序①行代码的e变量是null,即新放入的Entry对象指向null,就没有产生Entry链。

提示:根据上面代码可以看出,在同一个bucket存储Entry链的情况下,新放入的Entry总是位于bucket中,而最早放入该bucket中的Entry则位于这个Entry链的最末端。

上面程序中还有以下两个变量。

■ size:该变量保存了该HashMap中所包含的key-value对的数量。

■ threshold:该变量包含了HashMap能容纳的key-value对的极限,它的值等于HashMap的容量乘以负载因子(load factor)。

从上面程序中②行代码可以看出,当size++>= threshold时,HashMap会自动调用resize方法扩充HashMap的容量。每扩充一次,HashMap的容量就增大一倍。

看一下什么是负载因子:

上面程序中使用的table其实就是一个普通数组,每个数组都有一个固定的长度,这个数组的长度就是HashMap的容量。HashMap包含如下几个构造器。

■ HashMap():构建一个初始容量为16,负载因子为0.75的HashMap。

■ HashMap(int initialCapacity):构建一个初始容量为initialCapacity,负载因子为0.75的HashMap。

■ HashMap(int initialCapacity, float loadFactor):以指定初始容量、指定的负载因子创建一个HashMap。

//以指定初始化容量、负载因子创建HashMap
      public HashMap(int initialCapacity, float loadFactor)
      {
            //初始容量不能为负数
            if (initialCapacity < 0)
                throw new IllegalArgumentException("Illegal initial capacity: " +
                        initialCapacity);
            //如果初始容量大于最大容量,让初始容量等于最大容量
            if (initialCapacity > MAXIMUM_CAPACITY)
                initialCapacity=MAXIMUM_CAPACITY;
            //负载因子必须是大于0的数值
            if (loadFactor <= 0 || Float.isNaN(loadFactor))
                throw new IllegalArgumentException("Illegal load factor: " +
                    loadFactor);
            //计算出大于initialCapacity的最小的2的n次方值
            int capacity=1;
            while (capacity < initialCapacity)
                capacity <<= 1;
            this.loadFactor=loadFactor;
            //设置容量极限等于容量乘以负载因子
            threshold=(int)(capacity * loadFactor);
            //初始化table数组
            table=new Entrycapacity];                             //①
            init();
      }
上面代码中粗体字代码包含了一个简洁的代码实现:找出大于initialCapacity的、最小的2的n 次方值,并将其作为HashMap的实际容量(由 capacity变量保存)。例如,给定initialCapacity为10,那么该HashMap的实际容量就是16。
程序①行代码处可以看出,table的实质就是一个数组,一个长度为capacity的数组。

从上面代码可以看出,创建HashMap时指定的initialCapacity并不等于HashMap的实际容量。通常来说,HashMap的实际容量总比 initialCapacity 大一些,除非指定的initialCapacity参数值恰好是2的n次方。当然,掌握了HashMap容量分配的知识之后,应该在创建HashMap时将initialCapacity参数值指定为2的n次方,这样可以减少系统的计算开销。

对于HashMap及其子类而言,它们采用Hash算法来决定集合中元素的存储位置。当系统开始初始化HashMap时,系统会创建一个长度为capacity的Entry数组。这个数组里可以存储元素的位置被称为“桶(bucket)”,每个bucket 都有其指定索引,系统可以根据其索引快速访问该bucket里存储的元素。

public V get(Object key)
      {
            //如果key是null,调用getForNullKey取出对应的value
            if (key == null)
                    return getForNullKey();
            //根据该key的hashCode值计算它的hash码
            int hash=hash(key.hashCode());
            //直接取出table数组中指定索引处的值
            for (Entry<K,V> e=tableindexFor(hash, table.length)];
                    e != null;
                    //搜索该Entry链的下一个Entry
                    e=e.next)                                  //①
            {
                    Object k;
                    //如果该Entry的key与被搜索key相同
                    if (e.hash == hash && ((k=e.key) == key || key.equals(k)))
                        return e.value;
            }
            return null;
      }

如果HashMap的每个bucket里只有一个Entry,HashMap可以根据索引快速地取出该bucket里的Entry。在发生“Hash冲突”的情况下,单个bucket里存储的不是一个Entry,而是一个Entry链,系统只能按顺序遍历每个Entry,直到找到想搜索的Entry为止。如果恰好要搜索的Entry位于该Entry链的最末端(该Entry最早放入该bucket中),那系统必须循环到最后才能找到该元素。

归纳起来简单地说:

  • HashMap在底层将key-value对当成一个整体进行处理,这个整体就是一个Entry对象。
  • HashMap底层采用一个Entry[]数组来保存所有的key-value对,当需要存储一个Entry对象时,会根据Hash算法来决定其存储位置;当需要取出一个Entry时,也会根据Hash算法找到其存储位置,直接取出该Entry。由此可见,HashMap之所以能快速存、取它所包含的Entry,完全类似于现实生活中的:不同的东西要放在不同的位置,需要时才能快速找到它。
  • 当创建HashMap时,有一个默认的负载因子(load factor),其默认值为0.75。这是时间和空间成本上的一种折衷:增大负载因子可以减少Hash表(就是那个Entry数组)所占用的内存空间,但会增加查询数据的时间开销,而查询是最频繁的的操作(HashMap的get()与put()方法都要用到查询);减小负载因子会提高数据查询的性能,但会增加Hash表所占用的内存空间
  • HashMap时根据实际需要适当地调整load factor的值。如果程序比较关心空间开销,内存比较紧张,可以适当地增加负载因子;如果程序比较关心时间开销,内存比较宽裕,则可以适当减少负载因子。通常情况下,程序员无需改变负载因子的值。
  • 如果开始就知道HashMap会保存多个key-value对,可以在创建时就使用较大的初始化容量,如果HashMap中Entry的数量一直不会超过极限容量(capacity * load factor),HashMap就无需调用resize()方法重新分配table数组,从而保证较好的性能。当然,开始就将初始容量设置太高可能会浪费空间(系统需要创建一个长度为capacity的Entry数组),因此创建HashMap时初始化容量设置也需要小心对待。

HashSet:

HashSet的绝大部分方法都是通过调用HashMap的方法来实现的,因此 HashSet和HashMap两个集合在实现本质上是相同的。

注意

由于HashSet的add()方法添加集合元素时实际上转变为调用HashMap的put()方法来添加 key-value对,当新放入 HashMap的Entry 中key 与集合中原有Entry的key 相同(hashCode()返回值相等,通过equals比较也返回true)时,新添加的Entry的value将覆盖原来Entry的value,但key不会有任何改变。因此,如果向HashSet中添加一个已经存在的元素,新添加的集合元素(底层由HashMap的key保存)不会覆盖已有的集合元素。

hashset出始化源码
public class HashSet<E>
            extends AbstractSet<E>
            implements Set<E>, Cloneable, java.io.Serializable
      {
            //使用HashMap的key保存HashSet中的所有元素
            private transient HashMap<E,Object> map;
            //定义一个虚拟的Object对象作为HashMap的value
            private static final Object PRESENT=new Object();
            ...
            //初始化HashSet,底层会初始化一个HashMap
            public HashSet()
            {
                map=new HashMap<E,Object>();
            }
            //以指定的initialCapacity、loadFactor创建HashSet
            //其实就是以相应的参数创建HashMap
            public HashSet(int initialCapacity, float loadFactor)
            {
                map=new HashMap<E,Object>(initialCapacity, loadFactor);
            }
            public HashSet(int initialCapacity)
            {
                map=new HashMap<E,Object>(initialCapacity);
            }
            HashSet(int initialCapacity, float loadFactor, boolean dummy)
            {
              map=new LinkedHashMap<E,Object>(initialCapacity, loadFactor);
            }
   //调用map的keySet来返回所有的key
            public Iterator<E> iterator()
            {
                return map.keySet().iterator();
            }
            //调用HashMap的size()方法返回Entry的数量,得到该Set里元素的个数
            public int size()
            {
                return map.size();
            }
            //调用HashMap的isEmpty()判断该HashSet是否为空
            //当HashMap为空时,对应的HashSet也为空
            public boolean isEmpty()
            {
                return map.isEmpty();
            }
            //调用HashMap的containsKey判断是否包含指定key
            //HashSet的所有元素就是通过HashMap的key来保存的
            public boolean contains(Object o)
            {
                return map.containsKey(o);
            }
            //将指定元素放入HashSet中,也就是将该元素作为key放入HashMap
            public boolean add(E e)
            {
                return map.put(e, PRESENT) == null;
            }
            //调用HashMap的remove方法删除指定Entry,也就删除了HashSet中对应的元素
            public boolean remove(Object o)
            {
                return map.remove(o)==PRESENT;
            }
            //调用Map的clear方法清空所有Entry,也就清空了HashSet中所有元素
            public void clear()
            {
                map.clear();
            }
            ...
      }

HashSet判断两个对象相等的标准除了要求通过equals()方法比较返回true之外,还要求两个对象的hashCode()返回值相等。而上面程序没有重写Name类的hashCode()方法,两个Name对象的hashCode()返回值并不相同,因此HashSet会把它们当成2个对象处理,程序返回false。

由此可见,当试图把某个类的对象当成 HashMap的key,或者试图将这个类的对象放入HashSet中保存时,重写该类的equals(Object obj)方法和hashCode()方法很重要,而且这两个方法的返回值必须保持一致。当该类的两个hashCode()返回值相同时,它们通过equals()方法比较也应该返回true。通常来说,所有参与计算 hashCode()返回值的关键属性,都应该用于作为equals()比较的标准。

猜你喜欢

转载自blog.csdn.net/qq_37256896/article/details/103325204