打怪升级之小白的大数据之旅(二十一)<Java面向对象进阶之集合三Map集合>

打怪升级之小白的大数据之旅(二十一)

Java面向对象进阶之集合三Map集合

上次回顾

上一章我们对整个Collection进行了学习,本章是集合的最终章,今天的内容会让大家有很多熟悉感,这是为什么呢?卖个关子,请听我细细讲来…

Map集合

概述

  • Map是集合框架两大根接口的另外一个,它的特点是键值对,即它存储的对象是以键(key)和值(value)的映射关系来对这种方式进行数据存储
  • 在现实生活中,我们也经常遇到键值对映射关系的集合:身份证号码与个人的关系,账号和密码的关系,这种一一对应的关系就叫做映射,在Java中存储这种映射关系数据的集合就是Map集合
  • 我来举个栗子说明一下它和Collection的区别:在这里插入图片描述

Map与Collection的区别

  • Collection中的集合,元素是孤立存在的(理解为单身),向集合中存储元素采用一个个元素的方式存储。
  • Map中的集合,元素是成对存在的(理解为夫妻)。每个元素由键与值两部分组成,通过键可以找对所对应的值。
  • Collection中的集合称为单列集合,Map中的集合称为双列集合。
  • 需要注意的是,Map中的集合不能包含重复的键,值可以重复;每个键只能对应一个值(这个值可以是单个值,也可以是个数组或集合值)

Map常用方法

1、添加操作

  • V put(K key,V value)
  • void putAll(Map<? extends K,? extends V> m)

2、删除

  • void clear()
  • V remove(Object key)

3、元素查询的操作

  • V get(Object key)
  • boolean containsKey(Object key)
  • boolean containsValue(Object value)
  • boolean isEmpty()

4、元视图操作的方法:

  • Set keySet()
  • Collection values()
  • Set<Map.Entry<K,V>> entrySet()

5、其他方法

  • int size()

老样子,举个例子演示一下Map集合的方法使用:

public class MapDemo {
    public static void main(String[] args) {
        //创建 map对象
        HashMap<String, String>  map = new HashMap<String, String>();

        //添加元素到集合
        map.put("黄晓明", "杨颖");
        map.put("吴京", "谢楠");
        map.put("邓超", "孙俪");
        System.out.println(map);

        //String remove(String key)
        System.out.println(map.remove("邓超"));
        System.out.println(map);

        // 想要查看 黄晓明的媳妇 是谁
        System.out.println(map.get("黄晓明"));
        System.out.println(map.get("邓超"));    
    }
}
  • 注:
    • 使用put方法时,若指定的键(key)在集合中没有,则没有这个键对应的值,返回null,并把指定的键值添加到集合中;

    • 若指定的键(key)在集合中存在,则返回值为集合中键对应的值(该值为替换前的值),并把指定键所对应的值,替换成指定的新值

Map集合的遍历

  • Collection集合的遍历是通过增强for和itearator进行遍历,但是Map集合不能使用它们进行遍历,因为Map接口没有继承java.lang.iterable接口,所以它没有实现iterator()的方法
  • 对Map集合遍历应该怎么做呢?有两种做法:
    • 分开遍历:第一步,单独遍历所有的key,然后根据key再遍历所有的value
    • 成对遍历:将键值对这个数据当做一个整体进行遍历
  • 分开遍历比较简单,待我讲完成对遍历的原理,我会举一个例子来展示这两种遍历在这里插入图片描述
  • 在Map接口内部,有一个内部的接口(忘了的可以回看内部类那一章),这个内部接口就是Map.Entry,每一种Map内部都有自己的Map.Entry的实现类
  • 当我们在Map中存储数据时,它的数据实际上是存储在Map.Entry接口的实例中,然后在Map集合中插入Map.Entry的实例化对象
  • 有一点绕,我们把它当做前面讲的LinkedList中的Node节点概念来理解,Map集合存储的键值对是一个整体,这个整体就是Map.Entry
  • Map遍历示例代码:
    import java.util.Collection;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.Set;
    
    /*
    * 集合的遍历
    * */
    public class Demo3 {
    
            public static void main(String[] args) {
                // 创建HashMap集合
                HashMap<String,String> map = new HashMap<>();
                // 添加数据
                map.put("许仙", "白娘子");
                map.put("董永", "七仙女");
                map.put("牛郎", "织女");
                map.put("许仙", "小青");
    
                // 遍历方式一,分开遍历
                // 获取所有的key
                System.out.println("所有的key:");
                Set<String> keySet = map.keySet();
                for (String key : keySet) {
                    System.out.println(key);
                }
                // 获取所有的值
                System.out.println("所有的value:");
                Collection<String> values = map.values();
                for (String value : values) {
                    System.out.println(value);
                }
                System.out.println("---------------------------------");
    
                // 遍历方式一,利用Key来获取所有的映射关系
                Set<String> keys = map.keySet();
                for (String key : keys) {
                    System.out.println(key +"=="+map.get(key));
                }
                System.out.println("---------------------------------");
                System.out.println("所有的映射关系");
                Set<Map.Entry<String,String>> entrySet = map.entrySet();
                for (Map.Entry<String,String> entry : entrySet) {
    //			System.out.println(entry);
                    System.out.println(entry.getKey()+"->"+entry.getValue());
                }
            }
    
    }
    

Map的实现类

Map接口的常用实现类:HashMap、TreeMap、LinkedHashMap和Properties。其中HashMap是 Map 接口使用频率最高的实现类

HashMap

  • HashMap的底层结构:哈希=数组+链表+红黑树(jdk8之后)
  • 特点是Key唯一,无序,并且Key和Vlaue都可以为null
  • Hashmap的增删改查综合效率高于单纯的数组、链表
HashMap的构造方法
// 空参构造,没有初始容量,第一次添加元素时,初始容量为16
HashMap map = new HashMap();
// 指定初始容量的构造,得到的初始容量一定是2的n次幂(比指定值大的最小的2的次幂)
HashMap map = new HashMap(int capacity);
HashMap的常用方法
  • 作为Map最经常使用的实现类,它的地位就如同ArrayList在List中的地位一样,前面Map的案例就是使用HashMap
  • 因此我再举个案例就过了
    package com.test01Set;
    
    import javax.naming.NamingEnumeration;
    import java.util.*;
    
    /*
    * 练习
    * 创建Map集合
    * 存储
    * key   学生对象Student
    * value 手机对象CellPhone
    * 遍历整个Map集合
    *
    * */
    public class Demo2 {
        public static void main(String[] args) {
            // 定义一个手机袋,用于存放学生手机
            Map<Student,CellPhone> phoneData = new HashMap<>();
            phoneData.put(new Student("张三"),new CellPhone("华为mate40pro","夏日胡杨",7800));
            phoneData.put(new Student("李四"),new CellPhone("iphonexs","黑色",8500));
            phoneData.put(new Student("王五"),new CellPhone("小米10","白色",6300));
            phoneData.put(new Student("赵六"),new CellPhone("一加8T","黑色",6000));
            phoneData.put(new Student("田七"),new CellPhone("黑鲨手机","银色",5666));
    
            // 当key是一个自定义对象时,添加重复数据不会去重,同样需要重新hashcode和equals
            phoneData.put(new Student("田七"),new CellPhone("黑鲨手机","银色",5666));
    
    
            // 利用entry遍历整个手机袋
            Set<Map.Entry<Student, CellPhone>> entries = phoneData.entrySet();
            for (Map.Entry<Student, CellPhone> entry : entries) {
                System.out.println(entry);
    
            }
            System.out.println("-------------------------");
            // 遍历方式二,利用keySet进行遍历
            Set<Student> students = phoneData.keySet();
            for (Student student : students) {
                System.out.println(student+" "+phoneData.get(student) );
            }
    
        }
    }
    
    
    
    // 创建学生类
    class Student{
        // 定义属性
        private String name;
        // 定义构造器
        public Student(String name) {
            this.name = name;
        }
        // 定义getter/setter方法
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        // 重写tostring
    
        @Override
        public String toString() {
            return "姓名:" + name;
        }
    
        // 重写equals
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Student student = (Student) o;
            return Objects.equals(name, student.name);
        }
        // 重写hashcode
        @Override
        public int hashCode() {
            return Objects.hash(name);
        }
    }
    
    // 创建手机类
    class CellPhone{
        // 定义属性
        private String brand;
        private String Color;
        private double price;
    
        // 定义构造器
    
        public CellPhone(String brand, String color, double price) {
            this.brand = brand;
            Color = color;
            this.price = price;
        }
    
        // 定义getter/setter方法
    
        public String getBrand() {
            return brand;
        }
    
        public void setBrand(String brand) {
            this.brand = brand;
        }
    
        public String getColor() {
            return Color;
        }
    
        public void setColor(String color) {
            Color = color;
        }
    
        public double getPrice() {
            return price;
        }
    
        public void setPrice(double price) {
            this.price = price;
        }
    
        // 重写toString
    
        @Override
        public String toString() {
            return "手机信息: {" +
                    "品牌:'" + brand + '\'' +
                    ", 颜色:'" + Color + '\'' +
                    ", 价格:" + price +
                    '}';
        }
    }
    

LinkedHashMap

  • LinkedHashMap 是 HashMap 的子类
  • LinkedHashMap就如同LinkedHashSet一样,它在内部维护着一个运行于所有条目的双重链接列表。此链接列表定义了迭代顺序,该迭代顺序通常就是将键插入到映射中的顺序(插入顺序)
  • 同样的,它的主要作用就是,我们书写的时候什么顺序,它输出的顺序,也是什么样
  • 示例代码:
    public static void main(String[] args) {
    		LinkedHashMap<String,Double> map = new LinkedHashMap<>();
    		map.put("张三", 10000.0);
    		//key相同,新的value会覆盖原来的value
    		//因为String重写了hashCode和equals方法
    		map.put("张三", 12000.0);
    		map.put("李四", 14000.0);
    		//HashMap支持key和value为null值
    		String name = null;
    		Double salary = null;
    		map.put(name, salary);
    		
    		Set<Entry<String, Double>> entrySet = map.entrySet();
    		for (Entry<String, Double> entry : entrySet) {
    			System.out.println(entry);
    		}
    	}
    

TreeMap

  • TreeMap=Map集合+红黑树,是不是感觉今天的Map特别熟悉?跟昨天的Set简直像是一个模子出来的?等下将实现类介绍完,我会告诉大家原因
  • TreeMap跟TreeSet的作用一样,它的作用就是排序,当然了,它的自定义对象也必须能比较大小,它的自然排序方式也是继承自Comparable方法,我们也可以使用Comparator接口进行自定义排序
  • 没有太多特殊的地方了,具体的昨天TreeSet讲过了,直接上示例代码:
    package com.test01Set;
    import java.util.Comparator;
    import java.util.Map.Entry;
    import java.util.Set;
    import java.util.TreeMap;
    
    import org.junit.Test;
    public class Demo4 {
            @Test
            public void test1() {
                TreeMap<String,Integer> map = new TreeMap<>();
                map.put("Jack", 11000);
                map.put("Alice", 12000);
                map.put("zhangsan", 13000);
                map.put("baitao", 14000);
                map.put("Lucy", 15000);
    
                //String实现了Comparable接口,默认按照Unicode编码值排序
                Set<Entry<String, Integer>> entrySet = map.entrySet();
                for (Entry<String, Integer> entry : entrySet) {
                    System.out.println(entry);
                }
            }
            @Test
            public void test2() {
                //指定定制比较器Comparator,按照Unicode编码值排序,但是忽略大小写
                TreeMap<String,Integer> map = new TreeMap<>(new Comparator<String>() {
                    @Override
                    public int compare(String o1, String o2) {
                        return o1.compareToIgnoreCase(o2);
                    }
                });
                map.put("Jack", 11000);
                map.put("Alice", 12000);
                map.put("zhangsan", 13000);
                map.put("baitao", 14000);
                map.put("Lucy", 15000);
    
                Set<Entry<String, Integer>> entrySet = map.entrySet();
                for (Entry<String, Integer> entry : entrySet) {
                    System.out.println(entry);
            }
        }
    }
    

Hashtable

  • HashMap和Hashtable都是哈希表实现的
  • Hashtable就像List集合中的ArrayList和Vector一样,主要用于对比二者的关系会提到,用的很少
  • HashMap和Hashtable判断两个 key 相等的标准是:两个 key 的hashCode 值相等,并且 equals() 方法也返回 true。因此,为了成功地在哈希表中存储和获取对象,用作键的对象必须实现 hashCode 方法和 equals 方法
  • Hashtable是线程安全的,因此它的效率低于HashMap,任何非 null 对象都可以用作键或值
  • HashMap是线程不安全的,并允许使用 null 值和 null 键

Properties

  • Properties 类是 Hashtable 的子类,因为Hashtable线程安全…Properties 可保存在流中或从流中加载。属性列表中每个键及其对应值都是一个字符串
  • 一般用的不太多,了解一下就好,存取数据时,建议使用setProperty(String key,String value)方法和getProperty(String key)方法
  • 它的主要作用就是操作系统,我在其他核心类那一章介绍System时使用过该类的方法:
	public static void main(String[] args) {
		Properties properties = System.getProperties();
		String p2 = properties.getProperty("file.encoding");//当前源文件字符编码
		System.out.println(p2);
	}

Set集合与Map集合的关系

我来填一下昨天的坑,为什么Set集合中,HashSet的add方法底层会使用map的put呢?

  • 答案很简单,Set的内部实现就是一个Map即HashSet的内部实现是一个HashMap
  • 我摘一下Set的源码让大家清楚的了解这一点:

HashSet源码

public HashSet() {
        map = new HashMap<>();
    }

    public HashSet(Collection<? extends E> c) {
        map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
        addAll(c);
    }

    public HashSet(int initialCapacity, float loadFactor) {
        map = new HashMap<>(initialCapacity, loadFactor);
    }

    public HashSet(int initialCapacity) {
        map = new HashMap<>(initialCapacity);
    }

	//这个构造器是给子类LinkedHashSet调用的
    HashSet(int initialCapacity, float loadFactor, boolean dummy) {
        map = new LinkedHashMap<>(initialCapacity, loadFactor);
    }

LinkedHashSet源码

public LinkedHashSet(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor, true);//调用HashSet的某个构造器
    }

    public LinkedHashSet(int initialCapacity) {
        super(initialCapacity, .75f, true);//调用HashSet的某个构造器
    }

    public LinkedHashSet() {
        super(16, .75f, true);
    }

    public LinkedHashSet(Collection<? extends E> c) {
        super(Math.max(2*c.size(), 11), .75f, true);//调用HashSet的某个构造器
        addAll(c);
    }

TreeSet源码

public TreeSet() {
        this(new TreeMap<E,Object>());
    }

    public TreeSet(Comparator<? super E> comparator) {
        this(new TreeMap<>(comparator));
    }

    public TreeSet(Collection<? extends E> c) {
        this();
        addAll(c);
    }

    public TreeSet(SortedSet<E> s) {
        this(s.comparator());
        addAll(s);
    }
  • 这下真相大白了吧,我们的这个熟悉感就是因为Set的底层就是Map,但我们本章开篇说集合和Collection时说过,Set中只有一个元素,它是单身汉,而Map是一个键值对是情侣,它是怎么做的呢?我们以HashSet中的源码举例:
    private static final Object PRESENT = new Object();
    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }
    public Iterator<E> iterator() {
        return map.keySet().iterator();
    }
    
  • 原来是,把添加到Set中的元素作为内部实现map的key,然后用一个常量对象PRESENT对象,作为value。
  • 这是因为Set的元素不可重复和Map的key不可重复有相同特点。Map有一个方法keySet()可以返回所有key。

Hash表详细结构与HashMap源码分析

了解清楚了Set与Map的关系,接下来,我就详细讲解一下Hash表和HashMap的源码,主要目的,同样是为了让大家更便于理解它们
在这里插入图片描述
HashMa中的哈希表在jdk1.8之后,变为红黑树版的哈希表,哈希表和红黑树我在上一章为大家介绍了,这里就不在重复讲解了,它变为红黑树的原理就是对链表中的数据进行自动判断,超过设定的阈值会自动升为红黑树或退化为链表

HashMap的存储原理

  1. 计算元素的存储位置
    计算key的hashCode值,进行高低位16位异或运算,再对底层数组长度取模运算,得到元素存储的索引位置
    2.首先根据索引位置进行数据添加,然后判断是链表还是红黑树,没有重复则进行元素的添加,如果是链表,在链表尾部插入
    3.通过equals比较key是否相同,如果相同则覆盖旧的key,并且返回旧vale

HashMap的扩容原理
1.创建HashMap的时候没有容量
2.当添加第一个元素时,初始化容量为16
3.当元素个数超过扩容阈值 threshold=加载因子loadFactor(0.75)*容量capacity时,进行扩容,库容的新容量为旧容量的2倍
4. 我们将每个索引位置的链条数据想象为一个桶型的数据。当这个桶中的元素个数(链条元素个数)大于8时,并且整个容量(可以理解为横向的那个数组)小于64时,就会进行扩容,并且将数据进行分散
5.当整个容量超过64时,则会将桶中的链表转为红黑树。当红黑树的节点数<=6时,会退化为链表

Collections工具类

  • Collections可以参考数组的工具类Arrays
  • Collections 是一个操作 Set、List 和 Map 等集合的工具类。Collections 中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法
  • Collection的常用方法

再次回顾集合框架

在这里插入图片描述
好了,整个集合已经全部介绍完毕了,我们现在再来回顾这张框架图

  • 首先,对于单列数据,我们存储的时候就选用Collection中的实现类,当不需要对数据去重时,通常使用List集合,并且根据需求,查询效率要求高,就使用ArrayList,对增删效率要求高就使用LinkedList
  • 需要去重,就使用Set集合,通常使用HashSet,因为Set默认无序,所以我们需要让它安装我们书写顺序输出,就使用LInkedHashSet,当我们需要对数据进行排序,就需要使用TreeSet,进行对象排序,默认内部是自然排序,继承了Comparable,正因如此,我们无法对自定义对象进行排序,所以需要重写Comparable或者自定义一个Comparator
  • 因为是单列数据,所以Collection内部有一个迭代器,方便我们对其进行遍历,而因为List使用的次数更多,所以专门实现了一个针对List的迭代器Listiterator
  • Map与Collection的区别就是它是双列数据,存储的键值对,因为Set的底层是Map,它们的底层结构都是哈希表,所以它们有很多共通之处,比如排序的实现方式等。了解了Set各个实现类原理,触类旁通,就懂了Map各个实现类的底层原理
  • Collections工具类就如同Arrays专门服务数组一样,Collections专门服务集合。里面封装了各种操作集合的方法
  • 最后我总结了集合常用的API供大家参考在这里插入图片描述

总结

本章是集合系列的终章,总结我前面已经写了,就不赘述了,下一章,我将为大家带来泛型的学习。java的知识点也即将进入尾声了,泛型、IO流、网络编程、多线程,学完这些,我就正式开始为大家带来大数据系列的相关教程。希望大家可以打好java的基础底子,为大数据之旅做好铺垫。好了,欢迎大家后台留言、吐槽。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值