Back

Java集合类(三)

Map接口及其子类的剖析

Java集合类(三)


  1. Map接口实现类的特点和常用方法
  2. Map接口的六大遍历方式
  3. Map小结及HashMap底层源码分析
  4. Hashtable基本介绍

Map接口实现类的特点和常用方法

Map接口的特点

注意:这里讲的是Jdk8的Map接口特点

  • Map与Collection并列存在。用于保存具有映射关系的数据:key-value

  • Map中的key和value可以是任何引用类型的数据,会封装到HashMao$Node对象中

  • Map中的key不允许重复,原因和HashSet一样

  • Map中的value可以重复

  • Map的key可以为null,value也可以为null,但是key为null的结点只能有一个,value为null的结点可以有多个

  • 常用String类作为Map的key

  • key和value之间存在单向一对一关系,即通过指定的key总能找到对应的value

    代码演示

    package com.conllection_.maps;
    
    import java.util.HashMap;
    import java.util.Map;
    
    public class Map_ {
        public static void main(String[] args) {
            Map map = new HashMap();
    
            //key不可以重复,重复的会被等价替换
            //value可以重复
            map.put("no1","成志恒");//k-v
            map.put("no2","张无忌");//k-v
            map.put("no1","张三丰");//当有相同的key,就等于等价替换
            map.put("no3","成志恒");//k-v
            //key只能有一个null
            map.put(null,null);
            map.put(null,"abc");//等价替换
            //value可以多个为null
            map.put("no4",null);
            map.put("no5",null);//value可以多个为null
            map.put(1,"赵敏");
    
            //通过get方法,传入key,会返回对应的value
            System.out.println(map.get("no3"));
    
            System.out.println("map = " + map);
        }
    }
    

    image01

Map接口k-v详解

  • Map存放数据的key-value示意图,一对k-v是放在一个HashMap$Node中的。因为Node实现了Entry接口,所以有些书上也说一对k-v就是一个Entry。

  • k-v 数据最后是存放在HashMap$Node node = newNode(hash,key,value,null) 这个对象里面的。

  • k-v 为了方便程序员的遍历,还会创建EntrySet集合,该集合存放的元素的类型是Entry。而一个Entry对象本身就具有key,value值。所以有EntrySet<Entry<K,V»,即

    transient Set<Map.Entry<K,V>> entrySet;
    
  • EntrySet中,定义的类型是Map.Entry,但是实际上存放的还是HashMap$Node。原因如下

    static class Node<K,V> implements Map.Entry<K,V> 
    
  • 由于Map.Entry提供了两个重要的方法:K getKey(); V getValue() ,所以当我们把HashMap$Node对象存放到EntrySet就可以方便我们的遍历。

    image02

    程序演示

    package com.conllection_.maps;
    
    import java.util.HashMap;
    import java.util.Map;
    import java.util.Set;
    
    public class MapSource {
        public static void main(String[] args) {
            Map map = new HashMap();
            map.put("no1","成志恒");//k-v
            map.put("no2","张无忌");//k-v
    
            Set set = map.entrySet();
            for (Object obj:set) {
                //向下转型
                Map.Entry entry = (Map.Entry)obj;
                System.out.println(entry.getKey() + "-" + entry.getValue());
            }
            System.out.println("Map = " + map);
    
        }
    }
    

    输出结果:

    image03

    keySet():与Node封装到EntrySet集合一样,不过他是封装到Set集合,利用该方法可以单独遍历Key值

    values:与Node封装到EntrySet集合一样,不过他是封装到Collection集合,利用该方法可以单独遍历Value值

    Set set1 = map.keySet();
    System.out.println(set1);
    
    Collection values = map.values();
    System.out.println(values);
    

    运行结果

    image04

Map接口的常用方法

  1. put():添加元素

  2. remove():根据键删除映射关系

  3. get():根据键获取值

  4. size():获取元素的个数

  5. isEmpty():判断元素个数是否为0

  6. clear():清除集合键值对

  7. containsKey():查找键是否存在

    代码演示

    package com.conllection_.maps;
    
    import java.util.HashMap;
    import java.util.Map;
    
    public class MapMethod {
        public static void main(String[] args) {
            Map map = new HashMap();
    //        1. put():添加元素
            map.put("no1",new Book("小王子",100));
            map.put("no1","成志恒");
            map.put("no2","史森明");
            map.put("no3","李元浩");
            map.put("no4","李元浩");
            map.put(null,"小虎");
            map.put("no5",null);
    
            System.out.println("map = " + map);
    //        2. remove():根据键删除映射关系
            map.remove("no3");
            System.out.println("map = " + map);
    //        3. get():根据键获取值
            System.out.println(map.get("no1"));
    //        4. size():获取元素的个数
            System.out.println(map.size());
    //        5. isEmpty():判断元素个数是否为0
            System.out.println(map.isEmpty());
    //        6. clear():清除集合键值对
            map.clear();
            System.out.println(map.isEmpty());
            System.out.println(map.size());
    //        7. containsKey():查找键是否存在
            map.put("no1",new Book("小王子",100));
            System.out.println(map.containsKey("no1"));
            System.out.println(map.containsKey("no5"));
    
    
        }
    }
    class Book{
        private String name;
        private double price;
    
        public Book(String name, double price) {
            this.name = name;
            this.price = price;
        }
    
        @Override
        public String toString() {
            return "Book{" +
                    "name='" + name + '\'' +
                    ", price=" + price +
                    '}';
        }
    }
    

    image05

Map接口的六大遍历方式

  • 遍历以下Map接口的元素

    import java.util.Set;
    
    public class MapFor {
        public static void main(String[] args) {
            Map map = new HashMap();
            map.put("no1","成志恒");
            map.put("no2","史森明");
            map.put("no3","李元浩");
            map.put("no4","李元浩");
            map.put(null,"小虎");
            map.put("no5",null);
        }
    }
    
  1. 使用keySet()+增强for遍历k-v

    System.out.println("=====第一种遍历方式=====");
    Set keySet = map.keySet();
    
    for (Object key : keySet) {
        System.out.println(key+"="+map.get(key));
    }
    

    运行结果

    image06

  2. 使用keySet()+迭代器遍历k-v

    System.out.println("=====第二种遍历方式=====");
    Iterator iterator = keySet.iterator();
    while (iterator.hasNext()) {
        Object next =  iterator.next();
        System.out.println(next+"="+map.get(next));
    }
    

    运行结果

    image07

  3. 使用values()+增强for遍历value(因为value不能映射key,所以只能遍历value)

    System.out.println("=====第三种遍历方式=====");
    Collection values = map.values();
    for (Object value : values) {
        System.out.println(value);
    }
    

    运行结果

    image08

  4. 使用values()+迭代器遍历

    System.out.println("=====第四种遍历方式=====");
    Iterator iterator1 = values.iterator();
    while (iterator1.hasNext()) {
        Object next =  iterator1.next();
        System.out.println(next);
    }
    

    运行结果

    image09

  5. 使用EntrySet()+增强for遍历

    System.out.println("=====第五种遍历方式=====");
    Set entrySet = map.entrySet();
    
    for (Object obj : entrySet) {
        //将obj转成Map.Entry
        Map.Entry entry = (Map.Entry)obj;
        System.out.println(entry.getKey()+"-"+entry.getValue());
    }
    

    运行结果

    image10

  6. 使用EntrySet()+迭代器遍历

    System.out.println("=====第六种遍历方式=====");
    Iterator iterator2 = entrySet.iterator();
    while (iterator2.hasNext()) {
        Object next =  iterator2.next();
        Map.Entry entry = (Map.Entry)next;
        System.out.println(entry.getKey()+"-"+entry.getValue());
    }
    

    运行结果

    image11

HashMap练习题

  • 使用HashMap添加三个元素对象,要求:

    • 键:员工ID,值:员工对象
    • 遍历显示工资>18000的员工(遍历方法两种)
    • 员工类:姓名、工资、员工id

    员工类Staff代码如下

    class Staff{
        private String name;
        private double wages;
        private int id;
    
        public Staff(String name, double wages, int id) {
            this.name = name;
            this.wages = wages;
            this.id = id;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public double getWages() {
            return wages;
        }
    
        public void setWages(double wages) {
            this.wages = wages;
        }
    
        public int getId() {
            return id;
        }
    
        public void setId(int id) {
            this.id = id;
        }
    
        @Override
        public String toString() {
            return "Staff{" +
                    "name='" + name + '\'' +
                    ", wages=" + wages +
                    ", id=" + id +
                    '}';
        }
    }
    

    测试类MapExercise代码如下

    package com.conllection_.maps;
    
    import java.util.*;
    
    public class MapExercise {
        public static void main(String[] args) {
            Map map = new HashMap();
            map.put("01",new Staff("成志恒",19000,01));
            map.put("02",new Staff("史森明",20000,02));
            map.put("03",new Staff("小虎",17000,03));
            map.put("04",new Staff("李元浩",18000,04));
    
    
            System.out.println("===第一种方式===");
            //使用entrySet方法返回映射中包含的映射的 Set 视图
            Set entrySet = map.entrySet();
            //利用增强for循环遍历
            for (Object o : entrySet) {
                //向下转型,为了能调用父类的方法
                Map.Entry entry = (Map.Entry)o;
                //把value对象指向从entry集中取出来的value
                Object value = entry.getValue();
                //向下转型,这样可以使用Staff对象的getWages方法。
                Staff staff = (Staff) value;
                if(staff.getWages()>18000){
                    System.out.println(staff.toString());
                }
    
            }
            System.out.println("===第二种方式===");
            //使用keySet()方法返回封装到Map封装到Set里面的结点的Key值
            Set set = map.keySet();
            //创建迭代器
            Iterator iterator = set.iterator();
            while (iterator.hasNext()) {
                //获取迭代器中的数据
                Object next =  iterator.next();
                //向下转型,吧staff对象指向从map里面取到的value对象。
                Staff staff1 = (Staff) map.get(next);
                if (staff1.getWages()>18000){
                   System.out.println(staff1);
                }
            }
    
    
        }
    }
    

    运行结果:

    image12

    上述题目中,数据的封装情况如下图

    image13

    所以当我们需要取出value值时,我们需要先通过EntrySet取出Entry,然后通过Entry的指向取到HashSet$Node的值。

Map小结及HashMap底层源码分析

Map小结

  1. Map接口的常用实现类:HashMap、Hashtable和Properties

  2. HashMap是Map接口使用频率最高的实现类

  3. HashMap是以key-value对的方式来存储数据的

  4. key不能重复,但是value可以重复,允许使用null键(只能有一个)和null值(可以多个)

  5. 如果添加相同的可以,则会覆盖原来的key-value,等同于修改。(key不会替换,value会替换)

    if (e != null) { // existing mapping for key
        V oldValue = e.value;
        if (!onlyIfAbsent || oldValue == null)
            //此处e指向替换前的Node,所以 e.value = value;即可完成替换
            e.value = value;
        afterNodeAccess(e);
        return oldValue;
    }
    
  6. 与HashSet一样,不保证映射的顺序,因为底层是以hash表的方式来存储的。

  7. HashMap没有实现同步,因此线程是不安全的。方法没有做同步互斥的操作,没有synchronize

HashMap底层机制及源码剖析

  • HashMap底层机制示意图

    image14

    (key,value)是一个Node实现了Map$Entry<K,V>

  • HashMap的扩容机制[和HashSet扩容机制相同]

    • HashMap底层维护了Node类型的数组table,默认为null
    • 当创建对象时,将加载因子(loadfactor)初始化为0.75
    • 当添加key-value是,通过key的hash值得到在table的索引。然后判断该索引处是否有元素,如果没有元素直接添加。如果该索引处有元素,继续判断该元素的key和准备加入的key是否相等,如果相等,则直接替换value如果不相等需要判断是树结构还是链表结构,做出相应处理。如果添加时发现容量不够,则需要扩容。
    • 第一次添加时,会将需要扩容的table容量扩容到16,临界值(threshold)为12
    • 如果需要再次扩容,会将需要扩容的table容量扩容到原来的2倍(32),临界值为原来的2倍(24) 依次类推。
    • 在Java8中,如果一条链表的元素个数到达TREEIFY_THRESHOLD(默认是8),并且table的大小>=MIN_TREEIFY_CAPACITY(默认是64),就会进行树化(转换红黑树),否则任然采用数组扩容机制

    源码分析

    创建MapSource类

    package com.conllection_.maps;
    
    import java.util.HashMap;
    import java.util.Map;
    
    public class MapSource {
        public static void main(String[] args) {
            Map map = new HashMap();
            map.put("no1","成志恒");//k-v
            map.put("no2","张无忌");//k-v
            map.put("no1","张无忌");//k-v
        }
    }
    

    调试程序,执行HashMap的无参构造器,=。

    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
    

    将负载因子(loadFactor)初始化并且创建一个空的table。

    向HashMap集合添加元素,执行put方法

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

    通过hash方法计算key的Hash值,然后返回给putVal方法。

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

    执行putVal方法,程序首先判断table是否为null,如果为null就table的容量扩容到16

    if ((tab = table) == null || (n = tab.length) == 0){
    	n = (tab = resize()).length;
    }
    

    实现扩容的方法为risize()方法。

    继续调试,程序会判断当前hash值对应的table索引位置上是否已经存在值,如果不存在值,就把当前Hash值的结点赋值进去。

    if ((p = tab[i = (n - 1) & hash]) == null){
        tab[i] = newNode(hash, key, value, null);
    }
    

    然后modCount++(记录修改的次数),size++(记录已添加元素的个数);

    倘若当前hash值对应的table索引位置上已存在值,首先会判断当前索引位置上的hash值是否与需要添加的key值对应的hash值相同且满足以下两个条件之一:

    • 准备添加的key值与p指向的Node结点的key是同一个对象
    • p指向的Node结点的key的equals()和准备假如的key比较后相同。

    如果上述条件皆成立,直接让e指向p(p为当前索引位置上的结点),e为辅助变量

    if (p.hash == hash &&
        ((k = p.key) == key || (key != null && key.equals(k)))){
        e = p;
    }
    

    然后继续执行,把需要添加的key值对应的value值替换掉原先存在的key值的value值

    if (e != null) { // existing mapping for key
        V oldValue = e.value;
        if (!onlyIfAbsent || oldValue == null)
            e.value = value;
        afterNodeAccess(e);
        return oldValue;
    }
    

    返回修改后的value值。

    倘若上述条件不成立,程序会继续进行判断,判断需要添加的结点是否为树结点

    else if (p instanceof TreeNode){
        e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
    }
    

    如果是树结点,会执行putTreeVal()方法,把需要添加的树节点添加到红黑树上。

    如果不是树节点,程序会继续调试,进入到下一个else。

    else {
        for (int binCount = 0; ; ++binCount) {
            if ((e = p.next) == null) {
                p.next = newNode(hash, key, value, null);
                if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                    treeifyBin(tab, hash);
                break;
            }
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                break;
            p = e;
        }
    }
    

    在此处程序会进入一个死循环,直至找到对应索引位置上的尾结点或者找到key相同的结点才会退出。详细见HashSet底层添加元素源码分析的最后一步。

模拟HashMap触发扩容、树化情况,并debug验证

  • 创建测试类MapSource01

    package com.conllection_.maps;
    
    import java.util.HashMap;
    import java.util.Map;
    import java.util.Objects;
    
    public class MapSource01 {
        public static void main(String[] args) {
            Map map = new HashMap();
            for (int i=0; i<12;i++){
              map.put(new A(i),"hello");
            }
            System.out.println(map);
        }
    }
    

    创建A类,重写HashCode方法,让他们有统一的Hash值

    class A{
        private int no;
    
        public A(int no) {
            this.no = no;
        }
    
        public int getNo() {
            return no;
        }
    
        @Override
        public int hashCode() {
            return 100;
        }
    
        public void setNo(int no) {
            this.no = no;
        }
    
        @Override
        public String toString() {
            return "\nA{" +
                    "no=" + no +
                    '}';
        }
    }
    

    运行调试,直至添加到第9个元素,因为达到树化的条件(一条链表的元素个数到达8个),但table的大小<MIN_TREEIFY_CAPACITY(默认是64),所以table会扩容。

    image15

    继续调试,table再次扩容,直至table扩容到容量为64时,该链表会发生树化

    image16

Hashtable

Hashtable的基本介绍

  • Hashtable存放的元素时键值对:即K-V

  • Hashtable的键和值都不能为null,否则会抛出NullPointerException

    if (value == null) {
        throw new NullPointerException();
    }
    
  • Hashtable使用方法基本上和HashMap一样

  • Hashtable是线程安全的(synchronize),HashMap是线程不安全的

Hashtable底层的简单剖析

  • 底层有数组 Hashtable$Entry[],初始化大小为11

  • 临界值 threshold = 8 (11*0.75)

    创建HashtableSource类调试

    package com.conllection_.maps;
    
    import java.util.Hashtable;
    
    public class HashtableSource {
        public static void main(String[] args) {
            Hashtable hashtable = new Hashtable();
    
            hashtable.put("john",100);
            hashtable.put("jack",100);
            hashtable.put("mary",200);
            hashtable.put("tom",300);
            hashtable.put("smith",500);
            hashtable.put("minster",600);
            hashtable.put("chris",200);
            hashtable.put("ssm",500);
    
            System.out.println("hashtable = " + hashtable);
        }
    }
    

    调试程序,我们可以发现

    image17

  • Hashtable按照自己的扩容机制进行扩容

    继续调试HashtableSource,执行put方法,可以发现该接口添加元素时执行了addEntry方法把添加的K-V封装到Entry

    private void addEntry(int hash, K key, V value, int index) {
        modCount++;
    
        Entry<?,?> tab[] = table;
        //当增加的元素数量大于或等于临界值时,执行rehash方法进行扩容
        if (count >= threshold) {
            // Rehash the table if the threshold is exceeded
            rehash();
    
            tab = table;
            hash = key.hashCode();
            index = (hash & 0x7FFFFFFF) % tab.length;
        }
    
        // Creates the new entry.
        @SuppressWarnings("unchecked")
        Entry<K,V> e = (Entry<K,V>) tab[index];
        tab[index] = new Entry<>(hash, key, value, e);
        count++;
    }
    

    执行rehash方法进行扩容。下面截取部分rehash源码。

    protected void rehash() {
        int oldCapacity = table.length;
        Entry<?,?>[] oldMap = table;
    
        // overflow-conscious code
        //进行扩容
        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
        Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
        }
    

    我们可以发现,当容量达到临界值时,rehash方法会通过 **int newCapacity = (oldCapacity « 1) + 1;**这条语句进行扩容。所以hashtable第一次扩容后的容量为23(11*2+1);

    image18

HashMap与Hashtable对比

image19

Properties

  • Properties类继承于Hashtable类并实现了Map接口,也是使用一种键值对的形式来保存数据

  • 他的使用特点和Hashtable类似

  • Properties还可以用了从xxx.properties文件中,加载数据到Properties类对象,并进行读取和修改

  • xxx.properties文件通常作为配置文件,详解见Java读取properties配置文件

    Properties的简单使用

    Properties_类

    package com.conllection_.maps;
    
    import java.util.Properties;
    
    public class Properties_ {
        public static void main(String[] args) {
            //Properties 继承了Hashtable
            //Properties 也是通过k-v存储数据,当然key与value都不能为null
            Properties properties = new Properties();
    
            //添加元素
            properties.put("jack",1);
            properties.put("john",2);
            properties.put("tom","2");
    
            System.out.println("properties = "+properties);
    
            //删除元素
            properties.remove("jack");
            System.out.println("properties = "+properties);
    
            //修改元素
            properties.put("jack",3);//替换
            System.out.println("properties = "+properties);
    
            //查找元素
            System.out.println(properties.get("jack"));
            System.out.println(properties.getProperty("tom"));
        }
    }
    

    运行结果

    image20

    需要注意的是,使用getPeoperties时,当put进去的值不是String类型的时候,会返回null

    例如在Properties类中添加以下代码

    properties.put("jack",1);
    System.out.println(properties.getProperty("jack"));
    

    image21

    阅读getProperty源码,我们可以发现其中原理

    public String getProperty(String key) {
        //把oval指向key对应的value
        Object oval = super.get(key);
        //使用三目运算符,当oval是String类型时,返回强转为String类型的oval,否则为null
        String sval = (oval instanceof String) ? (String)oval : null;
        //当sval为null,且设定了defaults的值时,返回defaultsValue,否则返回sval
        return ((sval == null) && (defaults != null)) ? defaults.getProperty(key) : sval;
    }
    
Built with Hugo
Theme Stack designed by Jimmy