Map集合及源码分析

1. Map接口的基本介绍(jdk8为例)

  1. Map与Collection并列存在,用于保存具有映射关系的数据:key-value
  2. Map中的key和value可以是任何引用类型的数据,会封装到HashMap$Node对象中
  3. Map中的key不允许重复,原因和HashSet一样(如果出现一样的情况,后者会将前者覆盖
  4. Map中的value可以重复
  5. Map中的key可以为null,value也可以为null,但是key为null只能有一个,value为null可以有多个
  6. 常用String类作为Map的key
  7. key和value之间存在单向的一对一关系,即通过key总能找到对应的value
  8. Map的存放是无序的,和Set一样

2. Map源码分析

2.1 HashMap接口的特点

Map存放数据的key-value示意图,一对key-value是存放在一个Node中的。因为Node类实现了Entry接口,所以也说一对key-value就是一个Entry。
请添加图片描述
请添加图片描述

请添加图片描述

请添加图片描述

注意:很多地方说到,Map的key是存放在Set的实现类里面,value是存放在Collection的实现类里,这样的说法十分不严谨,实际上,真正的key-value是存放在Node节点里面的。Set和Collection中只是引用指向Node节点上。key-value为了方便遍历,还会在底层创建EntrySet集合,该集合存放的元素的类型是Entry,而一个Entry对象就包含了key-value。Entry对象的key存放的值是Node节点的key,Entry对象的value存放的值是Node节点的value。那么为什么要这么设计呢?因为便于方便遍历,Entry对象中有两个方法getKey(),getValue()方法。总结来说,就是把Node转为Entry(为什么Node能转为Entry呢,因为Node实现了Entry接口,所以Node的实例能赋值给Entry),然后将Entry放到EntrySet集合中。

请添加图片描述

请添加图片描述
Entry的getKey(),getValue()方法

public class Map_ {
    
    
    public static void main(String[] args) {
    
    
        //
        Map map = new HashMap();
        map.put(1,"zjh");
        map.put(2,"jxj");
        System.out.println(map.toString());

        Set set = map.entrySet();
        for (Object entry:set){
    
    
            Map.Entry me = (Map.Entry)entry;
            System.out.println(me.getKey());
            System.out.println(me.getValue());
        }
    }
}

结果

{
    
    1=zjh, 2=jxj}
1
zjh
2
jxj

重点:其实EntrySet集合中的数据是指向Node的,并不是复制Node的值来放到EntrySet中

public class Map_ {
    
    
    public static void main(String[] args) {
    
    
        //
        Map map = new HashMap();
        map.put(1,"zjh");
        map.put(2,"jxj");
        System.out.println(map.toString());

        Set set = map.entrySet();
        for (Object entry:set){
    
    
            Map.Entry me = (Map.Entry)entry;
            System.out.println(me.getKey());
            System.out.println(me.getValue());
        }
    }
}

Set set = map.entrySet();中打断点
请添加图片描述
由此可见,上述说法成立。

public class Map_ {
    
    
    public static void main(String[] args) {
    
    
        //
        Map map = new HashMap();
        map.put(1,"zjh");
        map.put(2,"jxj");
        Collection values = map.values();
        Set keySet = map.keySet();
    }
}

由此段代码可得知,map的values和keys存放的结构。(有兴趣可以深究源码)

2.2 HashMap底层机制及源码分析

请添加图片描述

  1. k-v是一个Node实现了Map.Entry,查看HashMap的源码可以看到
  2. jdk7的HashMap是数组+链表,jdk8的HashMap是数组+链表+红黑树
  3. HashMap底层维护了Node类型的数组table,默认为null
  4. 当创建对象时,将加载因子(loadfactor)初始化为0.75。加载因子应用于计算临界值,临界值 = 容量 * 加载因子
  5. 当添加key-value时,通过key的哈希值得到在table的索引位置,然后判断该索引位置是否有元素,如果没有元素则添加,如果有元素,则判断该元素的key是否和准备加入的key是否相同,如果相同,则替换value,如果不相同,则需要判断该索引处的Node节点是链表形式还是红黑树形式,并做出相应处理,如果添加时发现容量不够,则需要扩容。
  6. 第一次添加,则需要扩容的容量是16,临界值是12
  7. 以后再次扩容,则需要扩容table为原来的2倍,临界值为原来的2倍,即24,以此类推
  8. 在java8中,如果一条链表的元素个数超过TREEIFY_THRESHOLD(默认是8),并且table的大小 >= MIN_TREEIFY_CAPACITY(默认为64),就会进行红黑树化(并不是将所有的链表进行红黑树化)

2.2.1 HashMap源码分析

public class Map_ {
    
    
    public static void main(String[] args) {
    
    
        Map map = new HashMap();
        map.put("java",10);
        map.put("php",10);
        map.put("java",20);
        System.out.println("map->"+map.toString());
    }
}

对此代码进行debug分析
请添加图片描述
初始化HashMap,默认容量是16,加载因子为0.75
请添加图片描述
接下来是对基本数据类型的装箱,因为我们添加的value是10
请添加图片描述
接下来执行hash算法,计算key的hash值
请添加图片描述
之后获得hash值后进行putVal()方法,这是最核心的方法,为了一步步分析,将核心代码拷贝下来,不以图片的形式进行展示,方便做笔记

 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    
    
                   // 创建tab数组,和p节点以及辅助变量
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // 判断tab是否为空或者tab的长度是否为0,如果符合条件将执行resize方法,resize方法就是对tab进行扩容
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // 此时tab表以及初始化为16,临界值为12,加载因子为0.75
        // 通过hash得到索引位置,如果索引位置为空,则直接进入到此位置
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
    
    
        	// 进入到这一步就说明,得到的索引位置已经有Node节点了
        	// 定义e节点,注意此时的p节点已经是该索引位置上的第一个节点了
            Node<K,V> e; K k;
            // 判断p的hash值和即将存放的key的hash值是否相同,且判断key是否相同或者key不为空且内容相同,则将p赋值给e,即不添加
            // 注意,覆盖的操作在下面
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            // 如果当前节点是TreeNode类型的数据,执行putTreeVal方法(如果当前节点已经是红黑树了,就按照红黑树的方式进行处理)
            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);
                        // 判断条件,判断是否需要树化 即判断单个链表的长度是否大于等于7-1
                        if (binCount >= TREEIFY_THRESHOLD - 1) 
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 在循环的时候发现整个链表中有和待加入节点有相同的,就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;
        // threshold:临界值
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

2.3 HashTable底层机制及源码分析

  1. HashTable存放的元素是键值对,key-value
  2. HashTable的键值对都不能为null
  3. HashTable的使用方法基本和HashMap一样
  4. HashTable是线程安全的,HashMap是线程不安全的

2.3.1 HashTable源码分析

public class Table_ {
    
    
    public static void main(String[] args) {
    
    
        Hashtable hashtable = new Hashtable();
        hashtable.put(1,"zjh");
        hashtable.put(2,"jxj");
        hashtable.put(1,"wc");
        System.out.println(hashtable);
    }
}

请添加图片描述
hashTable的初始化大小为11,加载因子为0.75
请添加图片描述
从这里既可以看出table数组存放的是Entry,而不是Node。且value不能为空,否则会报NullPointerException。
上图的for循环中就是判断是否需要进行value的覆盖,如果不需要就进入到addEntry方法
请添加图片描述
rehash()方法是扩容方法
扩容机制:int newCapacity = (oldCapacity << 1) + 1;

  protected void rehash() {
    
    
  		// 将老的table的长度赋值给oldCapacity 
        int oldCapacity = table.length;
        Entry<?,?>[] oldMap = table;

        // overflow-conscious code
        // 获取新的容量,即老的容量向左移位再加1,即oldCapacity *2+1
        int newCapacity = (oldCapacity << 1) + 1;
        
        if (newCapacity - MAX_ARRAY_SIZE > 0) {
    
    
            if (oldCapacity == MAX_ARRAY_SIZE)
                // Keep running with MAX_ARRAY_SIZE buckets
                return;
            newCapacity = MAX_ARRAY_SIZE;
        }
        // 这里就是真正的扩容,下面就不继续深究
        Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];

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

        for (int i = oldCapacity ; i-- > 0 ;) {
    
    
            for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
    
    
                Entry<K,V> e = old;
                old = old.next;

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

2.4 Properties源码分析

  1. Properties类继承自HashTable类并且实现了Map接口,也是使用一种键值对的形式来保存数据。
  2. 他的使用特点与Hashtable类似
  3. Properties还可以用于从 xxx.properties文件中,加载数据到Properties类对象,进行读取并修改
  4. 说明:xxx.properties文件通常作为配置文件

2.4.1 源码分析

2.5 TreeSet的源码分析

当使用无参构造器创建TreeSet的时候,仍然是无序的

public class TreeSet_ {
    
    
    public static void main(String[] args) {
    
    
        TreeSet<Object> treeSet = new TreeSet<>();
        treeSet.add("jack");
        treeSet.add("tom");
        treeSet.add("a");
        treeSet.add("b");
        System.out.println(treeSet);
    }
}

请添加图片描述
如何使得TreeSet是有序的?使用TreeSet提供的构造器,传入一个比较器来实现有序

public class TreeSet_ {
    
    
    public static void main(String[] args) {
    
    

        TreeSet<Object> treeSet = new TreeSet<>(new Comparator<Object>() {
    
    
            @Override
            public int compare(Object o1, Object o2) {
    
    
                return ((String)o1).compareTo((String) o2);
            }
        });
        treeSet.add("jack");
        treeSet.add("tom");
        treeSet.add("a");
        treeSet.add("b");
        System.out.println(treeSet);
    }
}

请添加图片描述
源码分析

  1. TreeSet的底层是TreeMap
  2. 构造器里传入Comparable时,会将Comparable赋值给自己的熟悉comparable

add方法源码解析
请添加图片描述
进入到map的put方法
请添加图片描述

 public V put(K key, V value) {
    
    
        Entry<K,V> t = root;
        if (t == null) {
    
    
            compare(key, key); // type (and possibly null) check

            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        int cmp;
        Entry<K,V> parent;
        // split comparator and comparable paths
        // 我们的比较器赋值给cpr
        Comparator<? super K> cpr = comparator;
        if (cpr != null) {
    
    
            do {
    
    
                parent = t;
                // 这里进行了比较
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        else {
    
    
            if (key == null)
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
                Comparable<? super K> k = (Comparable<? super K>) key;
            do {
    
    
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        Entry<K,V> e = new Entry<>(key, value, parent);
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
    }

由上图可以看到将comparator赋值给了cpr,之后判断cpr是否为空,不为空则比较两个元素的key的值,因为set的底层时map,key才是真的值,value只是占位的常量

2.6 TreeMap源码分析

public class TreeMap_ {
    
    
    public static void main(String[] args) {
    
    
        TreeMap<Object, Object> treeMap = new TreeMap<>();
        treeMap.put("jack","杰克");
        treeMap.put("tom","汤姆");
        treeMap.put("kristina","克瑞斯提莫");
        treeMap.put("smith","史密斯");
    }
}

如果使用默认的构造器创建treemap
请添加图片描述
可以发现是无序的

TreeMap的构造器中可以传入Comparable

public class TreeMap_ {
    
    
    public static void main(String[] args) {
    
    
        TreeMap<Object, Object> treeMap = new TreeMap<>(new Comparator<Object>() {
    
    
            @Override
            public int compare(Object o1, Object o2) {
    
    
                return ((String)o1).compareTo(((String)o2));
            }
        });
        treeMap.put("jack","杰克");
        treeMap.put("tom","汤姆");
        treeMap.put("kristina","克瑞斯提莫");
        treeMap.put("smith","史密斯");
        System.out.println(treeMap);
    }
}

请添加图片描述
源码解读

  1. 构造器,把传入的Comparator传给了TreeMap
  2. 第一次添加,把k-v封装到entry中,放入到root
Entry<K,V> t = root;
        if (t == null) {
    
    
            compare(key, key); // type (and possibly null) check

            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
  1. 其余的添加和上面的TreeSet解读大致相同,因为TreeSet的底层时TreeMap
Comparator<? super K> cpr = comparator;
        if (cpr != null) {
    
    
            do {
    
    
                parent = t;
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }

3. Map接口和常用方法

map的体系继承图
请添加图片描述

  1. put:添加key-value
  2. remove:根据key删除映射关系
  3. get:根据key获取value
  4. size:获取元素个数
  5. isEmpty:判断个数是否为0
  6. clear:清除map
  7. containsKey:查找key是否存在

3.1 Map接口的6大遍历方法

请添加图片描述
上述已经讲解过此图,Map的遍历只需要牢牢地记住此图即可。

  1. containsKey:查找key是否存在
  2. keySet:获取所有的key,返回的是set集合
  3. entrySet:获取所有的关系
  4. values:获取所有的value,返回的是Collections

3.1.1 通过map的keySet()方法

public class MapFor {
    
    
    public static void main(String[] args) {
    
    
        Map map = new HashMap();
        map.put(1,"zjh");
        map.put(2,"jxj");
        map.put(3,"zxw");
        map.put(4,"hjx");
        map.put(5,"zjy");

        // 先获取所有的key,再取出对应的value
        Set set = map.keySet();
        // 增强for循环
        for (Object key : set) {
    
    
            System.out.println(map.get(key));
        }
        // 迭代器
        Iterator iterator = set.iterator();
        while (iterator.hasNext()){
    
    
            System.out.println(map.get(iterator.next()));
        }
    }
}

通过map的keySet()方法来获取所有的key,然后一一取出数据。里面可以细分为通过增强for循环和迭代器来遍历keySet集合。

3.1.2 通过map的values()方法

  // 获取所有的value,在不关心key的前提下
        Collection collection = map.values();
        for (Object o : collection) {
    
    
            System.out.println(o);
        }

        Iterator iterator = collection.iterator();
        while (iterator.hasNext()){
    
    
            System.out.println(iterator.next());
        }

3.1.3 通过map的entrySet()方法

		Set entrySet = map.entrySet();
        for (Object o : entrySet) {
    
    
            Map.Entry entry = (Map.Entry) o;
            System.out.println(entry.getKey()+"--"+entry.getValue());
        }

        Iterator iterator = entrySet.iterator();
        while (iterator.hasNext()){
    
    
            Map.Entry entry = (Map.Entry) iterator.next();
            System.out.println(entry.getKey()+"--"+entry.getValue());
        }

3.2 HashMap小结

  1. Map接口的常用实现类:HashMap,HashTable,Properties
  2. HashMap是Map接口使用频率最高的实现类
  3. HashMap是以key-value的方式来存储数据
  4. key不能重复,但是value可以重复,key,value允许null值
  5. 添加相同的key,后者会把前者覆盖(可以打断点进行查阅)
  6. 与HashSet一样,输出的时候不能保证输出的顺序是添加的顺序,因为底层是以hash表来存储的(jdk8的hashMap底层:数组+链表+红黑树)
  7. HashMap没有实现同步,因此是线程不安全的

猜你喜欢

转载自blog.csdn.net/weixin_45690465/article/details/123133446