Android中需要了解的数据结构(二)

1,289 阅读7分钟

前言

前面了解完List接口的相关实现类 Android中需要了解的数据结构(一)

Map接口

Map与List、Set接口不同,它是由一系列键值对组成的集合,提供了key到Value的映射。在Map中它保证了key与value之间的一一对应关系。也就是说一个key对应一个value,所以它不能存在相同的key值,当然value值可以相同。
实现map的集合有:HashMap、HashTable、TreeMap、WeakHashMap。

HashMap

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

HashMap继承了Map,实现了map的所有方法。key和value允许使用全部的元素,包括null, 注意遍历hashMap是随机的,如果你想定义遍历顺序,请使用LinkedHashMap。
在Java言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。

Java8 对HashMap 底层做了优化 本文以Java8为例

  /**
    * An empty table instance to share when the table is not inflated.
    * Orcle的JDK中名字叫Node<K,V>
    */
    static final HashMapEntry<?,?>[] EMPTY_TABLE = {};

    /**
    The table, resized as necessary. Length MUST Always be a power of two.
    Orcle的JDK中名字叫Node<K,V>
    */
    transient HashMapEntry<K,V>[] table = (HashMapEntry<K,V>[]) EMPTY_TABLE;


    //Orcle的JDK
    /**
       * The table, initialized on first use, and resized as
       * necessary. When allocated, length is always a power of two.
       * (We also tolerate length zero in some operations to allow
       * bootstrapping mechanics that are currently not needed.)
       */
      transient Node<K,V>[] table;
    
      /**
       * Holds cached entrySet(). Note that AbstractMap fields are used
       * for keySet() and values().
       */
      transient Set<Map.Entry<K,V>> entrySet;

数组名叫table,初始化时为空。HashMapEntry/Node是HashMap的静态内部类,数据节点都保存在这里面:

  static class HashMapEntry<K, V> implements Entry<K, V> {
      final K key;
      V value;
      final int hash;
      HashMapEntry<K, V> next;  
  }

  //java8
  static class Node<K,V> implements Map.Entry<K,V> {
          final int hash;
          final K key;
          V value;
          Node<K,V> next;
  }
    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);
    }

    /**
     * Returns a power of two size for the given target capacity.
     */
    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }
}
    int threshold;// 所能容纳的key-value对极限 
    final float loadFactor;//负载因子 默认0.75
    int modCount;  
    int size;
    /**
     * The table, initialized on first use, and resized as
     * necessary. When allocated, length is always a power of two.
     * (We also tolerate length zero in some operations to allow
     * bootstrapping mechanics that are currently not needed.)
     */
    transient Node<K,V>[] table;

HashMap是通过transient Node<K,V>[]table来存储数据,Node就是数组的元素,每个Node其实就是一个key-value对,它持有一个指向下一个元素的引用,这就构成了链表。

那么为什么要有链表呢?原因是为了解决 哈希冲突 当我们新增或者查找一个元素的时候,我们都会通过将我们的key的hashcode通过哈希函数映射到数组中的某个位置,通过数组下标一次定位就可完成操作。
如果两个不同的元素,通过哈希函数得出的实际存储地址相同怎么办?也就是说,当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,其实这就是所谓的哈希冲突,也叫哈希碰撞。
哈希冲突的解决方案有多种:开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数法,链地址法,而HashMap即是采用了链地址法,也就是数组+链表的方式。

HashMap中的核心put方法:

    public V put(K key, V value) {
        // 对key的hashCode()做hash
        return putVal(hash(key), key, value, false, true);
    }

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //tab为空则创建
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //计算index,并对null做处理 
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            // 节点key存在,直接覆盖value
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //判断该链为红黑树
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            //该链为链表
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                         //链表长度大于8转换为红黑树进行处理
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        //超过最大容量 就扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

HashMap中put元素的时候,先根据key的hashCode重新计算hash值,根据hash值得到这个元素在数组中的位置(即下标),如果数组该位置上已经存放有其他元素了,则通过key 的 equals 比较返回 true,新添加 Node 的 value 将覆盖集合中原有 Node 的 value,但key不会覆盖。如果这两个 Node 的 key 通过 equals 比较返回 false,新添加的 Node 将与集合中原有 Node 形成 链表。

所以重写equals方法必须要重写hashcode方法

HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表,那么对于查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。

Hashtable

    public class Hashtable<K,V> extends Dictionary<K,V>
    implements Map<K,V>, Cloneable, java.io.Serializable{}
    
    public Hashtable(int initialCapacity) {
        this(initialCapacity, 0.75f);
    }

    /**
     * Constructs a new, empty hashtable with a default initial capacity (11)
     * and load factor (0.75).
     */
    public Hashtable() {
        this(11, 0.75f);
    }
    public Hashtable(Map<? extends K, ? extends V> t) {
        this(Math.max(2*t.size(), 11), 0.75f);
        putAll(t);
    }

Hashtable继承Dictionary类,同样是通过key-value键值对保存数据的数据结构。 解决冲突时与HashMap也一样也是采用了散列链表的形式,Hashtable和HashMap最大的不同是Hashtable的方法都是同步的,在多线程中,你可以直接使用Hashtable,而如果要使用HashMap,则必须要自己实现同步来保证线程安全。当然,如果你不需要使用同步的话,HashMap的性能是肯定优于Hashtable的。此外,HashMap是接收null键和null值的,而Hashtable不可以。

Hashtable于HashMap的区别

  • HashMap是继承自AbstractMap类,而HashTable是继承自Dictionary类,不过它们都实现了同时实现了map、Cloneable、Serializable这三个接口。
  • HashMap支持key或者value为null,而HashTable不支持。
  • Hashtable默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。
  • 计算hash值的方法不同
         //Hashtable
         for (int i = oldCapacity ; i-- > 0 ;) {
             for (HashtableEntry<K,V> old = (HashtableEntry<K,V>)oldMap[i] ; old != null ; ){
                 HashtableEntry<K,V> e = old;
                 old = old.next;
    
                 int index = (e.hash & 0x7FFFFFFF) % newCapacity;
                 e.next = (HashtableEntry<K,V>)newMap[index];
                 newMap[index] = e;
             }
         
         //HashMap
         static final int hash(Object key) {
             int h;
             return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
         }
    
    Hashtable在计算元素的位置时需要进行一次除法运算,而除法运算是比较耗时的。

TreeMap

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

有序散列表,实现SortedMap接口,底层通过红黑树实现。可以根据key的自然顺序进行自动排序,当key是自定义对象时,TreeMap也可以根据自定义的Comparator进行排序。另外,TreeMap和HashMap一样,也是非同步的。