第14章 集合——(4)Map

一、Map接口特点

Set接口和Map接口的联系与区别:

  1. Set底层存放的是:K-V,K:输入的要存放的对象,V:PRESENT常量
  2. Map存放的是:K-V,K和V:输入的要存放的对象

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

  1. Map和Collection并列存在,用于保存具有映射关系的数据:Key-Value。
  2. Map中的Key和Value可以是任何引用数据类型的数据,会封装到HashMap$Node对象中。
  3. Map 中的 Key 不允许重复,可以为 null,但 key 为 null 只能有一个。
  4. Map 中的 Value 可以重复,可以为 null,value 为 null 可以有多个。
  5. 常用 String 类作为 Map 的 key。
  6. key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到对应的 value(get 方法)
import java.util.HashMap;
import java.util.Map;

@SuppressWarnings({"all"})
public class Map_ {
    public static void main(String[] args) {
        Map map = new HashMap();

        //(1)Map和Collection并列存在,用于保存具有映射关系的数据:Key-Value。
        //(2)Map中的Key和Value可以是任何引用数据类型的数据,会封装到HashMap$Node对象中。
        //(3)Map中的Key不允许重复,原因和HashSet一样
        //(4)Map中的Value可以重复
        //(5)Map的key和value都可以为null,但key为null只能有一个,value为null可以有多个
        //(6)常用String类作为Map的key
        //(7)key和value之间存在单向一对一关系,即通过指定的key总能找到对应的value(get方法)
        map.put("No1", "韩顺平");//Key:No1,Value:韩顺平
        map.put("No2", "张无忌");
        map.put("No2", "张三丰");//添加相同的key,就会替换原本的value
        map.put("No3", "韩顺平");
        map.put(null, null);
        map.put(null, "abc");//替换
        map.put("No4", null);
        map.put("No5", null);
        map.put(1, "赵敏");//1首先进行包装
        map.put(new Object(), "金毛狮王");

        System.out.println(map);
        //输出结果:{No2=张三丰, null=abc, No1=韩顺平, 1=赵敏, No4=null, No3=韩顺平, No5=null, java.lang.Object@1540e19d=金毛狮王}
        //通过get方法,传入key,会返回对应的value
        System.out.println(map.get("No2"));//张三丰
    }
}

        7.一对 k-v 放在一个 HashMap$Node 中。因为 Node 实现了 Entry 接口,所以有些书也说一对 k-v 就是一个 Entry。

//HashMap 的静态内部类 Node    
static class Node<K,V> implements Map.Entry<K,V> {
        //一对 k-v 放在一个 HashMap$Node 中
        //因为 Node 实现了 Entry 接口,所以有些书也说一对 k-v 就是一个 Entry
        final int hash;
        final K key;//键key
        V value;//值value
        Node<K,V> next;

        ...
     }

k-v 真正存放在 HashMap$Node 中,keySet 集合和 Values 集合只是指向了 HashMap$Node 中的 key 和 value 而已(引用)。为了方便遍历,keySet 集合中放置了所有 key 的引用,Values 集合中放置了所有 Value 的引用,一组 k-v 形成一个 Entry,所有 Entry 放置在 entrySet 集合中。

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

@SuppressWarnings({"all"})
public class MapSource_ {
    public static void main(String[] args) {
        Map map = new HashMap();
        map.put("no1", "张无忌");//key:no1 value:张无忌
        map.put("no2", "韩顺平");//k-v
        map.put(new Car(), new Person());
        /*
        1.k-v 最终是 HashMap$Node node = newNode(hash, key, value, null)
            Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
                return new Node<>(hash, key, value, next);
            }
            newNode 方法:返回一个 Node 对象
        2.为了方便遍历,创建了一个 entrySet 集合,该集合存放的元素类型是 Entry,一个 Entry 对象包含 k,v
          EntrySet<Entry<K,V>> : transient Set<Map.Entry<K,V>> entrySet;
          entrySet 集合中,定义的元素类型是 Map.Entry,但实际存放的是 HashMap$Node(HashMap$Node 实现了 Map.Entry)
        3.方便遍历:Map.Entry 提供了两个重要的方法-getKey()和getValue()方法
         */

        Set entrySet = map.entrySet();//entrySet集合:存放的元素类型是 Entry,Entry 中存放 k-v 的引用
        System.out.println(entrySet.getClass());//class java.util.HashMap$EntrySet
        for (Object obj: entrySet) {
            System.out.println(obj.getClass());//class java.util.HashMap$Node

            //为了从 HashMap$Node 中取出 k-v
            //1.先向下转型
            Map.Entry entry = (Map.Entry) obj;//向下转型,编译类型:Object -> Map.Entry
            //2.getKey():获取key值;getValue():获取key值
            System.out.println(entry.getKey() + "-" + entry.getValue());//no2-韩顺平,no1-张无忌

        }

        Set keySet = map.keySet();//keySet集合:存放key的引用
        System.out.println(keySet.getClass());//class java.util.HashMap$KeySet
        Collection values = map.values();//values集合:存放value的引用
        System.out.println(values.getClass());//class java.util.HashMap$Values

    }
}

class Car {}
class Person {}

二、Map接口方法

  • put:添加,当put的key值已存在,则替换。
  • remove:根据键删除映射关系。
  • get:根据键获取值。如果键存在,则返回对应的值;如果键不存在,则返回null。
  • size:获取元素个数。
  • isEmpty:判断个数是否为0。
  • clear:清除。
  • containsKey:查找键是否存在。
import java.util.HashMap;
import java.util.Map;

@SuppressWarnings({"all"})
public class MapMethod {
    public static void main(String[] args) {
        //put:添加,如果 put 的 key 已存在,则替换。
        Map map = new HashMap();
        map.put("邓超", new Book("", 100));
        map.put("邓超", "孙俪");//替换
        map.put("王宝强", "马蓉");
        map.put("宋喆", "马蓉");
        map.put("刘令博", null);
        map.put(null, "刘亦菲");
        map.put("鹿晗", "关晓彤");
        System.out.println(map);//{邓超=孙俪, 宋喆=马蓉, 刘令博=null, null=刘亦菲, 王宝强=马蓉, 鹿晗=关晓彤}

        //remove:根据键删除映射关系
        map.remove(null);
        System.out.println(map);//{邓超=孙俪, 宋喆=马蓉, 刘令博=null, 王宝强=马蓉, 鹿晗=关晓彤}

        //get:根据键获取值
        //     如果键存在,则返回对应的值
        //     如果键不存在,则返回null
        Object val = map.get("鹿晗");
        System.out.println("鹿晗对应的val = " + val);//关晓彤
        Object val1 = map.get("张三");
        System.out.println("张三对应的val1 = " + val1);//null

        //size:获取元素个数
        System.out.println("k-v的数量:" + map.size());//5

        //isEmpty:判断个数是否为0
        System.out.println(map.isEmpty());//false

        //clear:清除
        map.clear();
        System.out.println(map);//{}

        //containsKey:查找键是否存在
        System.out.println(map.containsKey("hsp"));//false

    }
}

class Book {
    private String name;
    private int num;

    public Book(String name, int num) {
        this.name = name;
        this.num = num;
    }
}

三、Map六大遍历方式

获取所有的 k-v:

  • keySet     (1)增强for循环 (2)迭代器
  • entrySet  (1)增强for循环 (2)迭代器

获取所有的 value:

  • values      (1)增强for循环 (2)迭代器
import java.util.*;

@SuppressWarnings({"all"})
public class MapFor {
    public static void main(String[] args) {
        Map map = new HashMap();
        map.put("邓超", "孙俪");
        map.put("王宝强", "马蓉");
        map.put("宋喆", "马蓉");
        map.put("刘令博", null);
        map.put(null, "刘亦菲");
        map.put("鹿晗", "关晓彤");

        //第一组:通过 keySet 获取所有的 k-v
        //先取出所有的 key,再通过 key 取出对应的 value
        Set keySet = map.keySet();
        //(1)增强for循环——快捷键:iter
        System.out.println("======第1种方法======");
        for (Object key : keySet) {
            System.out.println(key + "-" + map.get(key));
        }
        //(2)迭代器
        System.out.println("======第2种方法======");
        Iterator iterator = keySet.iterator();
        while (iterator.hasNext()) {
            Object key =  iterator.next();
            System.out.println(key + "-" + map.get(key));
        }

        //第二组:通过 values 获取所有的 value
        Collection values = map.values();
        //(1)增强for循环
        System.out.println("======取出所有的value-增强for循环======");
        for (Object value : values) {
            System.out.println(value);
            /*包含2个“马蓉”。
                孙俪
                马蓉
                null
                刘亦菲
                马蓉
                关晓彤
             */
        }
        //(2)迭代器
        System.out.println("======取出所有的value-迭代器======");
        Iterator iterator1 = values.iterator();
        while (iterator1.hasNext()) {
            Object value =  iterator1.next();
            System.out.println(value);
        }

        //第三组:通过 entrySet 获取所有的 k-v
        Set entrySet = map.entrySet();//entrySet 集合中存放的元素编译类型是 Map.Entry,运行类型是 HashMap$Node
        //(1)增强for
        System.out.println("======第3种方法======");
        for (Object entry : entrySet) {
            Map.Entry m = (Map.Entry) entry;//向下转型
            System.out.println(m.getKey() + "-" + m.getValue());
        }
        //(2)迭代器
        System.out.println("======第4种方法======");
        Iterator iterator2 = entrySet.iterator();
        while (iterator2.hasNext()) {
            Object entry =  iterator2.next();
            Map.Entry m = (Map.Entry) entry;//向下转型
            System.out.println(m.getKey() + "-" + m.getValue());
        }
    }
}

四、Map课堂练习与小结

使用 HashMap 添加3个员工对象,要求:键:员工id,值:员工对象,并遍历显示工资>18000的员工(遍历方式最少2种)。员工类:姓名、工资、员工id。

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

@SuppressWarnings({"all"})
public class MapExercise {
    public static void main(String[] args) {
        Map map = new HashMap();
        map.put("001", new Employee("赵", 19000, "001"));
        map.put("002", new Employee("钱", 10000, "002"));
        map.put("003", new Employee("孙", 20000, "003"));

        //第一种方法:entrySet + 增强for循环
        Set entrySet = map.entrySet();
        System.out.println("===entrySet + 增强for循环===");
        for (Object entry : entrySet) {
            Map.Entry m = (Map.Entry) entry;
            if (((Employee)m.getValue()).getSalary() > 18000) {
                System.out.println(m.getKey() + "-" + m.getValue());
            }
        }

        //第二种方法:keySet + 迭代器
        Set keySet = map.keySet();
        System.out.println("===keySet + 迭代器===");
        Iterator iterator = keySet.iterator();
        while (iterator.hasNext()) {
            Object key =  iterator.next();
            if (((Employee)map.get(key)).getSalary() > 18000) {
                System.out.println(key + "-" + map.get(key));
            }
        }

    }
}

class Employee {
    private String name;
    private int salary;
    private String id;

    public Employee(String name, int salary, String id) {
        this.name = name;
        this.salary = salary;
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

    public int getSalary() {
        return salary;
    }

    public void setSalary(int salary) {
        this.salary = salary;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    @Override
    public String toString() {
        return "Employee{" +
                "name='" + name + '\'' +
                ", salary=" + salary +
                ", id='" + id + '\'' +
                '}';
    }
}

四、HashMap

1.介绍

  • HashMap 是 Map 接口使用频率最高的实现类。
  • HashMap 是以 k-v 的方式来存储数据的,k-v 会封装到 HashMap$Node 对象中。
  • HashMap 中 key 不允许重复,可以为 null,但 key 为 null 只能有一个。
  • HashMap 中 value 允许重复,可以为 null,value 为 null 可以有多个。
  • 如果添加相同的 key,则会覆盖原来的 value,等同于修改。
  • 与 HashSet 一样,不保证映射的顺序,因为底层是以 hash 表的方式来存储的。
  • HashMap 没有实现同步,因此是线程不安全的。

2.底层机制

JDK7.0的 HashMap 底层实现“数组+链表”,JDK8.0的底层实现“数组+链表+红黑树”。

  • HashMap 底层维护了一个 table 数组,该数组存储了 HashMap$Node 类型的元素,默认为null。
  • 初始化加载因子 loadFactor = 0.75。
  • 第一次添加元素,数组table扩容至16,临界值threshold为16*0.75=12。(0.75为加载因子loadFactor)
  • 添加元素时,先得到 key 的哈希值,再转化为在 table 的索引。然后判断该索引处是否有元素:
    • 如果没有元素直接添加;
    • 如果有元素,判断该元素的 key 和准备添加的元素的 key 是否相等:
      • 如果相等,则替换 val;
      • 如果不相等,则判断该索引处是树结构还是链表结构,做出相应处理。
  • 如果添加时元素个数 > 临界值,则进行扩容。16(12)→32(24)→64(48)→...
  • 在Java8中,如果一条链表的元素个数 >= TREEIFY_THRESHOLD(默认是8),并且table的大小 >= MIN_TREEIFY_CAPACITY(默认是64),就会进行树化(红黑树)。
import java.util.HashMap;

@SuppressWarnings({"all"})
public class HashMapSource1 {
    public static void main(String[] args) {
        HashMap hashMap = new HashMap();
        hashMap.put("java", 10);
        hashMap.put("php", 20);
        hashMap.put("java", 20);//替换

        System.out.println(hashMap);//{java=20, php=20}

        /*源码:
          (1)HashMap 构造器:初始化加载因子 loadFactor = 0.75
            public HashMap() {
                this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
            }
            此时,HashMap$Node[] table = null

          (2)对基本数据类型进行自动装箱
            public static Integer valueOf(int i) {
                if (i >= IntegerCache.low && i <= IntegerCache.high)
                    return IntegerCache.cache[i + (-IntegerCache.low)];
                return new Integer(i);
            }

          (3)put方法:该方法会执行hash(key)得到key对应的hash值
            public V put(K key, V value) {
                return putVal(hash(key), key, value, false, true);
            }

          (4)hash方法
            涉及算法(h = key.hashCode()) ^ (h >>> 16) ^按位异或 >>>无符号右移16位
            为了让不同的key尽量得到不同的hash值
            static final int hash(Object key) {//key:"java"
                int h;
                return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
            }

          (5)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;

                //如果 table 数组为 null,或者长度为0,就将 table 数组扩容到16(第一次)
                if ((tab = table) == null || (n = tab.length) == 0)
                    n = (tab = resize()).length;

                //通过 hash 值得到元素在数组 table 中的索引位置i
                //1.如果该索引处没有元素,就创建一个 Node 对象,并将该Node添加到该索引位置
                if ((p = tab[i = (n - 1) & hash]) == null)//p指向索引位置i处的Node元素
                    tab[i] = newNode(hash, key, value, null);
                //2.如果该索引处有元素:
                else {
                    //辅助变量
                    Node<K,V> e; K k;

                    //第1种情况:如果该索引位置的元素和准备添加的元素的 key 的 hash 值相同
                    //         并且满足下面两个条件之一:
                    //        (1)该索引位置的元素的 key 和准备添加的元素的 key 是同一个对象
                    //        (2)准备添加的元素的 key 的equals()方法和该索引位置的元素的 key 比较后相同(非String类对象,equals的具体实现程序员自己决定)
                    //         就不能加入
                    if (p.hash == hash &&
                        ((k = p.key) == key || (key != null && key.equals(k))))
                        e = p;

                    //第2种情况:如果该索引处是一颗红黑树,就调用putTreeVal来进行添加
                    else if (p instanceof TreeNode)
                        e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

                    //第3种情况:如果该索引位置处是一个链表,就使用for循环依次和该链表的每一个元素进行比较
                    //        (1)依次比较的过程中,如果有相同的情况,直接break
                    //        (2)依次比较后都不相同,则加入到该链表的最后
                    //         把元素添加到链表后,立即判断该链表是否已经达到8个结点
                    //         如果达到8个就调用treeifyBin()对当前这个链表进行树化(转成红黑树)
                    //         注意:在转成红黑树时,要进行判断,判断条件(table 为 null 或 table 数组的大小<MIN_TREEIFY_CAPACITY(64))
                    //         if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
                    //                 resize();
                    //         如果条件成立,先对table进行扩容
                    //         如果条件不成立,将table转成红黑树--->剪枝
                    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;//对应(2)
                            }
                            if (e.hash == hash &&
                                ((k = e.key) == key || (key != null && key.equals(k))))
                                break;//对应(1)
                            p = e;
                        }
                    }

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

                ++modCount;
                //每增加一个元素,就size++,如果 HashMap 中元素个数 > 临界值,就扩容
                if (++size > threshold)
                    resize();
                afterNodeInsertion(evict);
                return null;
            }

         */

    }
}

3.扩容机制

  • 第一次添加时,table数组扩容到16,临界值(threshold)是16*0.75=12(0.75是加载因子(loadFactor))。
  • 如果table数组使用到了临界值12,就会继续扩容到16*2=32,新的临界值是32*0.75=24。依次类推... 16(12)→32(24)→64(48)→128(96)...
  • 不管是在链表后缀接结点,还是在table新的索引位置上增加结点,都会增加table使用,size++。
  • 在Java8中,如果一条链表的元素个数 >= TREEIFY_THRESHOLD(默认是8),并且table的大小 >= MIN_TREEIFY_CAPACITY(默认是64),就会进行树化(红黑树),否则仍然采用数组扩容机制。
import java.util.HashMap;

@SuppressWarnings({"all"})
public class HashMapSource2 {
    public static void main(String[] args) {
        HashMap hashMap = new HashMap();
        for (int i = 1; i < 100; i++) {
            hashMap.put(i, "hello");
        }
        /*
        第 1 次添加元素,table 数组扩容到16,临界值为12
        table数组使用达到12,table数组再次扩容到32,临界值是24
        table数组使用达到24,table数组再次扩容到64,临界值是48
        table数组使用达到48,table数组再次扩容到128,临界值是96
        table数组使用达到96,table数组再次扩容到256,临界值是192
        ...
        注意:当向hashMap中增加一个Node,不管是添加到链表后,还是加在table的索引位置,都算是增加了一个table数组使用
         */

//        for (int i = 1; i <= 12; i++) {
//            hashMap.put(new AA(i), "hello");
//        }
        /*
        第 1 次添加元素,table 数组扩容到16,临界值为12,在索引为4的位置添加一个Node元素
        第 2-8 次添加元素,在 table 数组索引为 4 的位置的最后缀接一个结点,第 8 次添加后,table的索引为 4 的位置上:一个8结点的链表
        第 9 次添加元素,table 数组扩容到32,table 数组索引为 4 的位置上:一个9结点的链表
        第 10 次添加元素,table 数组扩容到64,table 数组索引为 36 的位置上:一个10结点的链表(位置由4变至36)
        第 11 次添加元素,树化,table 表容量不变,table 的索引为 36 的位置上:链表->红黑树
         */

        System.out.println(hashMap);
        /*
            {
            A{num=7}=hello,
            A{num=1}=hello,
            A{num=2}=hello,
            A{num=3}=hello,
            A{num=4}=hello,
            A{num=5}=hello,
            A{num=6}=hello,
            A{num=12}=hello,
            A{num=8}=hello,
            A{num=9}=hello,
            A{num=10}=hello,
            A{num=11}=hello}
         */


    }
}

class AA {
    private int num;

    public AA(int num) {
        this.num = num;
    }

    @Override
    public int hashCode() {
        return 100;
    }

    @Override
    public String toString() {
        return "\nA{" +
                "num=" + num +
                '}';
    }
}

五、Hashtable

1.介绍

  • Hashtable 存放的元素是键值对,即 K-V。
  • Hashtable 的键和值都不能为 null,否则会抛出 NullPointerException。
  • Hashtable 使用方法基本上和 HashMap 一样。
  • Hashtable 是线程安全的(synchronized),HashMap 是线程不安全的。

六、Properties

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值