春秋招java后端方向技能全面突破-基础篇05

HashMap

首先要说的是HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。同时HashMap 的实现不是同步的,这也就是意味着它不是线程安全的。它的key、value都可以为null。此外,HashMap中的映射也不是有序的(由于没有实现了NavigableMap接口,对于NavigableMap来说,它是一个可导航的键-值对集合)。

对于Hashmap来说,它是存在两个参数会影响其性能,“初始容量”(这里得容量是指哈希表中桶的数量,初始容量是表在创建时的容量)和“加载因子”(哈希表在达到自动增加之前所能达到最大的尺度),当哈希表的数目超过当前容量和加载因子乘积的时候,就要对其进行重建表结构,即rehash,使哈希表变成大约两倍的桶数。

整体的一个实现结构如下:(!!这里要特别注意它的链表为单向链表

HashMap是通过"拉链法"实现的哈希表。它包括几个重要的成员变量:table, size, threshold, loadFactor, modCount。
  table是一个Entry[]数组类型,而Entry实际上就是一个单向链表。哈希表的"key-value键值对"都是存储在Entry数组中的。 
  size是HashMap的大小,它是HashMap保存的键值对的数量。 
  threshold是HashMap的阈值,用于判断是否需要调整HashMap的容量。threshold的值="容量*加载因子",当HashMap中存储数据的数量达到threshold时,就需要将HashMap的容量加倍。
  loadFactor就是加载因子。 
  modCount是用来实现fail-fast机制的。

这里提一下数据节点Entry的数据结构:

static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;
    V value;
    // 指向下一个节点
    Entry<K,V> next;
    final int hash;

    // 构造函数。
    // 输入参数包括"哈希值(h)", "键(k)", "值(v)", "下一节点(n)"
    Entry(int h, K k, V v, Entry<K,V> n) {
        value = v;
        next = n;
        key = k;
        hash = h;
    }

    public final K getKey() {
        return key;
    }

    public final V getValue() {
        return value;
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    // 判断两个Entry是否相等
    // 若两个Entry的“key”和“value”都相等,则返回true。
    // 否则,返回false
    public final boolean equals(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry e = (Map.Entry)o;
        Object k1 = getKey();
        Object k2 = e.getKey();
        if (k1 == k2 || (k1 != null && k1.equals(k2))) {
            Object v1 = getValue();
            Object v2 = e.getValue();
            if (v1 == v2 || (v1 != null && v1.equals(v2)))
                return true;
        }
        return false;
    }

    // 实现hashCode()
    public final int hashCode() {
        return (key==null   ? 0 : key.hashCode()) ^
               (value==null ? 0 : value.hashCode());
    }

    public final String toString() {
        return getKey() + "=" + getValue();
    }

    
}

而对于拉链发解决Hash冲突,则为以下步骤:

  1. 得到一个 key
  2. 计算 key 的 hashValue
  3. 根据 hashValue 值定位到 data[hashValue] 。( data[hashValue] 是一条链表)
  4. 若 data[hashValue] 为空则直接插入
  5. 不然则添加到链表末尾

再说说HashMap的方法,clear()(清空HashMap。它是通过将所有的元素设为null来实现的),containsKey()(判断HashMap是否包含key,containsKey() 首先通过getEntry(key)获取key对应的Entry,然后判断该Entry是否为null,这里需要强调的是:HashMap将“key为null”的元素都放在table的位置0处,即table[0]中;“key不为null”的放在table的其余位置

这里提两个方法:

get() 的作用是获取key对应的value:

public V get(Object key) {
    if (key == null)
        return getForNullKey();
    // 获取key的hash值
    int hash = hash(key.hashCode());
    // 在“该hash值对应的链表”上查找“键值等于key”的元素
    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;
}

put() 的作用是对外提供接口,让HashMap对象可以通过put()将“key-value”添加到HashMap中:

public V put(K key, V value) {
    // 若“key为null”,则将该键值对添加到table[0]中。
    if (key == null)
        return putForNullKey(value);
    // 若“key不为null”,则计算该key的哈希值,然后将其添加到该哈希值对应的链表中。
    int hash = hash(key.hashCode());
    int i = indexFor(hash, table.length);
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        // 若“该key”对应的键值对已经存在,则用新的value取代旧的value。然后退出!
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }

    // 若“该key”对应的键值对不存在,则将“key-value”添加到table中
    modCount++;
    addEntry(hash, key, value, i);
    return null;
}

最后提一下HashMap的遍历方式:

1 遍历HashMap的键值对

第一步:根据entrySet()获取HashMap的“键值对”的Set集合。
第二步:通过Iterator迭代器遍历“第一步”得到的集合。

2 遍历HashMap的键

第一步:根据keySet()获取HashMap的“键”的Set集合。
第二步:通过Iterator迭代器遍历“第一步”得到的集合。

3 遍历HashMap的值

第一步:根据value()获取HashMap的“值”的集合。
第二步:通过Iterator迭代器遍历“第一步”得到的集合。

TreeMap

首先我们应该知道TreeMap 是一个有序的key-value集合,它是通过红黑树(该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法,具体红黑树内容我会在后续的数据结构中详细讲的)实现的,对于我们提及的R-B Tree,它包含几个重要的成员变量: root, size, comparator。
  root 是红黑数的根节点。它是Entry类型,Entry是红黑数的节点,它包含了红黑数的6个基本组成成分:key(键)、value(值)、left(左孩子)、right(右孩子)、parent(父节点)、color(颜色:Red-false   Black-ture)。Entry节点根据key进行排序,Entry节点包含的内容为value,红黑数排序时,根据Entry中的key进行排序;Entry中的key比较大小是根据比较器comparator来进行判断的,size是红黑数中节点的个数。(由于后端面试中TreeMap提及的不是特别多,手撕红黑AVL之类就有点过分了,所以这里就不多提及了)

HashSet

首先明确一点,不管是HashSet还是TreeSet都是继承自Map的,比如继承自HashMap的HashSet具有了非同步(!!如果多个线程同时访问一个哈希 set,而其中至少一个线程修改了该 set,那么它必须 保持外部同步。这通常是通过对自然封装该 set 的对象执行同步操作来完成的。如果不存在这样的对象,则应该使用 Collections.synchronizedSet 方法来“包装” set。最好在创建时完成这一操作,以防止对该 set 进行意外的不同步访问)的特点,且可以为null值,同时Set还具有无序的特点。

TreeSet(非线程安全)

TreeSet 是一个有序的集合,它的作用是提供有序的Set集合。它继承于AbstractSet抽象类,实现了NavigableSet<E>, Cloneable, java.io.Serializable接口。(!!这里注意一下,TreeSet中的元素支持2种排序方式:自然排序 或者 根据创建TreeSet 时提供的 Comparator 进行排序。这取决于使用的构造方法。)

猜你喜欢

转载自blog.csdn.net/qq_40901379/article/details/84104276