Map集合

Map介绍

Map是一种键值对集合,并且每个键和每个值都可以是任何类型的对象。具体来说,它包含两个基本操作:将键与值关联起来的操作(put)、根据键查找值的操作(get)。

Map是一种基于哈希表(Hash Table)的数据结构,其内部基本结构是一个数组(桶),每一个桶都对应着一个链表。Map中的每一个元素(键值对)存储在对应的链表中。因此,Map的实际存储结构是一个数组加链表的结合体。

Map关系图

动态扩容:

 Map的动态扩容是指在使用Map时,如果我们往Map中不断添加新的键值对,当Map已经存储的键值对达到一定数量时,Map的内部数据结构将自动扩容以适应更多的键值对。
 Map的扩容机制基于一个称为“负载因子”的概念。负载因子是Map中实际存储元素数和容量(数组长度)之比。当负载因子超过设定的阈值(通常为0.75)时,Map会自动进行扩容,以保证数据结构的性能。

Map的扩容过程包括以下几个步骤:

  1. 构建一个新的桶数组,长度为原数组长度的两倍;
  2. 遍历原桶数组,将其中的每个桶中的键值对重新分配到新的桶数组中;
  3. 将新构建的桶数组设置为Map的内部存储结构。

Map的主要数据接口有以下几个:

  1. put(K key, V value):将键值对添加到Map中或将指定键的值替换为新值;
  2. get(Object key):返回与指定键相关联的值;
  3. remove(Object key):从Map中删除指定键及其关联的值;
  4. containsKey(Object key):判断Map中是否包含指定的键;
  5. containsValue(Object value):判断Map中是否包含指定的值;
  6. keySet():返回Map中的所有键的集合。

Map的优点主要包括:

  1. Map提供了基于键值对的快速查找,可以快速找到所需的数据;
  2. Map中的键值对可以是任意类型,使得它可以适用于各种数据类型;
  3. Map支持动态扩容,可以处理大型数据集合。

但是,Map也存在一些缺点,如:

  1. Map的实现类对内存的消耗比较大,因为它需要维护键和值之间的映射关系;
  2. 在遍历Map时,Map的键和值之间的关系不总是确定的,因为Map是一种无序集合;
  3. Map中的键是唯一的,如果存在重复的键,则旧值将被替换为新值。

Java中有多个Map的实现类,每个实现类有自己的特点和适用场景。以下是Java中常见的Map实现类及其特点:

  1. HashMap: 这是Java中最常用的Map实现类,它是无序的,并且允许空键和空值。HashMap的存储是基于哈希表实现的,因此查找速度快,但遍历时的顺序不确定;
  2. LinkedHashMap: 它是HashMap的扩展类,能够保持键值对的插入顺序,因此可以按照插入顺序遍历Map;
  3. TreeMap: 它是基于红黑树实现的有序的Map,能够按照键的自然顺序或自定义的顺序进行排序,因此可以实现排序后遍历Map;
  4. Hashtable: 与 HashMap类似,不同的是:key和value的值均不允许为null;它支持线程的同步,即任一时刻只有一个线程能写Hashtable,因此也导致了Hashtale在写入时会比较慢,只有hashtable是继承自Dictionary抽象类的,hashMap和treeMap都继承自AbstractMap抽象类,

除了上述常用的Map实现类,Java还提供了一些其他的Map实现类,如EnumMap、ConcurrentHashMap、Properties等。下面对这些Map的实现类进行简单介绍:

  1. EnumMap: 用于枚举类型的Map,键必须是同一个枚举类型的值。它是基于数组实现的,因此在查找和遍历时效率很高;
  2. ConcurrentHashMap: 这是一种高并发的Map,它是线程安全的,多个线程可以同时执行put和get等操作,不会发生冲突和数据错乱。它的核心思想是将Map分成多个片段,每个片段独立锁定,因此多个线程可以同时操作不同片段的数据;
  3. Properties: 这是一种特殊的Map,它的键和值都是字符串类型。通常情况下,它用于读取配置文件或国际化资源文件中的键值对。

总体来说,Map是Java中极其重要的一种数据结构,它非常适用于键值对的存储和快速查找。在选择Map实现类时,需要根据场景需求考虑其特点和适用性。

HahsMap实现Map

在HashMap中,键和值都可以为null,而且可以支持并发访问。HashMap通过数组和链表相结合的方式实现了存储和查找。

底层数据结构

HashMap的底层数据结构是一个Node数组,每个Node是一个链表的头节点,链表中的节点包含了键值对信息。如果链表的长度超过了一定阈值(TREEIFY_THRESHOLD),则会将链表转化为红黑树以提高查找效率。如果红黑树的节点数少于一定阈值(UNTREEIFY_THRESHOLD),则会将红黑树转化为链表,以节省内存。

HashMap的数据结构如下图所示:

hashMap

底层原理

 HashMap 实际上是一个“链表散列”的数据结构,即数组和链表的结合体。这是 jdk8 之前的实现方式,但是在JDK8 后对 HashMap 进行了底层优化,改为了由数组+链表+红黑树实现,主要的目的是提高查找效率。

 HashMap 的主结构类似于一个数组,添加值时通过 key 确定储存位置.每个位置是一个 Entry 的数据结构,该结构可组成链表.当发生冲突时,相同 hash 值的键值对会组成链表.这种数组+链表的组合形式大部分情况下都能有不错的性能效果,Java6、7 就是这样设计的.然而,在极端情况下,一组(比如经过精心设计的)键值对都发生了冲突,这时的哈希结构就会退化成一个链表,使 HashMap 性能急剧下降.所以在 Java8 中,HashMap 的结构实现变为数组+链表+红黑树

HahMap数据结构

 可以看出,HashMap 底层就是一个数组结构。数组中的每一项又是一个链表,当新建一个 HashMap 时,就会初始化一个数组.简单地说,HashMap 在底层将 key-value 当成一个整体进行处理,这个整体就是一个 Entry 对象。HashMap底层采用一个 Entry[] 数组来保存所有的 key-value 对,当需要存储一个 Entry 对象时,会根据 hash 算法来决定其在数组中的存储位置,在根据 equals 方法决定其在该数组位置上的链表中的存储位置;当需要取出一个 Entry 时,也会根据 hash 算法找到其在数组中的存储位置,再根据 equals 方法从该位置上的链表中取出该Entry。

底层实现

HashMap的底层实现主要包含以下几个核心方法:put、get、remove和resize。

put方法

put方法用于向HashMap中添加键值对,其实现如下:

	public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

    /**
     * Implements Map.put and related methods.
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            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);
                        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;
    }

首先,如果table数组还没有被初始化,则需要调用inflateTable方法进行初始化。然后,判断key是否为null,如果是null则调用putForNullKey方法处理。否则,计算key的hash值,并根据hash值得到table数组的下标i。在链表头部添加节点,如果发现存在相同的键则更新其对应的值;否则,添加新的节点。

get方法

get方法用于获取HashMap中指定键对应的值,其实现如下:

    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

    /**
     * Implements Map.get and related methods.
     *
     * @param hash hash for key
     * @param key the key
     * @return the node, or null if none
     */
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

首先,如果传入的key是null则调用getForNullKey方法处理。否则,计算key的hash值并找到对应的table中的节点。如果该节点为null,则返回null;否则,遍历链表或者红黑树,如果找到了对应节点则返回其对应的值。

remove方法

remove方法用于从HashMap中删除指定键对应的值,其实现如下:

    public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }

    /**
     * Implements Map.remove and related methods.
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to match if matchValue, else ignored
     * @param matchValue if true only remove if value is equal
     * @param movable if false do not move other nodes while removing
     * @return the node, or null if none
     */
    final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            Node<K,V> node = null, e; K k; V v;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            else if ((e = p.next) != null) {
                if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else {
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                else if (node == p)
                    tab[index] = node.next;
                else
                    p.next = node.next;
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

首先,根据指定的key计算hash值并找到对应的table中的节点。如果该节点为null,则返回null;否则,遍历链表或者红黑树。如果找到了对应节点并且节点的value和指定的value相等,则将该节点移除,否则返回null。

resize方法

resize方法用于对HashMap进行扩容,其实现如下:

    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

首先,计算新的table的大小和阈值。如果旧的table已经达到了最大容量,则直接返回旧的table。通过位运算来确定节点的位置。如果节点的next指针为null,则将节点直接放入新的table中。如果节点是TreeNode,则调用split方法拆分树节点。否则,按照节点的hash值将节点放入不同的链表中。

除了这些方法之外HashMap中还有其他的一些方法,如hashCode,hash,containsKey等等,这里就不一一叙述,想了解的可以去HashMap中点一下,看一看起源码.

LinkedHashMap实现Map

LinkedHashMap是HashMap的一个子类,是一个有序的Map容器。它在HashMap的基础上实现了按照插入顺序或者是最近使用顺序来访问元素的功能,同时也保持了HashMap的快速查询和插入的特性。

LinkedHashMap类的声明

public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>

LinkedHashMap继承了HashMap,所以它拥有HashMap的所有方法,同时它也实现了Map接口,可以充当Map的一个实现类。下面我们来看一下LinkedHashMap的构造函数

构造函数


    public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

LinkedHashMap的构造函数包含三个参数,分别是初始容量,装载因子和accessOrder。其中,初始容量和装载因子的意义和HashMap中的一样,不再赘述。accessOrder代表按照什么顺序来访问Map中的元素,如果accessOrder设为true,那么访问Map中的元素将按照最近使用的顺序进行访问,否则按照插入顺序进行访问。

LinkedHashMap排序

LinkedHashMap中最容易让人忽略的部分就是accessOrder,它的默认值是false,表示按照插入顺序访问。在Java8中,如果我们不指定accessOrder,那么默认的LinkedHashMap就是按照元素的插入顺序进行访问。因此,在Java8及以上的版本中,我们可以使用以下代码创建一个按照元素的插入顺序进行访问的LinkedHashMap:

LinkedHashMap<Integer, String> map = new LinkedHashMap<>(16, 0.75F, false);

put()方法在HashMap中的put()方法源码是这样的:

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

在LinkedHashMap中,put()方法的源码是这样的:

public V put(K key, V value) {
    return put(key, value, false);
}

可以看到,LinkedHashMap中的put()方法多了一个参数accessOrder,这个参数是一个布尔值,决定了是否需要按照最近使用顺序进行访问。put()方法的实现与HashMap中的put()方法类似,主要的实现逻辑如下:

  • 判断Map是否为空,如果为空则创建一个新的Entry并将key和value赋值给它;
  • 判断Map中是否已经存在这个key,如果存在则更新value值;
  • 如果不存在,则判断Map的当前实际长度是否超过了容量,如果超过了容量则进行resize操作;
  • 为key和value创建一个新的Entry;
  • 将新的Entry插入到Table数组中;
  • 如果accessOrder为true,执行一次afterNodeInsertion()方法,将新插入的节点放到对应的链表最后;如果accessOrder为false,则不做任何操作。

LinkedHashMap中的put()方法比HashMap中的put()方法多了一个accessOrder参数的处理,如果为true,则要执行一次afterNodeInsertion()方法。

afterNodeInsertion()方法

    void afterNodeInsertion(boolean evict) { // possibly remove eldest
        LinkedHashMap.Entry<K,V> first;
        if (evict && (first = head) != null && removeEldestEntry(first)) {
            K key = first.key;
            removeNode(hash(key), key, null, false, true);
        }
    }

afterNodeInsertion()方法是LinkedHashMap中的一个私有方法,主要的实现逻辑是:

  • 将新插入的元素从链表中删除,然后重新插入到链表的最后;
  • 如果Map当前的实际长度大于了扩容阈值,则执行一次resize()操作。

关于LinkedHashMap的访问顺序

如果accessOrder为true,则访问LinkedHashMap中的元素将按照最近使用顺序进行访问。具体的实现方式是:在LinkedHashMap中,每个Entry对象除了key、value和hash值以外,还包含了两个Entry类型的属性,分别是before和after。这两个属性构成了一个双向链表,当一个元素被插入到Map中时,它会被插入到这个双向链表的末尾。如果一个元素已经存在于Map中并被访问过,那么它会被从链表中删除,然后重新插入到链表的末尾。

例如,我们创建一个accessOrder为true的LinkedHashMap,并往里面插入五个元素:

LinkedHashMap<Integer, String> map = new LinkedHashMap<>(16, 0.75F, true);
map.put(1, "a");
map.put(2, "b");
map.put(3, "c");
map.put(4, "d");
map.put(5, "e");

此时,这五个元素在Map中的顺序是1-2-3-4-5,也就是元素的插入顺序。

demo1

假设我们按照如下顺序访问这五个元素:

map.get(3);
map.get(2);
map.get(5);

那么,这五个元素在Map中的顺序就变成了1-4-3-2-5,也就是按照最近使用顺序进行重新排列。在访问元素的过程中,元素被重新插入到链表的末尾,从而改变了元素在Map中的顺序。

demo2

总结

LinkedHashMap是一个有序的Map容器,它在HashMap的基础上实现了按照插入顺序或者是最近使用顺序来访问元素的功能。如果accessOrder为true,则访问LinkedHashMap中的元素将按照最近使用顺序进行访问,通过维护一个双向链表实现。其实现原理和HashMap类似,主要区别在于它加入了双向链表和accessOrder参数的处理。

TreeMap实现Map

TreeMap是Java中的一种Map集合,它实现了Map接口,基于红黑树数据结构来维护键值对的有序关系。TreeMap中的键值对是按照键的自然顺序或者指定的比较器顺序进行排序的,这使得TreeMap适用于需要按照自然顺序或者自定义顺序进行排序的场景。

底层数据结构

TreeMap底层采用的是红黑树数据结构来维护键值对的有序性。红黑树是一种自平衡二叉查找树,它保证了每个节点的左子树和右子树的高度差最多为1,从而保证了树的平衡性。红黑树中的每个节点都有一个颜色,黑色或者红色,根节点为黑色。红黑树的插入和删除操作会通过旋转和变色来维护树的平衡性。

TreeMap内部维护了一个红黑树的根节点root,每个节点包含了一个键值对Entry和三个指针,分别指向左子树left,右子树right和父节点parent。Entry包含了键key和值value,以及指向前驱节点和后继节点的指针。

数据存储和排序

TreeMap中的键值对是按照键的自然顺序或者指定的比较器顺序进行排序的。在TreeMap中,Entry对象按照键的排序规则被插入到红黑树中,每个节点都具有一个唯一的键,因此不存在键重复的情况。红黑树根据键进行排序,因此,如果两个键相等,那么它们被视为同一个对象,而后续的put操作会替换旧值。

当我们向TreeMap中插入一个键值对时,首先判断该树是否为空。如果为空,则直接将键值对包装成Entry对象作为根节点插入即可。如果不为空,则从根节点开始递归搜索插入位置。如果插入的键小于当前节点的键,则将插入节点插入到当前节点的左子树中;如果插入的键大于当前节点的键,则将插入节点插入到当前节点的右子树中。插入完成后,需要对红黑树进行平衡,以保证树的高度平衡。

TreeMap的时间复杂度分析

在TreeMap中,查找、插入和删除一个元素的时间复杂度均为O(logn),其中n为TreeMap中的元素数量。这是因为TreeMap内部采用了红黑树这种自平衡的二叉查找树来维护元素的有序性,而红黑树的查找、插入和删除操作的时间复杂度均为O(logn)。

TreeMap中的方法说明

TreeMap实现了Map接口,提供了丰富的方法来操作键值对。下面是TreeMap中常用的方法:

put(K key, V value):插入一个键值对,如果该键已经存在,则会替换旧值并返回旧值。

get(Object key):返回键对应的值,如果不存在该键,则返回null。

remove(Object key):删除指定键对应的键值对,如果删除成功,则返回被删除的值,否则返回null。

isEmpty():判断该Map是否为空,如果为空,则返回true,否则返回false。

size():返回该Map中键值对的数量。

clear():清空该Map。

containsKey(Object key):判断该Map中是否包含指定的键,如果包含,则返回true,否则返回false。

containsValue(Object value):判断该Map中是否包含指定的值,如果包含,则返回true,否则返回false。

keySet():返回该Map中所有键组成的Set集合。

values():返回该Map中所有值组成的Collection集合。

entrySet():返回该Map中所有键值对组成的Set集合。每个元素是一个Map.Entry对象。

综上所述,TreeMap是一种基于红黑树数据结构的Map集合,能够自动对键进行排序,并且提供了丰富的操作方法。它的实现方法是非常重要的,因为在实际开发中,我们经常需要使用Map集合来存储和操作数据。

Hashtable实现Map

Hashtable是Java中的一个类,它实现了Java中的Map接口。它是一个基于哈希表实现的map,可以用来存储一组键值对,其中键和值都是对象。Hashtable使用键作为下标来访问它所存储的值,这就能够保证Hashtable对于查找和插入的操作都有很高的效率。

Hashtable的底层逻辑

Hashtable的底层是基于哈希表实现的。哈希表就是一种使用哈希函数来计算键值的索引位置的数据结构。哈希函数可以将任意长度的输入(键值)映射为固定长度的输出(索引位置)。Hashtable使用哈希函数来计算键值的索引位置,当插入或查找一个键值对时,该键值对的键会经过哈希函数计算,得到对应的哈希值,然后根据哈希值找到对应的索引位置,最终将值存储到该索引位置中。

哈希表的一个核心问题就是解决哈希冲突。当两个不同的键值经过哈希函数计算后得到的哈希值相同,它们就会被存储到哈希表的同一个索引位置中,这就是哈希冲突。常见的解决哈希冲突的方法有两种:开放地址法和链地址法。Hashtable采用的是链地址法,即将每个索引位置上的元素组织成链表,当冲突发生时,将新的键值对插入到该位置的链表尾部,解决哈希冲突的同时也保证了元素的有序性。

Hashtable的底层代码解析

下面我们来看一下Hashtable的底层代码,这里我们以JDK1.8版本的Hashtable为例进行讲解。

1. Hashtable的属性

public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable {

    /* 定义了Hashtable的一些常量属性 */
    private static final int DEFAULT_INITIAL_CAPACITY = 11;
    private static final float DEFAULT_LOAD_FACTOR = 0.75f;
    private static final int MAXIMUM_CAPACITY = 1 << 30;
    private transient Entry<?,?>[] table;
    private transient int count;
    private int threshold;
    private final float loadFactor;
......

首先我们看到了Hashtable中的一些常量属性。其中,DEFAULT_INITIAL_CAPACITY表示Hashtable的默认初始容量,DEFAULT_LOAD_FACTOR表示Hashtable的默认装载因子(即空间利用率),MAXIMUM_CAPACITY表示Hashtable的最大容量,table是存储所有键值对的数组,count表示Hashtable中键值对的数量,threshold表示Hashtable自动扩容时的临界值,loadFactor表示Hashtable的装载因子。

2. 构造方法

Hashtable有多个构造方法,其中一个最常用的是:

    public Hashtable(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+
                initialCapacity);
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal Load: "+loadFactor);
        if (initialCapacity==0)
            initialCapacity = 1;
        this.loadFactor = loadFactor;
        threshold = initialCapacity;
        init();
    }

构造方法中首先判断了传入的参数是否合法,如果有不合法的参数会抛出IllegalArgumentException异常。然后会将传入的参数作为Hashtable的属性值,进一步初始化Hashtable。

3. 插入键值对

Hashtable的put方法是用来插入键值对的,该方法的底层逻辑实际上就是根据键的哈希值找到对应的索引位置,然后将该键值对插入到对应索引位置上的链表中。

    public synchronized V put(K key, V value) {
        // Make sure the value is not null
        if (value == null) {
            throw new NullPointerException();
        }

        // Makes sure the key is not already in the hashtable.
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                V old = (V)e.value;
                e.value = value;
                return old;
            }
        }

        modCount++;
        if (count >= threshold) {
            // Rehash the table if the threshold is exceeded
            rehash();

            tab = table;
            hash = key.hashCode();
            index = (hash & 0x7FFFFFFF) % tab.length;
        }

        // Creates the new entry.
        @SuppressWarnings("unchecked")
                Entry<K,V> e = (Entry<K,V>) tab[index];
        tab[index.put(key, value);
        table = tab;
        count++;
        return null;
    }

该方法首先根据传入的键值对中的键值计算哈希值,并根据哈希值找到对应的索引位置。如果该位置已经存在同样的键值对,则直接用新的值替换旧的值,并返回旧的值。如果该位置上没有同样的键值对,就需要将该键值对插入到链表的尾部,并更新count的值。如果Hashtable中的键值对数量超过了阈值(threshold),就会调用rehash方法进行扩容,从而保证Hashtable的装载因子不会过高。

4. 扩容

Hashtable会自动扩容,在键值对数量超过阈值(threshold)时会调用rehash方法进行扩容。当插入一个新的键值对时,Hashtable会先检查是否需要扩容,如果需要就先调用rehash方法进行扩容,然后再插入新的键值对。

protected void rehash() {
    int oldCapacity = table.length;
    Entry&lt;?,?&gt;[] oldMap = table;

    // overflow-conscious code
    int newCapacity = (oldCapacity &lt;&lt; 1) + 1;
    if (newCapacity - MAXIMUM_CAPACITY &gt; 0) {
        if (oldCapacity == MAXIMUM_CAPACITY)
            // Keep running with MAXIMUM_CAPACITY buckets
            return;
        newCapacity = MAXIMUM_CAPACITY;
    }
    Entry&lt;?,?&gt;[] newMap = new Entry&lt;?,?&gt;[newCapacity];

    modCount++;
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    table = newMap;

    for (int i = oldCapacity ; i-- &gt; 0 ;) {
        for (Entry&lt;Object,Object&gt; old = (Entry&lt;Object,Object&gt;)oldMap[i] ; old != null ; ) {
            Entry&lt;Object,Object&gt; e = old;
            old = old.next;

            int index = (e.hash &amp; 0x7FFFFFFF) % newCapacity;
            e.next = (Entry&lt;K,V&gt;)newMap[index];
            newMap[index] = e;
        }
    }
}

该方法会先将旧数组的所有键值对重新放置到新的数组中,并重新计算它们的索引位置。具体实现上,它会根据旧数组的长度计算新数组的长度,并将每个键值对插入到新数组中对应的索引位置上。这样就可以应对一些哈希函数不够均匀,导致哈希冲突比较严重的情况。

5. 查找键值对

Hashtable的get方法用来查找键值对。该方法的底层逻辑实际上就是根据键的哈希值找到对应的索引位置,然后查找该索引位置上的链表,找到对应的键值对并返回其值。

    public synchronized V get(Object key) {
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                return (V)e.value;
            }
        }
        return null;
    }

该方法首先根据传入的键计算哈希值,并根据哈希值找到对应的索引位置。然后检查该位置上的链表,找到与传入键相同的键值对,如果找到则返回该键值对的值,否则返回null。

6. 删除键值对

Hashtable的remove方法用来删除键值对。该方法的底层逻辑实际上也是根据键的哈希值找到对应的索引位置,然后查找该索引位置上的链表,找到对应的键值对并删除它。

    public synchronized V remove(Object key) {
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;

        for (Entry<?,?> e = tab[index], prev = null; e != null; prev = e, e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                modCount++;
                if (prev != null) {
                    prev.next = e.next;
                } else {
                    tab[index] = e.next;
                }
                count--;
                V oldValue = (V)e.value;
                e.value = null;
                return oldValue;
            }
        }
        return null;
    }

该方法与get方法类似,首先根据传入的键计算哈希值,并根据哈希值找到对应的索引位置。然后检查该位置上的链表,找到与传入键相同的键值对,如果找到则删除该

ConcurrentHashMap实现Map

ConcurrentHashMap是Java中的一个线程安全的哈希表,实现了Map接口。它主要是针对多个线程同时访问一个哈希表时的线程安全问题进行优化。在多线程环境下,如果使用HashMap来实现哈希表,可能会出现线程不安全的现象,如读写竞争和死锁等。而ConcurrentHashMap在实现线程安全的同时,也保证了较高的并发性能。

ConcurrentHashMap的底层是基于散列表实现的。在单线程环境下,散列表是非常高效的,但在多线程环境下,需要考虑线程安全问题,因此ConcurrentHashMap在设计时引入了加锁机制,分段锁的思想。即将散列表分为一组组的Segement,每个Segment内部是线程安全的,多个线程可以同时访问不同的Segment,从而提高并发性能。

Segment的结构是一个数组,每个元素是一个HashEntry对象。HashEntry是键值对结构,每个Segment内部通过锁的方式来保证多线程安全性。在进行添加、修改、删除、查询等操作时,只需要锁住对应的Segment,而不是锁住整个哈希表,从而提高并发度。

ConcurrentHashMap的底层代码。

ConcurrentHashMap是一种线程安全的哈希表实现,它在内部使用了一种叫做分段锁(Segment)的并发控制机制。ConcurrentHashMap将内部的哈希表分成了若干个片段(Segment),每个Segment通过一个可重入锁(ReentrantLock)进行保护,不同Segment之间是相互独立的,这样在进行写操作时只需要锁住对应的Segment,而不需要锁住整个哈希表,从而提高了并发度。

ConcurrentHashMap中的每个Segment都是一个类,它实现了Map接口,同时包含了一个HashEntry数组和一个可重入锁:

    static final class Segment<K,V> extends ReentrantLock implements Serializable {
        // ...
        transient volatile HashEntry<K,V>[] table;
    }

HashEntry是一个内部类,用来存储键值对:

    static final class HashEntry<K,V> {
        final int hash;
        final K key;
        volatile V value;
        final HashEntry<K,V> next;
        // ...
    }

插入元素时,需要先根据键值的哈希值来确定它所应该存储的Segment,然后在这个Segment上进行插入操作。在Segment中插入元素时,会先判断哈希表中是否存在该键值,如果存在,就更新它的值,否则就将它插入到链表的头部。需要注意的是,当Segment中的元素数量达到一定阈值(默认为16),就会触发一次扩容操作,这样可以保证哈希表的负载因子始终在一个较低的范围内,从而提高了操作效率。

    final V put(K key, int hash, V value, boolean onlyIfAbsent) {
        // ...
        HashEntry<K,V>[] tab = table;
        int index = (tab.length - 1) & hash;
        HashEntry<K,V> first = tab[index];
        HashEntry<K,V> e = first;
        while (e != null && (e.hash != hash || !key.equals(e.key)))
            e = e.next;
        // ...
    }

查询元素时,需要先根据键值的哈希值来确定它所应该存储的Segment,然后在这个Segment中查找对应的键值对。在Segment中查找元素时,会先判断链表头部的哈希值是否等于要查找的键值的哈希值,如果相等,则遍历链表找到对应的键值对。

    final V get(Object key, int hash) {
        // ...
        HashEntry<K,V> e = first;
        while (e != null) {
            if (e.hash == hash && key.equals(e.key))
                return e.value;
            e = e.next;
        }
        // ...
    }

删除元素时,需要先根据键值的哈希值来确定它所应该存储的Segment,然后在这个Segment中查找并删除对应的键值对。在Segment中删除元素时,同样需要遍历链表找到对应的键值对,并删除它。

    final V remove(Object key, int hash, Object value) {
        // ...
        HashEntry<K,V> pred = null, e = first;
        while (e != null && (e.hash != hash || !key.equals(e.key))) {
            pred = e;
            e = e.next;
        }
        // ...
    }

ConcurrentHashMap还可以通过一些高级的方法来进一步提高它的并发性能和可靠性,比如key的分布式、精细化的控制锁的力度和时限等等。

ConcurrentHashMap的常见操作:

1. 添加元素put方法:

    public V put(K key, V value) {
        if (value == null){
            throw new NullPointerException();
        }
        int hash = hash(key.hashCode());
        return segmentFor(hash).put(key, hash, value, false);
    }

可以看到,put方法首先根据key的hashCode计算出hash值,然后通过segmentFor方法找到对应的Segment,然后在Segment中调用put方法,将键值对添加到哈希表中。

2. 删除元素remove方法:

public V remove(Object key) {
    int hash = hash(key.hashCode());
    return segmentFor(hash).remove(key, hash, null);
}

同样是根据key的hashCode值找到对应的Segment,在Segment中调用remove方法删除键值对。

3. 查询元素get方法:

public V get(Object key) {
    int hash = hash(key.hashCode());
    return segmentFor(hash).get(key, hash);
}

也是根据key的hashCode值找到对应的Segment,在Segment中调用get方法查找对应的值。

以上是ConcurrentHashMap的一部分代码,通过分析可以看出,ConcurrentHashMap在实现线程安全的同时还保证了并发度和性能。但是需要注意的是,虽然ConcurrentHashMap是线程安全的,但并不是所有操作都是原子性的。例如,在多线程并发地执行统计操作时,并不能保证结果的准确性。如果需要求得严格的精度,可能需要添加额外的同步机制或者使用其他的线程安全的数据结构。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值