• Map接口常用实现类学习


    HashMap

    1.8的HashMap:数组加链表加红黑树结构

    最重要的内部类有2个:

    Node,实现了Map.Entry接口。有4个成员变量:int hash,K key,V value,Node next,有且只有一个4个参数的构造器:Node(int hash, K key, V value, Node<K,V> next)。

    TreeNode,继承了LinkedHashMap.Entry,而LinkedHashMap.Entry又继承了HashMap.Node。LinkedHashMap.Entry除了继承自HashMap.Node的4个成员变量外,还有2个成员变量:LinkedHashMap.Entry before,LinkedHashMap.Entry after。TreeNode除了继承自LinkedHashMap.Entry的6个成员变量外,还有5个成员变量:TreeNode parent,TreeNode left,TreeNode right,TreeNode prev,boolean red,也有且只有一个4个参数的构造器:TreeNode(int hash, K key, V val, Node<K,V> next),构造器参数同HashMap.Node的构造器参数一样。

    常用方法实现:

    put()方法实现:

    内部调用putVal()方法。putVal()方法第一个参数是hash(key),是根据key计算出的一个hash值,如果key为null,则hash值为0,否则根据key的hashCode值计算出来一个值。第二个参数是key,第三个参数是value,第四个参数是boolean类型变量,是否仅在key不存在时生效,写死false。第五个参数是boolean类型变量,是否驱逐,写死true。

    putVal方法首先判断当前Node数组是否为null,或者为空,如果是的话,就调用resize()方法扩容,扩容后容量为大于等于初始容量的最小的2次幂。之后,通过hash值找到在数组中的位置。索引值是(n - 1) & hash,其中n是数组的长度,即hash值与数组长度-1做位与运算,计算的结果小于等于n - 1。找到位置后,看此位置有没有元素,如果没有元素,则新建一个Node实例并放到这个位置上,注意该Node实例的next属性值为null。如果有元素,则先判断该元素的hash属性值与传进来的hash值是否相等以及该元素的key属性值是否==或者equals传进来的key(从这里可以看出,HashMap判断两个key是否一样,是依赖key的hashCode()方法与equals()方法的)。如果返回true,则说明   就拿新的value值替换老的value值。之后,再把modcount变量值加1。再把size变量值加1,再判断size是否大于threshold变量值,如果大于,则要调用resize()扩容。 

    我们常用的类,都重写了hashCode()和equals()方法,如7个原始类型对应的包装类、ArrayList/LinkedList(AbstractList重写了)、HashSet/LinkedHashSet/TreeSet(AbstractSet重写了)、HashMap/LinkedHashMap/TreeMap(AbstractMap重写了)、String、Date、BigDecimal。如果key是自定义类型,则在定义这个类时,必须要重写hashCode()与equals()方法。Set的元素的类型,也要求重写hashCode()方法和equals()方法。

    get()方法实现:

    参考另一篇文章:https://www.cnblogs.com/koushr/p/5873371.html

    remove()方法实现:

    内部调用removeNode()方法,第一个参数是key的hash值,第二个参数是key,其他参数不重要。首先根据hash值找到索引位置,如果索引位置上没有元素,则返回null。如果有元素,则判断这个元素的hash属性值是否和入参key的hash值一样,并且这个元素的key属性值是否==或者equals入参key,如果返回true,则说明。如果是TreeNode,则调用TreeNode的removeTreeNode()方法,。再把modCount值加1,size值减1。

    clear()方法实现:

    先把modCount值加1,然后如果Node数组不为null且长度大于0,则把size置为0,并遍历数组,把所有位置都赋值为null。

    size()方法实现:

    返回size变量的值。put()、remove()、putAll()、clear()方法都会维护size值,putxxx是加1,remove是减1,clear直接清0。

    containsKey()方法实现:

    内部调用getNode()方法,然后判断getNode()返回值是否为null。

    containsValue()方法实现:

    遍历数组以及链表,直到某一个Node实例的value属性值==或者equals入参value,如果找到就返回true,否则返回false。从这里可以看出,HashMap判断value相同的依据是两个value==或者equals返回为true,跟value的hashCode()没关系。

    entrySet()方法实现:

    HashMap有一个Set类型的entrySet变量,如果是第一次调用,会把entrySet变量初始化为一个新生成的EntrySet实例并返回。此后再调用的话,就直接返回这个EntrySet实例。EntrySet是HashMap的内部类,继承了AbstractSet,重写了Set接口的很多方法,如size()方法直接返回当前HashMap实例的size变量值。clear()方法内部是调用当前HashMap实例的clear()方法。iterator()方法会返回一个EntryIterator实例。EntryIterator继承了HashIterator,其next()方法内部调用HashIterator的nextNode()方法。从nextNode()方法的实现中可以看出在迭代时不能对HashMap进行增删操作的原因。nextNode()方法内部会比较当前modCount与创建EntryIterator实例时的modCount是否相同,如果不同,就会抛ConcurrentModificationException,而增删操作会使得modCount+1,所以在调用nextNode()方法前不能对HashMap实例进行增删操作,但是可以在nextNode()方法之后对HashMap实例进行增删操作,这时假如还没迭代完的话,就又会调用nextNode()方法,就会抛ConcurrentModificationException异常,所以只有在最后一次迭代的时候,对HashMap实例进行增删操作不会抛异常。

    示例:

        public static void main(String[] args) {
            Map<Integer, Integer> map = new HashMap<>();
            map.put(1, 2);
            map.put(2, 2);
            Set<Entry<Integer, Integer>> entrySet = map.entrySet();
            System.out.println(entrySet);
            Iterator<Map.Entry<Integer, Integer>> iterator = entrySet.iterator();
            int i = 0;
            while (iterator.hasNext()) {
                //            map.put(3, 3);
                System.out.println(iterator.next());
                i++;
                if (i == map.size()) {
                    map.put(4, 4);
                }
            }
            System.out.println(entrySet);
        }

    map.put(3, 3);之后调用iterator.next()会抛异常,map.put(4, 4);之后,会跳出迭代,所以不会抛异常。

    两次打印entrySet的结果不一样,这是为啥呢?

    打印entrySet,其实是打印entrySet调用toString()方法的返回值。EntrySet的toString()方法继承自AbstractCollection,

        public String toString() {
            Iterator<E> it = iterator();
            if (! it.hasNext())
                return "[]";
    
            StringBuilder sb = new StringBuilder();
            sb.append('[');
            for (;;) {
                E e = it.next();
                sb.append(e == this ? "(this Collection)" : e);
                if (! it.hasNext())
                    return sb.append(']').toString();
                sb.append(',').append(' ');
            }
        }

    toString()内部会先调用iterator()方法拿到迭代器,接着去遍历。

    什么情况下要扩容?有以下几种情况:

    1、size > threshold的情况。包括第一次putVal时数组初始化以及之后putVal时size > threshold的情况。threshold是容量*负载因子取整后的值。

    2、链表长度达到8时,每增加一个链表元素,就会扩容,直到数组长度大于等于64。数组长度达到64之后,就要把链表结构转为红黑树结构了。TreeNode上场。

    if (binCount >= TREEIFY_THRESHOLD - 1) {
        treeifyBin(tab, hash);
    }

    在treeBin()方法中会调用resize()方法。

    resize()方法中,老数组元素在新数组中如何放,也是很复杂的。以后再研究。

    LinkedHashMap

    1.8的LinkedHashMap:数组加双向链表加红黑树结构

    类似于HashMap中Node内部类的作用,LinkedHashMap有一个内部类Entry,继承了HashMap.Node。LinkedHashMap.Entry有2个额外的属性,Entry类型的before、Entry类型的after。每插入一个新的Entry实例时,都要维护下和上一次插入的Entry实例的先后顺序。当前Entry实例的before属性被赋值为上一个Entry实例,上一个Entry实例的after属性被赋值为当前Entry实例。

    TreeMap

    1.8的TreeMap:红黑树实现

    内部类Entry代表红黑树节点,有6个成员变量:K key,V value,Entry<K,V> left,Entry<K,V> right,Entry<K,V> parent,boolean color(初始值是true,黑色节点)。

    构造器:

    有4个构造器,无参构造器,参数为Comparator实例的构造器,参数为Map实例的构造器,参数为SortedMap实例的构造器。

    TreeMap内部是由红黑树实现的,见https://www.cnblogs.com/koushr/p/5873421.html

    TreeMap是依据Comparator实例排序的,可以在构造TreeMap实例的时候传进去。如果用入参是SortedMap实例构造,则Comparator实例是SortedMap实例对应的Comparator实例。如果用无参构造器或者非SortedMap类型的Map实例,则Comparator实例是null,这时要求键类型K必须实现Comparable接口,从而调用key的compareTo方法来比较。如果K没有实现Comparable接口,就会报类型转换异常 K cannot be cast to java.lang.Comparable。

    TreeMap第一个放进去的TreeMap.Entry实例是初始根节点,之后放进去的就会依据Comparator实例或者key的compareTo方法找到自己的初始位置,之后会检查是否还满足红黑树的5个特性,如果不满足的话,就会有节点变色、旋转等操作(见fixAfterInsertion方法),以满足红黑树的5个特性。

    需要指出的是,TreeMap的有序和LinkedHashMap的有序是两种截然不同的意思。LinkedHashMap的序指的是插入顺序,如果accessOrder值是默认值false的话,而TreeMap的序指的是自排序后的顺序,而非插入顺序。举个简单的例子,往new LinkedHashMap()和new TreeMap()中分别放入5个元素,key依次是1、4、3、2、5,则LinkedHashMap实例的KeySet实例的Iterator实例遍历时,key值依次是1、4、3、2、5,和插入顺序一致。TreeMap实例的KeySet实例的Iterator实例遍历时,key值依次是1、2、3、4、5,因为内部排序时采用的是Integer实例的int compareTo(Integer anotherInteger)方法,单纯比较key的大小。如果我们想自定义取出顺序时,可以传入自定义的Comparator实例。

  • 相关阅读:
    进度条2
    VW
    SET和MAP
    解构赋值、
    Symbol
    箭头函数
    正则的补充
    java 面向对象(三十五):泛型在继承上的体现
    java 面向对象(三十六):泛型五 通配符
    java IO流 (一) File类的使用
  • 原文地址:https://www.cnblogs.com/koushr/p/5873437.html
Copyright © 2020-2023  润新知