Map集合以及它的实现类们

Map集合

Map集合的概念理解

在现实生活中,我们经常会碰见一对一标识的例子:如身份证号与个人、IP地址与主机名等。这些一一对应的关系就是映射。在Java之中也能为我们的对象添加一个“一”的对应关系—Map<K,V>集合,位于java.util.Map<K,V>包下。里面的K代表该集合中唯一对应的那个身份,也叫做键。里面的V代表该集合中的元素值,也就是我们要存储的数据。

集合分为单列集合双列集合

  • 单列集合:主要指的就是Collection<E>系列的集合,里面存储的是一个一个的元素。这些数据结合在一起就成了一组数据,也就是集合。里面的每一个元素都是孤立存在的。
  • 双列集合:主要指的是Map<K,V>系列的集合,里面存储的是一对一对的键-值对,其中键不能重复,值可以重复。

Map常用方法

Map<K,V>是map系列集合的根接口,Map<K,V>接口中提供了一些简单操作map集合的方法让子类去重写。

添加操作

  • V put(K key,V value):添加一组映射关系,往集合中添加一组数据
  • void putAll(Map<? extends K,? extends V> m):将指定map集合m中的所有映射关系添加现有map集合中,为当前map集合添加另外一个m集合的所有元素。

删除操作

  • V remove(Object key):判断map集合中是否包含键key,如果包含将该key-value映射关系移除,并返回移除的key的value值
  • default boolean remove(Object key,Object value):判断当前map集合中是否包含映射关系key-value,如果包含返回true,并将该映射关系移除,如果不包含返回false。
  • void clear():清除该map中的所有映射关系

修改操作(了解即可)使用put()同样可以完成修改操作

  • defalut V replace(K key,V value):替换指定键key的值为value。返回值为key位置修改之前的值
  • default boolean replace(K key,V oleValue,V newValue):替换指定键key的oldValue为newValue。返回值为true或false

查询操作

  • V get(Object key):查询map集合中指定键key的值并返回。
  • boolean containsKey(Object key):判断map集合中是否含有指定键key。
  • boolean containsValue(Object value):判断map集合中是否含有指定值value。

元视图操作的方法(用于获取遍历集合的方法)

  • Set<K> keySet():返回该map集合中所有的键所组成的Set集合。(键不可重复)
  • Set<Map.Entry<K,V>> entrySet():返回map集合中所有的键值对组成的Set集合。Entry指Map中的内部接口
  • Collection<V> values():返回map集合中所有的value值组成的Collection集合。(值可以重复)

其他操作

  • int size():返回该map集合中的键值对数量。如果值为空或键为空仍会计算在内,如果值和键都为空不会计算在内。
  • boolean isEmpty():判断该map集合是否为空。如果值为空或键为空或都为true,结果都不为空。

代码测试:

public class MapTest {

    public static void main(String[] args) {
        Map<Integer,Object> map = new HashMap<>();

        map.put(1,"map");
        map.put(2,"hashMap");
        map.put(3,"treeMap");
        System.out.println("map = " + map); //map = {1=map, 2=hashMap, 3=treeMap}

        Map<Integer,Object> mapNew = new HashMap<>();
        mapNew.put(4,"linkedHashMap");
        mapNew.put(5,"hashTable");
        System.out.println("mapNew = " + mapNew); //{4=linkedHashMap, 5=hashTable}

        map.putAll(mapNew);
        System.out.println("map = " + map); //map = {1=map, 2=hashMap, 3=treeMap, 4=linkedHashMap, 5=hashTable}

        Object remove = map.remove(4);
        System.out.println("remove = " + remove); //remove = linkedHashMap
        System.out.println("map = " + map); //map = {1=map, 2=hashMap, 3=treeMap, 5=hashTable}

        boolean b1 = map.remove(6, "123");
        boolean b = map.remove(5, "hashTable");
        System.out.println("b1 = " + b1); //b1 = false
        System.out.println("b = " + b); //b = true
        System.out.println("map = " + map); //map = {1=map, 2=hashMap, 3=treeMap}

        mapNew.clear();
        System.out.println("mapNew = " + mapNew); //mapNew = {}

        //map.put(1,"hashTable");
        //System.out.println("map = " + map); //map = {1=hashTable, 2=hashMap, 3=treeMap}
        Object table = map.replace(1, "hashTable");
        System.out.println("table = " + table); //table = map
        System.out.println("map = " + map); //map = {1=hashTable, 2=hashMap, 3=treeMap}

        boolean replace = map.replace(1, "hashTable", "map");
        System.out.println("replace = " + replace); //replace = true
        System.out.println("map = " + map); //map = {1=map, 2=hashMap, 3=treeMap}

        Object o = map.get(1);
        System.out.println("o = " + o); //o = map
        boolean containsKey = map.containsKey(3);
        System.out.println("containsKey = " + containsKey); //containsKey = true
        boolean hashMap = map.containsValue("hashMap");
        System.out.println("hashMap = " + hashMap); //hashMap = true

        Set<Integer> keySet = map.keySet();
        for (Integer integer : keySet) { // 1 2 3
            System.out.print(" " + integer);
        }
        System.out.println();
        Collection<Object> values = map.values();
        for (Object value : values) { // map hashMap treeMap
            System.out.print(" " + value);
        }
        System.out.println();
        Set<Map.Entry<Integer, Object>> entrySet = map.entrySet();
        for (Map.Entry<Integer, Object> entry : entrySet) { // 键1的值:map 键2的值:hashMap 键3的值:treeMap
            Integer key = entry.getKey();
            Object value = entry.getValue();
            System.out.print("键"+key+"的值:"+value+" ");
        }
        System.out.println();

        System.out.println("map = " + map); //map = {1=map, 2=hashMap, 3=treeMap}
        map.put(null,"null"); //4
        map.put(7,null); //5
        map.put(null,null); //5
        int size = map.size();
        System.out.println("size = " + size); //size = 5
        boolean empty = map.isEmpty(); //false
        boolean newEmpty = mapNew.isEmpty(); //true
        //mapNew.put(null,"null"); //false
        //newEmpty = mapNew.isEmpty();
        //mapNew.put(9,null); //false
        //newEmpty = mapNew.isEmpty();
        mapNew.put(null,null); //false
        newEmpty = mapNew.isEmpty();
        System.out.println("empty = " + empty); //empty = false
        System.out.println("newEmpty = " + newEmpty); //newEmpty = false
    }

}

结果演示:

Map集合方法演示
注意:
如果使用put方法时,如果是第一次添加该元素,即原来map集合中没有键为添加元素的键,也就表示没有这个键对应的值,返回null,并把新的元素放到map集合中。

如果map集合中含有该键,则会先将这个键对应的value值返回,再修改该键的值为新的值。

Map集合遍历方式

在单列集合Collection中可以通过Iterator对象进行遍历或增强for循环进行遍历。但是Map集合中没有实现获取iterator()的方法,所以不能使用获取迭代器对象的方法进行遍历,Map集合也没有继承java.lang.Iterable<T>接口,所以也不能使用增强for循环进行遍历。

Map集合可以借助Collection集合进行遍历。简单的遍历主要分为以下三种:

  • 单独遍历所有的key:使用keySet()方法
  • 单独遍历所有value:使用values()方法
  • 成对的遍历Map系列的集合中的每一对Map.Entry子接口的实现类对象:使用entrySet()方法。

Map.Entry接口
代码测试:

public class GetMapElementTest {

    public static void main(String[] args) {
        Map<Integer,Object> map = new HashMap<>();
        map.put(1001,"map");
        map.put(1002,"hashMap");
        map.put(1003,"treeMap");
        System.out.println("map = " + map); //map = {1001=map, 1002=hashMap, 1003=treeMap}

        Set<Integer> keySet = map.keySet();
        for (Integer integer : keySet) { //键:1001,值:map  键:1002,值:hashMap  键:1003,值:treeMap
            System.out.println("键:"+integer+",值:"+map.get(integer));
        }
        System.out.println("=======================================");
        Collection<Object> values = map.values();
        for (Object value : values) { //map hashMap treeMap 
            System.out.print(value+" ");
        }
        System.out.println();
        System.out.println("==================================================");
        Set<Map.Entry<Integer, Object>> entrySet = map.entrySet();
        for (Map.Entry<Integer, Object> entry : entrySet) { //键:1001,值:map  键:1002,值:hashMap  键:1003,值:treeMap
            Integer key = entry.getKey();
            Object value = entry.getValue();
            System.out.println("键:"+key+",值:"+value);
        }
    }
}

结果演示:

map迭代结果演示

Map集合的实现类

Map接口是双列集合的父接口,而实现了Map接口的实现类也常常被叫做map集合。Map接口的常用的实现类有:HashMapLinkedHashMapTreeMapProperties等。其中使用最多的是HashMap

HashMap集合

HashMap的底层实现与扩容机制:
HashMap在JDK1.8之前:底层实现是数组+链表,扩容机制是当table中元素的个数已经达到阈值(table.length*0.75)时并且新添加[index]桶已经是非空,那么table.length需要扩容为2倍。

HashMap在JDK1.8之后:底层实现是数组+链表/红黑树,扩容机制(1)是当table中元素的个数已经达到阈值(table.length*0.75)时并且新添加[index]桶已经是非空,那么table需要扩容为2倍。(2)当添加到[index]下时,发现[index]下的链表结点个数已经达到8个,而table的长度未达到64,此时table.length也会扩容为2倍

HashMap构造方法:
  • ​ HashMap();
  • ​ HashMap(int initialCapacity);//指定初始化容量
HashMap和Hashtable的区别

(1)线程安全性不同
HashMap 是线程不安全的,HashTable 是线程安全的,其中的方法是 Synchronize 的,在多线程并发的情况下,可以直接使用 HashTable,但是使用 HashMap 时必须自己增加同步处理。
(2)是否提供 contains 方法
HashMap 只有 containsValue 和 containsKey 方法;HashTable 有 contains、containsKey 和 containsValue 三个方法,其
中 contains 和 containsValue 方法功能相同。
(3)key 和 value 是否允许 null 值
Hashtable 中,key 和 value 都不允许出现 null 值。HashMap 中,null 可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为 null。
(4)数组初始化和扩容机制
HashTable 在不指定容量的情况下的默认容量为 11,而 HashMap 为 16,Hashtable 不要求底层数组的容量一定要为2 的整数次幂,而 HashMap 则要求一定为 2 的整数次幂。Hashtable 扩容时,将容量变为原来的 2 倍加 1,而 HashMap 扩容时,将容量变为原来的 2倍。

代码:

public static void main(String[] args) {
    HashMap<String,Double> map = new HashMap<>();
    map.put("张三", 10000.0);
    //key相同,新的value会覆盖原来的value
    //因为String重写了hashCode和equals方法
    map.put("张三", 12000.0);
    map.put("李四", 14000.0);
    //HashMap支持key和value为null值
    String name = null;
    Double salary = null;
    map.put(name, salary);

    Set<Entry<String, Double>> entrySet = map.entrySet();
    for (Entry<String, Double> entry : entrySet) {
        System.out.println(entry);
    }
}

LinkedHashMap

LinkedHashMap 是 HashMap 的子类。此实现与 HashMap 的不同之处在于,后者维护着一个运行于所有条目的双重链接列表。此链接列表定义了迭代顺序,该迭代顺序通常就是将键插入到映射中的顺序(插入顺序)。

public static void main(String[] args) {
		LinkedHashMap<String,Double> map = new LinkedHashMap<>();
		map.put("张三", 10000.0);
		//key相同,新的value会覆盖原来的value
		//因为String重写了hashCode和equals方法
		map.put("张三", 12000.0);
		map.put("李四", 14000.0);
		String name = null;
		Double salary = null;
		map.put(name, salary);
		
		Set<Entry<String, Double>> entrySet = map.entrySet();
		for (Entry<String, Double> entry : entrySet) {
			System.out.println(entry);
		}
	}

TreeMap

基于红黑树(Red-Black tree)的 NavigableMap 实现。该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法。

代码:

public class TestTreeMap {
	@Test
	public void defaultComparableTest() {
		TreeMap<String,Integer> map = new TreeMap<>();
		map.put("Jack", 11000);
		map.put("Alice", 12000);
		map.put("zhangsan", 13000);
		map.put("baitao", 14000);
		map.put("Lucy", 15000);
		
		//String实现了Comparable接口,默认按照Unicode编码值排序
		Set<Entry<String, Integer>> entrySet = map.entrySet();
		for (Entry<String, Integer> entry : entrySet) {
			System.out.println(entry);
		}
	}
	@Test
	public void selfComparatorTest() {
		//指定定制比较器Comparator,按照Unicode编码值排序,但是忽略大小写
		TreeMap<String,Integer> map = new TreeMap<>(new Comparator<String>() {

			@Override
			public int compare(String o1, String o2) {
				return o1.compareToIgnoreCase(o2);
			}
		});
		map.put("Jack", 11000);
		map.put("Alice", 12000);
		map.put("zhangsan", 13000);
		map.put("baitao", 14000);
		map.put("Lucy", 15000);
		
		Set<Entry<String, Integer>> entrySet = map.entrySet();
		for (Entry<String, Integer> entry : entrySet) {
			System.out.println(entry);
		}
	}
}

Set集合与Map集合的关系

Set集合的底层实际上都维护了一个Map集合,Set集合中不能重复的特点就是来源于Map集合的Key不能重复。HashSet的底层是一个HashMap,LinkedHashSet的底层是一个LinkedHashMap,TreeSet的底层就是一个TreeMap。

源码分析:

  • HashSet的源码分析:
public HashSet() {
        map = new HashMap<>();
    }

    public HashSet(Collection<? extends E> c) {
        map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
        addAll(c);
    }

    public HashSet(int initialCapacity, float loadFactor) {
        map = new HashMap<>(initialCapacity, loadFactor);
    }

    public HashSet(int initialCapacity) {
        map = new HashMap<>(initialCapacity);
    }

	//这个构造器是给子类LinkedHashSet调用的
    HashSet(int initialCapacity, float loadFactor, boolean dummy) {
        map = new LinkedHashMap<>(initialCapacity, loadFactor);
    }
  • LinkedHashSet的源码分析:
 public LinkedHashSet(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor, true);//调用HashSet的某个构造器
    }

    public LinkedHashSet(int initialCapacity) {
        super(initialCapacity, .75f, true);//调用HashSet的某个构造器
    }

    public LinkedHashSet() {
        super(16, .75f, true);
    }

    public LinkedHashSet(Collection<? extends E> c) {
        super(Math.max(2*c.size(), 11), .75f, true);//调用HashSet的某个构造器
        addAll(c);
    }
  • TreeSet的源码分析
public TreeSet() {
        this(new TreeMap<E,Object>());
    }

    public TreeSet(Comparator<? super E> comparator) {
        this(new TreeMap<>(comparator));
    }

    public TreeSet(Collection<? extends E> c) {
        this();
        addAll(c);
    }

    public TreeSet(SortedSet<E> s) {
        this(s.comparator());
        addAll(s);
    }

Set集合的存储分析:

private static final Object PRESENT = new Object();
public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}
public Iterator<E> iterator() {
    return map.keySet().iterator();
}

Map的表结构与HashMap底层分析

Hash表:

在1.8之前的Hash表的底层是一个动态数组+链表,在1.8之后Hash改成了动态数组+链表/红黑树存储数据

当存入一个数据时进行二次hash运算选择到数组中对应的槽位上,当第二次运算进入到相同的槽位,就会在该槽位上进行尾插法(1.7之前是头插法)形成链表的某一个节点,这样避免了hash冲突。而为什么在1.8之后加上了红黑树,主要是查询性能的提升,从原来的 O(n)到 O(logn)。使用红黑树来替代超过 8 个节点的链表, 通过 hash 碰撞,让 HashMap 不断产生碰撞,那么相同的 key 的位置的链表就会不断增长,当对这个 Hashmap 的相应位置进行查询的时候,就会循环遍历这个超级大的链表,性能就会下降,所以改用红黑树
Hash表结构红黑树

HashMap底层分析(重点)

HashMap底层分析图

一.创建对象后成员变量的变化
      // 默认的初始容量
      static final int DEFAULT_INITIAL_CAPACITY = 1 << 4
      // 底层数组最大容量
      static final int MAXIMUM_CAPACITY = 1 << 30;
      // 默认的加载因子/负载因子 扩容使用
      static final float DEFAULT_LOAD_FACTOR = 0.75f;
      // 树化阈值
      static final int TREEIFY_THRESHOLD = 8;
      // 反树化阈值
      static final int UNTREEIFY_THRESHOLD = 6;
      // 树化底层顺序表最小长度
      static final int MIN_TREEIFY_CAPACITY = 64;
      // 底层数组的类型
      transient Node<K,V>[] table;
      // 键值对数量
      transient int size;
      // 阈值 长度*负载因子
      int threshold;
      // 加载因子
      final float loadFactor;

       static class Node<K,V> implements Map.Entry<K,V> {
           final int hash;
           final K key;
           V value;
           Node<K,V> next;
       }

    第一次创建对象 将默认的负载因子的值 赋值给 成员变量
      public HashMap() {
            this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
        }

二.添加数据
   2.1 首次添加 key 为null
            给底层数组开辟空间 长度为16
            threshold=12
            直接将元素放到指定的位置
   2.2 指定位置第一次添加 key 不为null
            直接添加
   2.3 指定位置非第一次添加 hash相同 key 不相同
           七上八下:
           jdk7:后续添加元素在原有数据的上面(尾部)
           jdk8:后续添加元素在原有数据的下面(尾部)

   2.4 指定位置非第一次添加 hash相同 key 相同
         新的value 替换旧的value 并将旧的value返回

   2.5 扩容和树化
       扩容:size>threshold进行扩容
            数组长度*2
            阈值*2
       树化:
           1.节点数量>=8
           2.底层数组长度>=64
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

黑妖问路

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值