JavaSE学习笔记(13.Java之集合Map)

1. Map集合

Map集合提供的是一种key-value键值对元素的存储容器,key值不允许重复,重复的key值会导致元素覆盖!

Map接口提供的能力:

public interface Map<K,V> {

    /*判断map容器是否为空*/
    boolean isEmpty();

    /*判断key是否存在map中*/
    boolean containsKey(Object key);

    /*判断value是否存在map中*/
    boolean containsValue(Object value);
 
    /*通过key获取value*/
    V get(Object key);

    /*将key-value键值对更新到map中*/
    V put(K key, V value);

    /*将对应key的键值对从map中移除*/  
    V remove(Object key);

    /*将Map m的内容全部更新到当前map中去*/
    void putAll(Map<? extends K, ? extends V> m);

    /*清空map*/
    void clear();
    
    /*获取当前map中所有key值的set集合*/   
    Set<K> keySet();

    /*获取当前map中所有value值的集合,可以重复*/
    Collection<V> values();

    /*获取当前map中entry的set集合*/
    Set<Map.Entry<K, V>> entrySet();

    /*判断两个map集合是否相等*/
    boolean equals(Object o);

    /*返回map集合的hashCode*/
    int hashCode();
  
    /*Java8,Map接口中新增的default方法*/
    /*默认值为defaultValue的get方法*/
    default V getOrDefault(Object key, V defaultValue) {
        V v;
        return (((v = get(key)) != null) || containsKey(key))
            ? v
            : defaultValue;
    }
   
    /*指定action函数式接口的foreach*/
    default void forEach(BiConsumer<? super K, ? super V> action) {
        Objects.requireNonNull(action);
        for (Map.Entry<K, V> entry : entrySet()) {
            K k;
            V v;
            try {
                k = entry.getKey();
                v = entry.getValue();
            } catch(IllegalStateException ise) {
                // this usually means the entry is no longer in the map.
                throw new ConcurrentModificationException(ise);
            }
            action.accept(k, v);
        }
    }
    
    /*通过函数式接口function获取需要替换到Map中value值*/   
    default void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
        Objects.requireNonNull(function);
        for (Map.Entry<K, V> entry : entrySet()) {
            K k;
            V v;
            try {
                k = entry.getKey();
                v = entry.getValue();
            } catch(IllegalStateException ise) {
                // this usually means the entry is no longer in the map.
                throw new ConcurrentModificationException(ise);
            }

            // ise thrown from function is not a cme.
            v = function.apply(k, v);

            try {
                entry.setValue(v);
            } catch(IllegalStateException ise) {
                // this usually means the entry is no longer in the map.
                throw new ConcurrentModificationException(ise);
            }
        }
    }

    /*key值无效或者value为null,put成功*/
    default V putIfAbsent(K key, V value) {
        V v = get(key);
        if (v == null) {
            v = put(key, value);
        }

        return v;
    }

    /*key,value都匹配删除成功,否则进行remove行为*/
    default boolean remove(Object key, Object value) {
        Object curValue = get(key);
        if (!Objects.equals(curValue, value) ||
            (curValue == null && !containsKey(key))) {
            return false;
        }
        remove(key);
        return true;
    }

    /*key,value都匹配成功的时候,进行value值的替换*/
    default boolean replace(K key, V oldValue, V newValue) {
        Object curValue = get(key);
        if (!Objects.equals(curValue, oldValue) ||
            (curValue == null && !containsKey(key))) {
            return false;
        }
        put(key, newValue);
        return true;
    }

    /*key值有效的时候,进行替换,如果是新增key值,不会新建键值对*/
    default V replace(K key, V value) {
        V curValue;
        if (((curValue = get(key)) != null) || containsKey(key)) {
            curValue = put(key, value);
        }
        return curValue;
    }

    /*见compute*/
    default V computeIfAbsent(K key,
            Function<? super K, ? extends V> mappingFunction) {
        Objects.requireNonNull(mappingFunction);
        V v;
        if ((v = get(key)) == null) {
            V newValue;
            if ((newValue = mappingFunction.apply(key)) != null) {
                put(key, newValue);
                return newValue;
            }
        }

        return v;
    }

    /*见compute*/
    default V computeIfPresent(K key,
            BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
        Objects.requireNonNull(remappingFunction);
        V oldValue;
        if ((oldValue = get(key)) != null) {
            V newValue = remappingFunction.apply(key, oldValue);
            if (newValue != null) {
                put(key, newValue);
                return newValue;
            } else {
                remove(key);
                return null;
            }
        } else {
            return null;
        }
    }

    /*通过remappingFunction函数式接口对value进行二次计算,然后再进一步操作键值对*/
    default V compute(K key,
            BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
        Objects.requireNonNull(remappingFunction);
        V oldValue = get(key);

        V newValue = remappingFunction.apply(key, oldValue);
        if (newValue == null) {
            // delete mapping
            if (oldValue != null || containsKey(key)) {
                // something to remove
                remove(key);
                return null;
            } else {
                // nothing to do. Leave things as they were.
                return null;
            }
        } else {
            // add or replace old mapping
            put(key, newValue);
            return newValue;
        }
    }
    
    /*通过remappingFunction函数式接口,对value进行二次计算,再put*/    
    default V merge(K key, V value,
            BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
        Objects.requireNonNull(remappingFunction);
        Objects.requireNonNull(value);
        V oldValue = get(key);
        V newValue = (oldValue == null) ? value :
                   remappingFunction.apply(oldValue, value);
        if(newValue == null) {
            remove(key);
        } else {
            put(key, newValue);
        }
        return newValue;
    }
}

Java8在Map接口中新增的default方法:

Map接口的遍历:

public class MapTest
{
    public static void main(String args[])
    {
        Map<String,String> mp = new HashMap<>();
        mp.put("key1","value1");
        mp.put("key2","value2");
        mp.put("key3","value3");

        /*通过Map的keySet遍历*/
        for (String s : mp.keySet())
        {
            System.out.println("key:" + s + " value:" + mp.get(s));
        }

        /*通过Map的values遍历*/
        for (String s : mp.values())
        {
            System.out.println("value:" + s);
        }

        /*通过Map的entrySet遍历*/
        for (Map.Entry<String,String> entry : mp.entrySet())
        {
            System.out.println("key:" + entry.getKey() + " value:" + entry.getValue());
        }

        /*通过Map接口的forEach默认方法*/
        mp.forEach((key,value)->{
            System.out.println("key:" + key + " value:" +value);
        });
    }
}

2. HashMap集合:

2.1 HashMap集合简述:

  • HashMap是继承自AbstractMap抽象类,AbstractMap抽象类继承自Map接口;
  • HashMap是线程不安全集合;
  • HashMap可以接收null元素,key值,value值均可以为null;
  • HashMap是一个无序的集合;

 

2.2 HashMap的实现原理:

2.2.1 HashMap的原则:

一个key对应一个唯一的value;当key重复的时候,覆盖原有的value;HashMap的key的唯一性取决于HashCode值相等并且equals返回true!

2.2.2 HashMap的默认配置:

    /*Map容量,默认值为16,会被自动转换为最近的2的N次幂*/
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

    /*Map容器最大容量*/
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /*当达到容量的75%的时候,启动容器扩容,一次扩一倍*/
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /*Java8后,link长度达到8的时候,满足链表转红黑树条件1*/
    static final int TREEIFY_THRESHOLD = 8;

    /*Java8后,link长度从8降到6的时候,满足红黑树转换链表条件*/
    static final int UNTREEIFY_THRESHOLD = 6;

    /*Java8后,当容器达到64的时候,满足链表转红黑树条件2*/
    static final int MIN_TREEIFY_CAPACITY = 64;

2.2.3 HashMap的存储形式:

  • Java8以前,HashMap的存储结构是哈希+链表将元素key值的hashcode转换为哈希桶的索引;当未发生哈希碰撞的时候,元素直接入桶,当发生哈希碰撞的时候(即哈希桶索引相同、hashcode可能相同,也可能不相同),冲突元素遍历哈希桶中的链表,如果发现hashcode相等且equals相等的元素直接覆盖,未发现hashcode相等同时equals也相等的元素,将元素插入哈希桶中的链表!查找元素的时间复杂度为O(1)+O(n),如果哈希碰撞严重的情况时间复杂度就接近O(n)了,性能不优!所以有了后面的演进!
  • Java8以后,HashMap的存储结构是哈希+链表&红黑树,入桶规则和Java8以前相同,但是当Map容器达到64,哈希桶中链表元素超过8的时候,会自动将链表转换为红黑树,这样当哈希碰撞严重的时候,查找元素的时间复杂度就接近O(nlogn),比O(n)更优!

Ps:注意HashMap中的哈希碰撞有两种形式:

  • 一种是hashcode不相等,但是转换到哈希桶索引的时候,索引相等!这种碰撞由于hashcode不相同,红黑树是接近对称的,查找元素的复杂度接近O(nlogn)!
  • 另一种是hashcode相等时候发生的碰撞,这种碰撞会导致红黑树在遍历相等hashcode的时候更接近一个链表的遍历,因为相等hashcode的元素会一直在左树,这样查找元素的时间复杂度又接近O(n)了!但是可以让key值支持Comparable接口,支持相同hashcode但是equals不相等的场景的比较,这样在hashcode相等equals不相等的时候,HashMap会自动根据key对象提供的Comparable能力确认节点在红黑树的左边还是右边!这样查找元素的时间复杂度又提升到了O(nlogn)了!

2.2.4 hashcode()方法和equals()方法重写的注意事项:

  • HashMap实现通过key值对象的hashcode和equals来确认key值的唯一性,两者均相等的时候,确认为一个唯一key!
  • hashcode相等,一定会发生哈希碰撞;hashcode不相等,也可能会发生哈希碰撞,只是会降低碰撞的概率!碰撞越多,查询时,性能越差,所以不同的key值,最好使用不同的hashcode,来降低碰撞概率!
  • 当业务形态中,认为两个key值对象equals相等就是相同key的时候;建议重写equals()的同时一定要同步重写hashcode(),并且最好equals不相等,hashcode也不相等;equals相等,hashcode也相等,这样既满足设计也能保证性能最优!

简单分析原因(前提条件:当业务形态中,认为两个key值对象equals相等就是相同key的时候):

  1. equals相等,hashcode相等:由于equals、hashcode都相等,HashMap会用后来的key元素覆盖,原有的key元素!(元素位置:相同哈希桶的相同hashcode位置)
  2. equals相等,hashcode不相等:业务侧认为equals相等的,key就是相同的;但是由于hashcode不相等,HashMap会认为是两个key,分别都存放在容器里面,不符合设计预期!(元素位置:在不同的哈希桶或者相同哈希桶的不同hashcode位置)
  3. equals不相等,hashcode相等:由于hashcode相等,equals不相等,在发生哈希碰撞后,哈希桶中的红黑树会存在相同hashcode不同equals的节点;再获取数据的时候,需要遍历这些相同hashcode不同equals的节点,由于在key不支持Comparable接口的时候,左树右树的选择是通过hashcode判断的,导致相同hashcode会一直查找左树;如果这样的节点数目增多,红黑树的查找复杂度就接近链表查找的复杂度O(n)了,导致查询效率降低!(元素位置,在相同哈希桶的不同hashcode位置)
  4. equals不相等,hashcode不相等:由于hashcode不相等,哈希桶中的红黑树元素可以通过hashcode直接区分,不存在性能降低的问题,推荐使用!(元素位置,在不同的哈希桶或者相同哈希桶的不同hashcode位置)

2.2.5 HashMap扩容问题:

由于这里不想对HashMap的resize()进行展开描述,仅仅记录下Java8以前和Java8以后的实现差异;

  • Java8以前,在数据转移的时候,使用头插法,同时并发调用put()方法触发扩容,可能会造成逆序环形链表的问题,造成死链!
  • Java8以后,将头插法改为了尾插法,不会再出现逆序环形链表的问题,不会造成死链!

2.2.6 key值的不可变性:

HashMap中的元素位置主要是依赖于key值对象的hashcode和equals进行判断选择的,所有针对key值对象的选择,尽可能选择不可变对象;如果选择可变对象,必须保证在元素进入HashMap后,对象不再改变!因为如果key值对象改变导致hashcode和equals改变了,就会造成通过key值无法获取到正确元素的情况!

2.2.7 HashMap线程安全性问题讨论:

HashMap是个线程不安全的集合,多线程同时操作可能会造成数据异常;如链表与红黑树相互转换的时候并发、HashMap扩容的时候并发、链表插入读取的时候并发、红黑树插入读取的时候并发等场景;

如果想让HashMap支持线程安全,可以使用Collections.synchronizedMap(HashMap)进行包装!

2.2.8 HashXXX集合中contains()方法、equals()方法的特殊性:

由于HashXXX这类集合的特殊性:通过使用hashcode确定元素位置的方式来提高元素查询的效率!导致key相等的判断不仅仅通过equals方法判断,同时需要hashcode相等!这样的话HashSetXXX的contains()、HashMapXXX的containsKey()、HashXXX的equals()等涉及到key值判断的方法,都不再像其他集合那样仅仅判断eqauls就可以了,还需要同步判断hashcode!

2.2.9 hashcode重写建议:

原则:equals相等的hashcode必须相等,equals不相等的hashcode尽可能不相等!

  1. 将对象中每个有意义的变量计算出一个int类型的hashCode值
  2. 对每个hashCode值乘上一个不同的质数,最后求和

Ps:由于这里针对HashMap讨论比较简单,并未深入源码层面,推荐两篇比较详细的博文<HashMap看这篇就够了>   <一文搞定HashMap的实现原理和面试>

 

3. LinkedHashMap集合:

3.1 LinkedHashMap集合简述:

LinkedHashMap是HashMap的一个子类,是一个有序的HashMap(可以根据插入顺序,遍历Map);LinkedHashMap通过重写了HashMap中的newNode()方法,每次调用put插入数据的时候,都会同时将插入顺序记录到一个链表中;当需要遍历Map的时候,通过链表中的顺序进行遍历,来实现Map的有序性!

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

 

4. Hashtable集合:

4.1 Hashtable集合简述:

  • Hashtable继承自Map接口和Dictionary抽象类
  • Hashtable是线程安全集合;
  • Hashtable不可以接收null元素,key值,value值都不可以为null,否则会报空指针异常;
  • Hashtable是一个无序的集合;

4.2 Hashtable的实现:

  1. 存储形式:哈希+链表,细节实现与Java8以前的HashMap类似
  2. 同步方式:通过synchronized同步方法,实现线程安全

4.3 Hashtable与HashMap比较:

  • HashMap线程不安全,Hashtable线程安全
  • HashMap可以接收null元素,Hashtable不可以接收null元素
  • HashMap中引入红黑树提升查询性能,Hashtable依然使用链表存储哈希桶中的数据
  • HashMap与Hashtable都是无序的Map集合

Ps:由此可以看出,HashMap更加优选于Hashtable(Hashtable的查询效率很低、同步效率也很低、目前处于半废弃状态),如果需要线程安全的HashMap,可以使用Collections.synchronizedMap()进行包装或者使用ConcurrentHashMap集合!

 

5. TreeMap集合:

5.1 TreeMap集合简述:

  • TreeMap继承自Map接口和SortedMap抽象类
  • TreeMap是线程不安全集合;
  • TreeMap的key值不可以为null,value值可以为null;
  • TreeMap是一个有序的集合,顺序为key元素排列升序;

5.2 TreeMap的实现:

  • TreeMap不同于XXXHashMap,不是通过Hash桶实现的Key值的存在;TreeMap是一个有序Map,存储结构为红黑树;
  • TreeMap元素红黑树的确认规则:如果TreeMap选择自然排序(即构造函数中未设置Comparator对象),则通过每个key元素的compareTo方法进行元素大小关系判断,进而确认元素位置,所以在自然排序的TreeMap每个key元素必须是Comparable接口的实现类!如果TreeMap选择定制排序,则可以通过自己对Comparator的实现,来决定两个key元素判断标准,如果不再通过key元素的compareTo方法进行判断,key元素可以不再是Comparable的实现类!
  • TreeMap的key元素相等判断规则:仅仅根据元素的compareTo方法或者Comparator实现来判断,不使用equals进行判断;但是为了保证逻辑的一致性,建议compareTo方法或者Comparator实现逻辑与equals逻辑一致!当compareTo方法返回0的时候,判断为两个key元素相等,与HashMap不同的是,此时仅仅更新value的内容,key元素不再刷新!
public class TreeMapTest
{
	public static void main(String args[])
	{
		/*自然排序*/
		TreeMap<TmKey,String> tmap = new TreeMap<>();
		TmKey key1 = new TmKey(100);
		TmKey key2 = new TmKey(200);
		TmKey key3 = new TmKey(300);
		
		tmap.put(key2, "key2");
		tmap.put(key1, "key1");
		tmap.put(key3, "key3");
		System.out.println(tmap.size());

		for (Map.Entry<TmKey, String> entry : tmap.entrySet())
		{
			System.out.println(entry.getKey().num);
			System.out.println(entry.getValue());
		}
				
		
	}
}

class TmKey implements Comparable<Object>
{
	public int num;
	
	public TmKey(int num)
	{
		this.num = num;
	}
	
	@Override
	public int compareTo(Object obj) {
		
		return 0;

	}
	
	@Override
	public boolean equals(Object obj) {
		
		return true;
    }
	
}

输出:

Ps:Comparable接口中obj1.compareTo(obj2)方法,返回正整数表示obj1大于obj2;返回负整数表示obj1小于obj2;返回0表示obj1和obj2相等!

5.3 TreeMap的使用:

public class TreeMapTest
{
	public static void main(String args[])
	{
		/*自然排序*/
		TreeMap<TmKey,String> tmap = new TreeMap<>();
		TmKey key1 = new TmKey(100);
		TmKey key2 = new TmKey(200);
		TmKey key3 = new TmKey(300);
		
		tmap.put(key2, null);
		tmap.put(key1, null);
		tmap.put(key3, null);
		System.out.println(tmap.size());

		for (Map.Entry<TmKey, String> entry : tmap.entrySet())
		{
			System.out.println(entry.getKey().num);
		}
		
		/*定制排序*/
		TreeMap<TmKey,String> tmap2 = new TreeMap<>((obj1,obj2)->{
			return obj1.num > obj2.num ? 1 : obj1.num < obj2.num ? -1 : 0;
		});
		
		tmap2.put(key2, null);
		tmap2.put(key1, null);
		tmap2.put(key3, null);
		System.out.println(tmap2.size());

		for (Map.Entry<TmKey, String> entry : tmap2.entrySet())
		{
			System.out.println(entry.getKey().num);
		}
	}
}

class TmKey implements Comparable<Object>
{
	public int num;
	
	public TmKey(int num)
	{
		this.num = num;
	}
	
	@Override
	public int compareTo(Object obj) {
		
		TmKey m = (TmKey)obj;
		
		return this.num > m.num ? 1 : this.num < m.num ? -1 : 0;

	}
	
	public boolean equals(Object obj) {
		
		if (obj == this)
		{
			return true;
		}
		if (obj != null && obj.getClass() == TmKey.class)
		{
			TmKey m = (TmKey)obj;
			return m.num == this.num;
		}
		return false;
    }
	
}

Ps:通过上面TreeMap自然排序和定制排序的例子,可以说明TreeMap的遍历输出都是升序输出!

5.4 TreeMap提供的方法:

TreeMap类中除了实现了Map接口中的方法,由于它Key元素的有序性,还提供了大量的关于Entry和Key数值比较相关的方法,此处就不再展开赘述了!

常用方法:

 

6. WeakHashMap集合:

WeakHashMap与HashMap基本完全相同,只是HashMap的key保留对象实例的强引用;WeakHashMap的key保留对象的弱引用!当key被Jvm垃圾回收机制回收后,WeakHashMap会自动清理该key的键值对!

 

7. IdentityHashMap集合:

IdentityHashMap与HashMap基本完全相同,只是HashMap判断key相等的条件是hashcode相等同时equals相等;IdentityHashMap判断key相等的条件必须是两个key为同一个对象实例(key1==key2)!

 

8. EnumMap集合:

  • EnumMap继承自Map接口;
  • EnumMap是线程不安全集合;
  • EnumMap的key值不可以为null,value值可以为null;
  • EnumMap是一个有序的集合,集合顺序为key元素枚举值的数值升序;
  • EnumMap中的key元素只能是同一类枚举类型的枚举实例,EnumMap构造的时候已经确定了key值元素的枚举类型,如果类型不一致则抛出异常!
  • EnumMap是通过位运算进行存储,有着占用内存小,操作效率的优势;但是局限为key值只能是枚举类型!

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值