Java集合框架之Map接口

写在前面

这篇文章我接着来总结Java结合框架中的另一个接口:Map接口。
 Collection接口的特点是每次进行单个对象的保存,如果现在要进行两个对象的保存就只能用Map接口来实现,且这两个对象的关系是:key=value结构。这个结构最大的特点是可以通过key找到对应的value内容。
 在这里插入图片描述

  • Map接口定义:
    public interface Map<k,v>;
  • Map中定义的方法:
    public V put(k key,V value);向Map中追加数据
    public V get(Object key);根据key取得对应的value,如果没有返回null
    public Set<K> keySet();取得所有key信息、key不能重复
    public Collection<V> values();取得所有value信息,可以重复
    public Set<Map.Entry<k,v>> entrySet();将Map集合变为Set集合

Map本身是一个接口,要使用Map需要通过子类进行对象实例化

Collection保存数据的目的一般用于输出(Iterator),Map保存数据的目的是为了根据key查找,找不到返回null。

Set接口与Map接口的关系:

Set是Map的小马甲(Set内部就是使用Map来存储元素,将元素存储到内部Map的key)

Set下的两个常用子类:

  • HashSet:(HashMap的小马甲)
  • TreeSet:(TreeMap的小马甲)

1、HashMap子类(常用)

先来看Map的基本操作:

public class MapPractice {
    public static void main(String[] args) {
        Map<Integer, String> map = new HashMap<>();
        map.put(1, "Hello");
        map.put(1, "hello");
        map.put(3, "Java");
        map.put(2, "Nanfeng");
        System.out.println(map);
        //根据key取得value
        System.out.println(map.get(2));
        //查找不到返回null
        System.out.println(map.get(99));
    }
}

在这里插入图片描述
取得Map中所有key信息:

public class MapPractice {
    public static void main(String[] args) {
        Map<Integer, String> map = new HashMap<>();
        map.put(1, "Hello");
        map.put(1, "hello");
        map.put(3, "Java");
        map.put(2, "Nanfeng");
        Set<Integer> set = map.keySet();
        Iterator<Integer> iterator = set.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }
}

在这里插入图片描述
这部分的重点是对源码进行分析

1.1 HashMap内部实现

几个比较重要的属性:

  • final float loadFactor-负载因子(默认为0.75);
  • int threshold-容量 = table.length * loadFactor;
  • int TREEIFY_THRESHOLD = 8-默认树化阈值;
  • int UNTREEIFY_THRESHOLD = 6-默认解除树化阈值;

内部hash()与Object的hash()有什么区别?
观察源码可以发现,HashMap与ArrayList比较相似,也是在第一次添加元素的时候才初始化。

	public V put(K key, V value) {
    	return putVal(hash(key), key, value, false, true);
	}

上面的代码里,put()方法调用了一个内部的putVal()方法,先根据key值进行hash运算,再来看看内部hash()的具体实现:

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

上面这段代码表示,当key值为空的时候,把value放在下标为0的位置,也就是数组中第一个位置,当key不为空的时候,返回后边那一串代码的值,那么这一长串代码代表啥意思呢?下面我们来具体分析一下:
(h = key.hashCode()) ^ (h >>> 16)
这是一个异或运算,(h >>> 16)代表无符号右移16位,key.hashCode()是Object类提供的求哈希值的方法。那么为什么在HashMap中不直接采用key值的hashCode方法计算哈希值呢?
看下面这个例子:

public class MapPractice {
    public static void main(String[] args) {
        String str = "你好";
        System.out.println(str.hashCode());
    }
}

在这里插入图片描述
由于Object提供的hashCode得到的是一个32位整数值,直接将其作为数组下标,会浪费大量空间。
而将h右移16位,其实就是只保留了高16位,当数字越来越大的时候,低位的数字没多大意义。
将这两者进行异或运算,实际就是将高低16位都参与异或运算,减少hash冲突。
再来看看右移16位后的结果,差别不是一般的大:

public class MapPractice {
    public static void main(String[] args) {
        String str = "你好";
        System.out.println(str.hashCode());
        System.out.println(str.hashCode() >>> 16);
    }
}

在这里插入图片描述
再来详细分析putVal()方法:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // 此时哈希表还未初始化,第一次调用put方法
        if ((tab = table) == null || (n = tab.length) == 0)
            // 哈希表初始化
            n = (tab = resize()).length;
        // key值计算后的数组下标未存储元素,将元素直接添加到数组该索引下
        if ((p = tab[i=(n-1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        // 哈希表已经初始化并且key运算后的下标已经有元素了
        else {
            Node<K,V> e; K k;
            // 此时要插入的元素key值与数组元素key值相等,更新数组元素的value即可
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            // 此时链表已经树化,使用红黑树方式插入新节点
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            // 按照链表形式插入新节点
            else {
                for (int binCount = 0; ; ++binCount) {
                    // 链表中没有一个元素的key与当前元素key值相等,采用尾插法将当前元素插入到链表中
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 当前节点的key与链表中某一节点key值相等
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            // 此时并未添加新节点,只是更新了value值
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        // 此时确实添加了新节点
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

在putVal()中,有这么一句代码,if ((p = tab[i=(n-1) & hash]) == null)。真正的数组下标为i=(n-1) & hash,hash是上面说过的高16位整数,n是哈希表的大小。那么这里就有两个问题:
为何HashMap中哈希表的大小始终为2n?
2n-1二进制所有位数都为1,保证哈希中的所有索引下标都有机会被访问到。如果不是2n,比如说n=15,那么n-1就是14,二进制为1110,我们算的是下标的位置,如果最后一位为0,它与hash进行与运算之后,怎么也算不出1111这种下标,浪费了空间。

为何hash()得出来的值仍然不能直接作为数组下标?
即便保留高16位,得到的数仍有可能很大。而 i = (n - 1) & hash能保证得到的下标一定在数组的长度范围内。
举个栗子:

public class MapPractice {
    public static void main(String[] args) {
        int temp = "hello world".hashCode() >>> 16;
        System.out.println(temp);
        System.out.println(temp & 15);
        int temp1 = "南风知我意".hashCode() >>> 16;
        System.out.println(temp1);
        System.out.println(temp1 & 15);
    }
}

在这里插入图片描述

1.2 容量、负载因子和树化
  • 树化逻辑:
    当前数组下标对应的链表长度(添加完新节点后) >= 8 并且此时哈希表的长度 >= 64,才会将此链表树化,否则只是做了简单的扩容处理。

  • 树化原因:
    当链表长度过长时,哈希表的CURD时间复杂度会退化为O(n)。将链表变为红黑树,会将时间复杂度降为O(logn)。

  • 负载因子:
    决定了哈希表达到容量的百分比大小进行扩容阈值。
    若负载因子 > 0.75:增加了哈希表的利用率,哈希冲突概率明显增加
    若负载因子 < 0.75:降低了哈希冲突的概率,空间利用率降低

  • 容量:
    扩容:resize(初始化长度为16,但不是等数组全部填满才开始扩容,当容量达到16*0.75的时候就要扩容了)
    每次哈希表扩容时扩容为原先的2倍
    当红黑树节点个数小于6时,在下一次resize过程中,将红黑树退化为链表(节省空间)。

table.length:当前哈希表的长度,即数组的元素个数
map.size:当前存储的具体元素个数
这两者毫无关系,当发生第一次扩容的时候,size依然是存储的具体元素个数,而length是哈希表的长度,由16变为32。
size和length的大小关系也不能确定,如果哈希冲突特别频繁,一个数组索引下面都有可能有一个大的红黑树。

2、Hashtable子类

观察Hashtable:

public class MapPractice {
    public static void main(String[] args) {
        Map<Integer, String> map = new Hashtable<>();
        map.put(1, "Hello");
        map.put(1, "hello");
        map.put(3, "Java");
        map.put(2, "Nanfeng");
        System.out.println(map);
    }
}

在这里插入图片描述
考点:请解释HashMap和Hashtable的区别

  • 推出版本:
    HashMap是JDK 1.2提出的,Hashtable是JDK 1.0提出的
  • 性能:
    HashMap采用异步处理,性能高,Hashtable采用同步处理,性能较低
  • 安全性
    HashMap非线程安全,Hashtable线程安全
  • null操作
    HashMap允许存放null,有且只有一个,Hashtable的key和value都不为空,否则出现NullPointerException。

3、ConcurrentHashMap子类

concurrentHashMap如何高效实现线程安全?
与HashTable对比,HashTable是一个内置线程安全的类(1.0版本之后出现的),性能比较低,一张表就一把锁,并发线程数为1,即使其他线程在另外数组里操作都是互斥的,锁粒度太粗,锁的个数也太少了。
1.7版本采用Segement结构,一张表变为16把锁,锁粒度变细,由锁一张大表的区域变成锁一个小的哈希表区域。
1.8版本锁粒度更细,只锁数组元素一个元素,锁个数更多,支持的并发线程数更多。

Hashtable保证线程安全:
使用synchronized同步方法,锁当前Hashtable对象即整张哈希表,支持的并发线程数为1。

JDK 1.7 与JDK 1.8 ConcurrentHashMap的设计区别?

JDK1.7 ConcurrentHashMap结构

在这里插入图片描述

  • 使用Segement+哈希表
  • Segement初始化为16后无法扩容,扩容发生在每个Segement对应的小哈希表
  • 使用Lock体系保证线程安全(Segement是ReentrantLock子类),整张表有16把锁(锁的对象是Segement),支持的并发线程数为16
JDK1.8 ConcurrentHashMap结构

在这里插入图片描述

  • 结构与JDK 1.8 HashMap基本一致,使用哈希表+红黑树,不再使用Segement
  • 使用CAS机制+Synchronized同步块来保证线程安全,锁的对象为哈希表的数组元素(锁的粒度更细,锁的个数也更多,锁的个数会随着哈希表的扩容而增加,16,32,64.。。。)

4、TreeMap子类

TreeMap是一个可以排序的Map子类,它是按照key的内容排序的。
观察TreeMap的使用:

public class MapPractice {
    public static void main(String[] args) {
        Map<Integer, String> map = new TreeMap<>();
        map.put(2, "C");
        map.put(0, "A");
        map.put(1, "B");
        System.out.println(map);
    }
}

在这里插入图片描述
TreeMap里的排序依然是按照Comparable接口完成的。
有Comparable出现的地方,判断依据就依靠compareTo()方法完成,不再需要equals()与hashCode()。

HashMap、TreeMap、Hashtable的关系与区别:

  • 这三个类都是Map接口的常用子类,其中HashMap使用哈希表+红黑树(JDK1.8之后,JDK1.8之前单纯哈希表实现),TreeMap使用红黑树,Hashtable使用哈希表。
  • Hashtable使用synchronized同步方法(将整个哈希表上锁,锁粒度太粗),线程安全,性能很低。
    HashMap、TreeMap采用异步处理,线程不安全,性能较高。
  • 关于null
    HashMap key与value都允许为null
    TreeMap key不能为null,value允许为null
    Hashtable key与value均不为null

关于TreeMap的key值不能为空的解释:
要把元素放到TreeMap中,由于key值所在的类必须要覆写comparable或者comparator接口,然后去调用compareTo或者compar方法,这俩方法都是对象方法,必须有对象,null不能调用,所以key值绝对不能为空,否则会空指针异常。

5、Map集合使用Iterator输出(******)

Map接口与Collection接口不同,Collection接口有Iterator()方法可以很方便的取得Iterator对象来输出,而Map本身没有这个方法。
观察Collection接口与Map接口数据保存的区别:
在这里插入图片描述
在Map接口里有一个很重要的方法,可以将Map集合转为Set集合:
public Set<Map.entry<K,V>> entrySet();
Map如果要调用Iterator接口输出,是一个间接使用的模式,直接看代码:

public class MapPractice {
    public static void main(String[] args) {
        Map<Integer, String> map = new HashMap<>();
        map.put(1, "Hello");
        map.put(3, "Java");
        map.put(2, "Nanfeng");
        //1.将Map集合转为Set集合
        Set<Map.Entry<Integer, String>> set = map.entrySet();
        //2.获取Iterator对象
        Iterator<Map.Entry<Integer, String>> iterator = set.iterator();
        //3.输出
        while (iterator.hasNext()) {
            //4.取出每一个Map.Entry对象
            Map.Entry<Integer, String> entry = iterator.next();
            //5.取得key和value
            System.out.println(entry.getKey() + "=" + entry.getValue());
        }
    }
}

在这里插入图片描述

6、关于Map中key的说明

在之前使用Map集合的时候使用的都是系统类作为key(Integer,String等),实际上我们也可以采用自定义类作为key,这时一定要记得覆写Object类的hashCode()和equals()方法。

//自定义类作为key,系统类作为value
class Person {
    private Integer age;
    private String name;

    public Person(Integer age, String name) {
        this.age = age;
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Person{" + "age=" + age + ",name='" + name + "\'" + "}";
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null || getClass() != obj.getClass())
            return false;
        Person person = (Person) obj;
        return Objects.equals(age, person.age) && Objects.equals(name, person.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(age, name);
    }
}

public class MapPractice {
    public static void main(String[] args) {
        Map<Person, String> map = new HashMap<>();
        map.put(new Person(15, "张三"), "zs");
        System.out.println(map.get(new Person(15, "张三")));
    }
}

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值