打怪升级之小白的大数据之旅(二十一)
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的存储原理
- 计算元素的存储位置
计算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的基础底子,为大数据之旅做好铺垫。好了,欢迎大家后台留言、吐槽。