Java-Day-18( Map 接口、各实现类 )

发布时间 2023-05-10 00:15:07作者: 朱呀朱~

Java-Day-18

Map

接口

  • Map 存放是 K - V ( 双列 ) 元素,K 和 V 都是输入的具体的对象
    • Set 也是 K - V 键值对的形式,只不过除了 K 都是表示值,V 是用常量 PRESENT 来替代的

  • Map 接口实现类的特点 ( 这里讲的是 JDK8 的接口特点 )

    • Map 与 Collection 并列存在 ( 两大类之间无关 )。用于保存具有具有映射关系的数据:Key-Value

      Map map = new HashMap();
      map.put("no1", "zyz");
      map.put("no2", "duang");
      System.out.println("map=" + map);
      //        map={no2=duang, no1=zyz},仍是无序,key 实际还是 hash,仍是0,1,2...里计算的索引处存放着:no1=zyz
      
    • Map 中的 key 和 value 可以是任何引用类型的数据,会封装到 HashMap$Node 对象中 ( 同 Set 讲过的 )

    • Map 中的 key ( K ) 不允许重复 ( 原因和 HashSet 一样 ),如果重复了就等价于整体替换旧的

    • Map 中的 value ( V ) 是可以重复的

      • 类似于 K - V 是楼内,门牌号 K 是不会有重复的,但里面住所的装饰 V 是可以相同的
    • Map 中的 key、value 都可以为 null,但 key 仍只能有一个 null,value 可多个

    • 常用 String ( 要求是 Object 就可 ) 来作为 Map 的 key

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

      System.out.println(map.get("no2")); // duang (传key得value)
      
    • Map 存放数据:一对 key - value 是存放在一个 Node 中的 ( HashMap$Node:静态的,存有 hash、key、value、next 的 Node ),又因为 Node 实现了 Entry 接口,有些书上也说一对 k-v 就是一个 Entry

      • 但 Entry 里可理解为含 Set 和 Collection 两列分别存 k 和 v 的集合,实际只是简单的引用,还是指向的 Node,只不过为了便于程序员遍历,总和成了 EntrySet 集合 transient Set<Map.Entry<K,V>> entrySet; 存放 Entry 元素类型 ( EntrySet<Entry<K, V>> )

        // 这里举例用HashMap来实现Map接口
        Map map = new HashMap();
        map.put("no1", "zyz");
        map.put("no2", "duang");
        map.put(null, "dongdongdong");
        
        // getclass查看运行类型
        Set set = map.entrySet();
        System.out.println(set.getClass()); // class java.util.HashMap$EntrySet
        for (Object obj : set) {
            //            向下转型
            Map.Entry entry = (Map.Entry) obj;
            System.out.println(entry.getKey() + "-" + entry.getValue());
            //            内含get方法,便于程序员遍历
        }
        
        // Map接口定义了返回值:Set<K> keySet(); 和 Collection<V> values();
        Set set1 = map.keySet();
        System.out.println(set1.getClass()); // class java.util.HashMap$KeySet
        Collection values = map.values();
        System.out.println(values.getClass()); // class java.util.HashMap$Values
        

        image-20230509093106971

      • EntrySet 定义的类型是 Map.Entry Set<Map.Entry<K, V>> entrySet();,但是实际上存放的还是 HashMap$Node,因为是 Node 实现了 Entry ( HashMap$Node implements Map.Entry,因此 Node 可以扔进 Entry 里 )

        注意,entry 只是个接口而已,node 来实现它,并不是 node 带着数存进 entryset,而是带有 node 的数据信息的 entry 存进了 entryset

      • debug 可知 entrySet 里就是多个简单的指向,存的是真正数据的地址,并不是数据

      • HashMap implements Map,Map 实际上就是有一个 table 表,存有 node,这个表由数组 + 链表 + 红黑树,并为了方便管理做了一个控制,将 node 封装成了一个 entry,组合形成后放进集合 entrySet 里,又为了方便使用,通过 set 方法将 key 都给放进了集合的 KeySet ( Set ) 里,将 value 都放进了 Values ( Collection ) 里,这样就可以用 map 的 entrySet 取出 entry,再用 getKey 和 getValue 操作了

      • 简例:

        1. 一个学生 ( 数据 ) 有学号 k,姓名 v,前后桌等基本信息 ( Node )
        2. 老师把基本信息 ( Node ) 做成电子名片 ( Entry ) 的形式 ( 实际还是 Node )
        3. 然后把电子名片 ( Entry ) 通过 entrySet() 方法,记录在学生教育系统 ( EntrySet ) 中
        4. 这样做,是因为学生教育系统 ( Set 接口类型 ) 有遍历电子名片的功能 ( 迭代器 )
        5. 电子名片 ( Entry ) 有 getKey() 和 getValue() 功能就可以方便查询学生的学号 k 和姓名 v 了

常用方法

  • Map 接口的常用方法

    Map map = new HashMap();
    //        添加 put
    map.put("no1", "zyz");
    map.put("no2", "duang");
    //        根据键删除映射关系
    map.remove("no1");
    System.out.println(map);
    //        根据键获取值
    Object val = map.get("no2");
    System.out.println(map);
    //        获取元素的个数
    System.out.println("k-v 有" + map.size() + "个"); // k-v 有1个
    //        判断个数是否为空
    System.out.println(map.isEmpty()); //false
    //        清除k-v
    map.clear();
    System.out.println("map = " + map); // map = {}
    
    map.put("1", "dongdongdong");
    //        查找键是否存在
    System.out.println(map.containsKey("1")); // true
    
  • Map 接口的遍历方法

    public static void main(String[] args) {
        Map map = new HashMap();
        //        添加 put
        map.put("no1", "zyz");
        map.put("no2", "duang");
        map.put(null, "dongdongdong");
    
        //        第一组
        Set keyset = map.keySet(); // 存放所有key的集合
        //        1. 增强 for 循环
        System.out.println("通过key取到值的方法一:");
        for (Object key : keyset) {
            System.out.println(key + "-" + map.get(key));
        }
        //        2. 迭代器
        Iterator iterator = keyset.iterator();
        System.out.println("通过key取到值的方法二:");
        while (iterator.hasNext()) {
            Object key = iterator.next();
            System.out.println(key + "-" + map.get(key));
        }
        
        System.out.println("****************上为EntrySet里的KeySet***************");
        System.out.println("****************下为EntrySet里的Values***************");
        
        //        第二组
        Collection values = map.values(); // 存放所有value的集合
        //        1. 增强 for 循环
        System.out.println("方法一的直接取值");
        for (Object val : values) {
            System.out.println(val);
        }
        //        2. 迭代器
        Iterator iterator2 = keyset.iterator();
        System.out.println("方法二的直接取值");
        while (iterator2.hasNext()) {
            Object val = iterator2.next();
            System.out.println(map.get(val));
        }
    
        //        第三组
        System.out.println("**********下为通过EntrySet来获取 k-v");
        Set entrySet = map.entrySet(); // EntrySet<Map.Entry<K,V>>
        //        1. 增强 for 循环
        System.out.println("方法一");
        for (Object entry : entrySet) {
            //            向下转型掉 Object 为 Map.Entry
            Map.Entry m = (Map.Entry) entry;
            System.out.println(m.getKey() + "-" + m.getValue());
        }
        //        2. 迭代器
        System.out.println("方法二");
        Iterator iterator3 = entrySet.iterator();
        while (iterator3.hasNext()) {
            Object entry = iterator3.next();
            //            System.out.println(next.getClass()); // class java.util.HashMap$Node
            //            向下转型为 Map.Entry
            Map.Entry m = (Map.Entry) entry;
            // 			  m.getValue()才是值,单独接收可以再下转一次
            System.out.println(m.getKey() + "-" + m.getValue());
        }
    }
    
  • 小练习:使用 HashMap 添加了三个员工对象,要求键:员工id,值:员工对象

    并遍历显示工资大于10000的员工

    员工类:姓名、工资、员工 id

    public class test1 {
        public static void main(String[] args) {
            Map hashmap = new HashMap();
            hashmap.put(1, new Emp("zyz", 20000, 1));
            hashmap.put(2, new Emp("duang", 13000, 2));
            hashmap.put(3, new Emp("dongdongdong", 8000, 3));
    
            System.out.println("第一种遍历方式");
            Set keySet = hashmap.keySet();
            for (Object key : keySet) {
    //            System.out.println(obj);
                Emp emp = (Emp) hashmap.get(key);
                if (emp.getSalary() > 10000) {
                    System.out.println(emp);
                }
            }
    
            System.out.println("第二种遍历方式");
            Set entrySet = hashmap.entrySet();
            Iterator iterator = entrySet.iterator();
            while (iterator.hasNext()) {
                Map.Entry entry = (Map.Entry) iterator.next();
                Emp emp = (Emp) entry.getValue();
                if (emp.getSalary() > 10000) {
                    System.out.println(emp);
                }
            }
        }
    }
    
    class Emp {
        private String name;
        private double salary;
        private int id;
        public Emp(String name, double salary, int id) {
            this.name = name;
            this.salary = salary;
            this.id = id;
        }
    
        @Override
        public String toString() {
            return "Emp{" +
                    "name='" + name + '\'' +
                    ", salary=" + salary +
                    ", id=" + id +
                    '}';
        }
    
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public double getSalary() {
            return salary;
        }
        public void setSalary(double salary) {
            this.salary = salary;
        }
        public int getId() {
            return id;
        }
        public void setId(int id) {
            this.id = id;
        }
    }
    
    • EntrySet 方法的难点在于理解其包装,k-v 包装进 Node ( Emp ) 节点,中间 Entry 过渡,再包装到 EntrySet 里
    • 所以取出来就是先转 Map.Entry,再 getValue() 取转为节点使用
    • 勿忘了 entry 真正的运行类型:HashMap$Node

HashMap

了解小结

  • Map 接口的常用实现类:HashMap、Hashtable、Properties

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

  • HashMap 是以 k-v ( 键值 ) 对的方式来存储数据

  • key 不能重复,但是值可以重复,允许 null

  • 如果添加相同的 key,则会覆盖原来的 k-v,等同于修改替换

  • 与 HashSet 一样,不保证映射顺序 ( HashSet 本质就是 HashMap:数组 + 链表 + 红黑树 ),因为底层是以 hash 表的方式来存储的

  • HashMap 没有实现同步,因此线程是不安全的,方法无同步互斥方法 ( synchronized )

源码底层分析

  • 仍是 table ( Node 类型数组 ),里面重复索引的用链表形式,到时机树化

  • 如果添加相同的 key,则会覆盖原来的 k-v,等同于修改替换

    // 在 putVal():
    if (p.hash == hash &&
        ((k = p.key) == key || (key != null && key.equals(k))))
    // 知重复的话上述判断成立
        e = p;
    
    // 直接到:
    if (e != null) { // existing mapping for key
        V oldValue = e.value;
    // onlyIfAbsent 是从上一步传来的固定参数:false 
    // * @param onlyIfAbsent if true, don't change existing value
    // 就是用来判断是否要进行替换    
        if (!onlyIfAbsent || oldValue == null)
    // 把value值进行了替换        
            e.value = value;
        afterNodeAccess(e);
        return oldValue;
    }
    
    • 最初 Hashmap 构造器,获取 loadFactor = 0.75,table 表为 null
    • 存储的 put() 方法,返回 return putVal(hash(key), key, value, false, true);,在其中计算静态 hash 值 ( return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); )
    • 执行核心 putVal():
      • 最初空 table 走 resize(),拿到临界点等值 ( 初始化大小 16,临界 12 等 ),Node<k, v>[] newTab 给 table ( 内皆 null ),携长度,out 出来得 n
      • 计算索引值看此处是否为空 (p = tab[i = (n - 1) & hash]) == null ,空就创建并放入 Node tab[i] = newNode(hash, key, value, null);,不达临界,返回 null 表示成功
      • 之后步骤都同先前 Java-Day-17 所述
  • 大体核心代码各判断

    1. 表空否

    2. 计算索引处空否

    3. 上述都 F,存 Node e

      1. 索引处 hash 等 + 同对象或同值否
      2. 红黑树否
      3. 上述都 F,索引处死循环链表
        1. 链表内无同 k,e 为 null
          1. 树化否
        2. 链表内 hash 等 + 同对象或同值否
      4. e 空否 ( 都要经过 )
        1. 同 k 替换 v 否
    4. 修改成功 + 1

      1. 扩容否
  • 自己编写代码模拟扩容:

    public class test1 {
        public static void main(String[] args) {
            HashMap hashMap = new HashMap();
            for (int i = 0; i <= 12; i++) {
                hashMap.put(new Z(i), "zyz");
            }
    //        起初table内16个(下标到15),当到put到第9个时,table扩成了32个(下标到31),当前列表仍是Node非树化,所有数据仍都在一条链表上
    //        再put到第10个,table扩成了64个(下标到63),数组table大小变化会重新计算索引位置,所有数据仍都在一条链表上
            System.out.println("hashMap = " + hashMap); // 只修改了hashCode,equals没改,仍是12个k-v
        }
    }
    
    class Z {
       private int num;
    
        public Z(int num) {
            this.num = num;
        }
    //    为展现扩容debug:
    //    使得所有的Z对象的hashCode值都是100,这样就会都落在table的同一个索引位置了
        @Override
        public int hashCode() {
            return 100;
        }
    
        @Override
        public String toString() {
            return "\nZ{" +
                    "num=" + num +
                    '}';
        }
    }
    

LinkedHashMap

  • LinkedHashSet 底层是 LinkedHashMap 再底层是 HashMap,核心都在 HashMap 里

Hashtable

了解介绍

  • 存放元素仍是键值对
  • 键和值都不能为 null,否则会空指针异常
  • hashtable 是线程安全的 ( hashMap 线程不安全 )
  • 仍存在等键替换

源码底层分析

  • 底层有数组初始化:table = Hashtable$Entry[11]
    • 底层就是 Entry 数组,添加方法:addEntry(hash, key, value, index);
  • 临界值仍是 11 * 0.75 取的 8,put 到 9 时扩容到 23 ( 下标 22 )
  • 扩容机制是:int newCapacity = (oldCapacity << 1) + 1 —— 乘二加一
  • put:Node —> Entry —> table

HashMap 和 Hashtable 对比

版本 线程安全 ( 同步 ) 效率 null 键值
HashMap 1.2 不安全 可以
Hashtable 1.0 安全 较低 不可以

Properties

了解认识

  • Properties 类继承自 Hashtable 类并且实现了 Map 接口 ( 间接的 ),也是使用一种键值对的形式来保存数据
  • 使用特点和 Hashtable 类似
  • Properties 还可以用于从某一 xxx.properties 文件中,加载数据到 Properties 类对象,并进行读取和修改 ( 如:存储连接数据库信息等,就不用将信息写进程序里了 )
  • 说明:工作后 xxx.properties 文件常作为配置文件,IO 流时举例讲解

代码举例分析

  • Properties 类继承自 Hashtable

  • 可以通过 k-v 存放数据,键值都不能为 null

  • 无序,有替换

    public static void main(String[] args) {
        Properties properties = new Properties();
        //        增加
        properties.put("zyz", 100);
        properties.put("duang", 500);
        properties.put("duang", 300);
        System.out.println(properties);
        //        通过 key 获取值
        System.out.println(properties.get("duang")); // 300
        //        删除
        properties.remove("zyz");
        //        修改
        properties.put("zyz", 820820);
        System.out.println(properties);
    }