一、Map接口特点
Set接口和Map接口的联系与区别:
- Set底层存放的是:K-V,K:输入的要存放的对象,V:PRESENT常量。
- Map存放的是:K-V,K和V:输入的要存放的对象。
Map接口实现类的特点:(这里讲的是JDK8的Map接口特点)
- Map和Collection并列存在,用于保存具有映射关系的数据:Key-Value。
- Map中的Key和Value可以是任何引用数据类型的数据,会封装到HashMap$Node对象中。
- Map 中的 Key 不允许重复,可以为 null,但 key 为 null 只能有一个。
- Map 中的 Value 可以重复,可以为 null,value 为 null 可以有多个。
- 常用 String 类作为 Map 的 key。
- 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 是线程不安全的。