05 集合-Map(一)

Map

与Colleciton一样,Map是一个独立的根接口。它是一种 key - value形式的数据集合管理容器,key是独立的,不相同的,可以通过key快速查到value,一个key下会有一个或多个value。

Map接口里的方法,也包含增加、删除、查询、修改这四类操作:

增加:

  • put,单条 key - value关系添加
  • putAll,多条key-value关系添加
  • putIfAbsent,如果请求参数key没找到value,那边就把本次的key-value通过put方法添加进集合

查询:

  • get,根据key获取value
  • getOrDefault,根据key获取value,如果没获取到,就用默认值返回。
  • containsKey,map里是否包含指定key
  • containsValue,map里是否包含指定value
  • entrySet,将key-value作为一个整体,生成一个set对象
  • keySet,将map里的key生成到一个set对象
  • values,将每个key对应的value,生成到一个集合里。

删除:

  • remove,移除指定的key和它的所有value、移除指定的key和它指定的value
  • clear,清空集合

修改:

  • replace,1.8新增的方法 指定key,用新value替换旧value
  • merge、compute、computeIfAbsent、computeIfPresent ,1.8新增的,新的value需要通过参数里的函数与旧value处理后生成,然后再把key和新value保存。

Map接口内部,定义了内部接口Entry:

这个接口是Map里一组对象(key-value)的方法定义类,用来把key-value这种有关联关系行为的一组数据,同时获取。

AbstractMap

抽象类,实现Map接口。对Map接口里的方法,进行了模板实现,例如有:

  • remove
  • putAll
  • values、keySet
  • size、clear
  • containsKey、containsValue

AbstractMap类里定义了静态内部类SimpleEntry,它实现了Entry接口。内部实现了setValue方法,equals方法、hashCode方法。

HashMap

继承AbstractMap类,实现Map接口,在业务场景里高频使用。

在HashMap内部,使用了数组来存储key-value这个关系,并创建了内部类Node,来管理key-value。并且同时也有内部类TreeNode继承Node类,用来做红黑树格式的数据存储。

transient Node<K,V>[] table;

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
    
    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }
      // 1.构造方法,生成Node
      // 2.key 、 value 的获取
      // 3.toString、 equals、hashCode方法的实现
      // 4.setValue方法修改value
}

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    TreeNode<K,V> parent;  // red-black tree links
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev;    // needed to unlink next upon deletion
    boolean red;
    TreeNode(int hash, K key, V val, Node<K,V> next) {
        super(hash, key, val, next);
    }
    // …… 树相关的方法
}

构造方法创建HashMap对象

在使用构造方法创建HashMap对象时,可通过构造方法参数指定容量和负载因子。容量指的是内部Node数组的长度;负载因子指的是数组自动扩容时的数据条件比例。

要求默认的初始化数组长度,必须是2的次方值

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

// 最大容量是2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;

// 负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

指定容量时,会根据输入的参数进行处理,得到的结果是刚刚大于输入值的2的次方值。

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);
}

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;
}

HashMap的put方法存放数据过程:

1.对key进行hash值计算,key的hashCode值与向右移动16位的结果,与原值进行异或计算,得到的结果就是key的哈希值。

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

这hash方法的实现原理是使用数字的二进制结果的高位与低位进行异或处理得到新的数字。

1.字符串“tome”的hashCode值是3565907,换算成二进制是1101100110100101010011。
2.把此二进制结果数字从右边开始数移除16位,得到110110。
3.异或处理:如果a、b两个值不相同,则异或结果为1。如果a、b两个值相同,异或结果为0
    1101100110100101010011
^                   110110
——————————————————————————
    1101100110100101100101
4. 得到的新的二进制数字串1101100110100101100101对应的十进制是3565925。

2.检查底层数组table,如果它是个空数组,那么扩展数组。使用默认初始化参数创建Node数组,此时数组长度为16,扩容值为 16 * 0.75 = 12。使用new HashMap创建对象,此时构造方法无参数时,底层数组table其实还没初始化呢,是个null数组,所以会进行第一次初始化设置初始属性。

3.使用要存储的key的哈希值与【数组长度-1】进行与运算,得到key要放入的索引位i,如果此索引位当前没有值,那么使用Node的构造函数,创建新Node对象,并放在此位置。

4.如果得到索引位i,并且此位置上已有数据,那么执行如下判断处理逻辑:

  • 如果已有数据的哈希值与新数据哈希值相等,并且key也都相等,那么用新的value覆盖已有数据的value;
  • 如果当前索引位的Node 不是 TreeNode,那么进行如下处理:
    1. 如果遍历到结尾next 都是null了还没有匹配的清空,那么使用Node的构造函数创建一个新Node,被原先尾部的Node进行指向;若此时链表Node的个数为8个了,那么执行将链表转成红黑树的方法。
    2. 遍历遇到的Node的哈希值与新数据哈希值相等,并且key也都相等,那么用新的value覆盖已有数据的value;
    3. 因为Node对象是个后指向的链表节点,所以对此位置的Node通过访问next对象进行循环遍历;
  • 如果当前索引位置的Node是TreeNode,也就是红黑树结构,那么遇到哈希值相等,key也相等的Node,就用新value覆盖树上匹配到的value,否则就给树新增一个TreeNode。

5.第4步操作中如果新数据过来做的是新Node创建和添加到底层的操作,那么对数组里Node个数size进行加1;

6.如果此时size 大于扩容值,就对Map底层进行扩容处理。

HashMap数组扩容流程:

上面介绍了数组在空的时候的初始化方案,下面介绍数组里已经有Node对象时的扩容方式。

  1. 如果此时数组的长度大于等于设定的最大值,2的30次方;设置扩容的触发数字为Integer.MAX_VALUE;返回当前数组,扩容结束。(一般很难达到);
  2. 设置新容量为旧容量的2倍。若此时新容量不足2的30次方,同时若旧容量已经大于64了,那么设置扩容的触发值为之前扩容值的2倍;
  3. 创建好新容量的Node数组后,开始循环遍历旧数组,当数组索引位置下有Node对象时,执行以下逻辑:
    1. 将Node对象取出来后,把此索引位置设置为null(旧数组最终不使用,加快内存回收);
    2. 如果此Node对象是个单Node,那么根据它的哈希值与新数组的最后一位索引值进行与运算,得到新的索引位置,并把Node对象放在新数组的此位置。
    3. 如果此Node对象是个TreeNode,那么执行红黑树在扩容时的分裂处理并在新数组里按指定索引放置Node对象。
    4. 如果此Node对象是链表,那么在遍历链表时,根据每个Node的哈希与原数组长度的与运算结果是否等于0,将大链表分成了高位和低位两个链表。低位是新数组索引位置等于旧数组索引位置,高位是新数组索引位置等于旧数组索引值 + 旧数组长度。
    5. 设置两个链表的尾Node的next指向为null,等于0的链表,就是低位链表;不等于0的链表就是高位链表。
  4. 方法返回新的底层数组。

链表转红黑树的方法逻辑:

  1. 链表不是立马就转成红黑树,如果当前底层数组的长度还小于64,那么就先做扩容处理,不做转换;
  2. 转换前,先把Node链表,转成TreeNode链表,后者的数据顺序和前者保持一致;
  3. 遍历TreeNode链表,确定好红黑树的Root节点,并设置Root节点无parent节点,red标识为false;
  4. 非Root节点的处理流程:
    1. root节点的哈希值大于非root节点的哈希值,设置dir=-1;小于时设置dir=1;相等且,map的key类型并没有实现Comparable接口 或者 实现了接口且与root的key的compareTo比较结果等于0,设置dir结果为两个key的hashCode值比较结果,非root的key小于rootkey的hashCode,设置dir=-1,否则=1;
    2. 如果dir<=0,选择root的left节点,否则选择右节点,并要求选择的节点此时为null;否则就一直遍历,此时的root不一定是树的根root。
    3. 更新非root节点的为root节点的left或者right节点,同时非root节点的parent要指向root节点。
    4. 基于当前非root节点对整个红黑树(root)做平衡操作,得到新的root节点
  5. 最终所有的TreeNode链表转成了一颗红黑树,根据当前根节点的哈希值与当前数组长度,进行计算root要放在数组的索引位置。
  6. 此索引位置有对象且不是root时,root对象的前置引用指向移除,后置引用指向移除;原前置引用和后置引用现在互相指向;如果该对象非空,那么该对象的前置引用指向root,root的后置引用指向该对象。
  7. 此时根节点TreeNode它的指向有parent、prev、left、right、next。对此位置的root进行强制检查,要求它必须符合树的特性。否则会报错。
  8. 处理完毕

从红黑树里找数据

  1. 查找条件是输入的key和它的哈希值,先确定树根节点的左节点和有节点
  2. 从根节点开始比较它的哈希值与输入的哈希值的大小,如果相等,且key也相等,那么就直接返回查到的此节点;
  3. 如果大于输入的哈希值,就从树节点的左侧开始遍历找起来;如果小于,就从树节点的右侧开始找起来,并更新比较的节点对象引用。
  4. 除了比较哈希值之外,还会根据输入的key的类型、hashCode进行比较,用来确定选择左侧还是右侧;
  5. 指定方向查询到叶子节点处还没查到匹配数据,返回null

红黑树添加数据的流程:

  1. 输入条件是新添加数据的key,key的哈希值, value,和当前map、数组;拿到当前key要放置数据的索引位置上的树root。
  2. 先根据新数据的哈希值和key,确定从红黑树新增数据的方向,左侧或右侧,如果匹配到key值相等的,直接覆盖value,后续流程终止。
  3. 直至找到树的最下层,将新数据创建为一个TreeNode,放在最下层节点的左侧或者右侧,并更新节点的指向。
  4. 根据本次新添加的新节点TreeNode,基于整个root树做红黑树的自适应平衡操作。
  5. 平衡后的返回值Root对象,放在此数组的指定索引位置。(参见链表转红黑树的流程的第6步)

扩容时红黑树的处理:

  1. 在扩容遍历到当前索引位置是红黑树时,以当前索引、新数组、旧数组长度作为方法参数,来对红黑树处理;
  2. 处理方式可参考链表的一部分(树是从链表转过去的,这个next关系,并没有断。给树新添加节点数据时,新数据即是left或者right指向,也是next指向):
  3. 会根据next进行遍历,每个节点的哈希值与就数组长度进行与运算,等于0的Node连成一个链表lohead,非0的连成另一个链表hihead。
  4. 这两个链表分别处理,若长度小于等于6,那么把对应链表从TreeNode转成Node后,lohead放在新书组的当前索引位置,hihead链表放在当前索引+旧数组长度之和的索引位置。形成了在新数组的高位和低位。
  5. 若长度大于6,也形成高低位,但是如果另一方的链表存在,那么就对高低位上的链表专场树格式。

HashMap根据key获取value:

  1. 计算key的哈希值,并计算出数据索引位置,如果此位置没有数据,就返回null
  2. 如果此位置有对象,检查此位置对象的哈希值与输入的key的哈希值是否一样,key是否相等,都满足时就返回此位置Node的value;
  3. 如果此位置的Node不是TreeNode,就遍历Node链表,去匹配哈希值相等,key也相等的Node,返回它的value;
  4. 如果此位置的Node是TreeNode,就从TreeNode下去遍历匹配哈希值相等,key也相等的Node,返回它的value;
  5. 如果一直都没找到,返回null;
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:

  1. 通过待移除的key和它的哈希值,先从map里找到能匹配到的Node,哈希值相等,key也相等。
  2. 非TreeNode,对这个Node,他的prev指向和next指向变更,数组索引位置是null了,链表里因为指向移除,这个Node就背移除链表了。
  3. 对于TreeNode,也是要移除它的prev和next指向,并将原来背指向的两个引用进行关联。强制确定此索引位置的 Node的root节点。
  4. 如果root节点没有左或者右节点,说明此树还比较小,将他转成链表后,方法结束。
  5. 对查询到符合key的Node的左节点或右节点的引用进行处理,更改他们的指向节点。对移除此Node后,树的剩余节点进行平衡处理。
  6. 对平衡完毕的树重新放回数组位置。

清空HashMap

将数组里每个索引位置都设置为null

LinkedHashMap

继承了HashMap类,整个类内部方法只保留了属于LinkedHashMap特质应用的方法。同时,该类内部也保留了部分方法,供我们自定义继承该类后去实现相关逻辑。

对于先后不同顺序添加的数据,LinkedHashMap定义了内部类,来记录这些key-value的顺序指向,链表方式,before、after记录链表节点的前后,同时对链表的头尾节点,进行单独的记录。

/**
 * HashMap.Node subclass for normal LinkedHashMap entries.
 */
static class Entry<K,V> extends HashMap.Node<K,V> {
    Entry<K,V> before, after;
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}

/**
 * The head (eldest) of the doubly linked list.
 */
transient LinkedHashMap.Entry<K,V> head;

/**
 * The tail (youngest) of the doubly linked list.
 */
transient LinkedHashMap.Entry<K,V> tail;

同时LinkedHashMap有个独有属性,accessOder,在构造方法时进行指定,默认false,选择在执行了某操作后,是否对链表节点进行排序。

/**
 * The iteration ordering method for this linked hash map: <tt>true</tt>
 * for access-order, <tt>false</tt> for insertion-order.
 *
 * @serial
 */
final boolean accessOrder;

添加数据

因为是继承了HashMap方法,所以它的数据添加时的处理流程,用的就是HashMap类里的方法。HashMap方法里定义的几个方法,在LinkedHashMap里有具体的实现。

  1. 在对数组创建Node对象时,用的是LinkedHashMap里的内部类创建的,同时对LinkedHashMap里的顺序链表进行数据设置,新数据一直放在最尾部。
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
    LinkedHashMap.Entry<K,V> p =
        new LinkedHashMap.Entry<K,V>(hash, key, value, e);
    linkNodeLast(p);
    return p;
}
TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
    TreeNode<K,V> p = new TreeNode<K,V>(hash, key, value, next);
    linkNodeLast(p);
    return p;
}

// link at the end of list
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
    LinkedHashMap.Entry<K,V> last = tail;
    tail = p;
    if (last == null)
        head = p;
    else {
        p.before = last;
        last.after = p;
    }
}
  1. 添加完新数据后,如果是做了数据覆盖操作,对LinkeHashMap执行afterNodeAccess方法,该方法的作用是把本次被覆盖的数据,组成节点放在顺序的尾部。
  2. 如果确实是添加了新数据,执行afterNodeInsetion操作。符合条件,会根据它会把LinkedHashMap里记录顺序的head位置数据移除。此时的符合条件,可通过我们继承LinkedHashMap类,实现它的removeEldestEntry方法,保证返回true,才会执行。
  3. 移除一个节点,就要调整下head和tail所引用的对象。
void afterNodeRemoval(Node<K,V> e) { // unlink
    LinkedHashMap.Entry<K,V> p =
        (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
    p.before = p.after = null;
    if (b == null)
        head = a;
    else
        b.after = a;
    if (a == null)
        tail = b;
    else
        a.before = b;
}

获取数据:

  1. 同样是通过HashMap的方法获取到目标结果,如果为null,直接返回。
  2. 不为null,且accessOrder为true,就执行afterNodeAccess方法,将本次查询到的Node对象放在排序的尾部。

移除数据:

调用HashMap的移除数据的方法

清空数据:

  1. 调用HashMap的清空数据方法;
  2. head和tail都设置为null

LinkedHashMap小结:是对HashMap的扩展类,但是同时我们也可以对它进行扩展;它对Map里的数据进行了排序处理,最近被操作的数据排到最热的位置,要求accessOrder设置为true,为false就是原hashmap的排序形式。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值