集合--Map、HashMap
4.1 Map接口和常用方法
Map接口实现类的特点:
注意:这里讲的是JDK8的Map接口特点
-
Map与Collection并列存在。用于保存具有映射关系的数据:Key-Value(双列数据)
Map map = new HashMap(); map.put("no1","韩顺平"); //k-v map.put("no1","张三丰"); //当有相同的k,就等价于替换 -
Map 中的 key 和 value 可以是任何引用类型的数据,会封装 HashMap$Node 对象中
-
Map 中的 key 不允许重复,原因和 HashSet 一样
-
Map 中的 value 可以重复
-
Map 的key 可以为 null,value 也可以为null,注意 key 为null,只能有一个,value 为null,可以多个
-
常用String类作为 Map 的 key
-
key 和value 之间存在单向一对一关系,即通过指定的 key 总能找到对应
//通过get方法,传入 key,会返回对应的value System.out.println(map.get("no1")); -
Map存放数据的 key-value 示意图,一对 k-v 是放在一个 HashMap$Node (实际存放)中的,又因为 Node 实现了 Entry 接口,有些书上也说 k-v 就是一个 Entry (为了遍历方便)
Map map = new HashMap(); map.put("no1","韩顺平"); //k-v map.put("no2","张三丰"); //k-v //1.k-v 最后是 HashMap$newNode(hash,key,value,null) //2.k-v为了方便程序员的遍历,还会创建 EntrySet 集合,该集合存放的元素类型是 Entry,而一个 Entry对象就有k,v EntrySet<Entry<K,V>>,即: transient Set<Map.Entry<K,V>> entrySet //3.entrySet 中,定义的类型是 Map.Entry,但实际上存放的还是 HashMap$Node // 这是因为 static class Node<K,V> implements Map.Entry<K,V> //4.当把 HashMap$Node 对象 存放到 entrySet 就方便我们的遍历,因为 Map.Entry 提供了重要方法 // K getKey(); V getValue(); Set set = map.entrySet(); System.out.println(set.getClass());// HashMap$EntrySet for(Object obj : set){ //System.out.println(obj.getClass()); //为了从 HashMap$Node 取出k-v //1.先做一个向下转型 Map.Entry entry = (Map.Entry) obj; System.out.println(entry.getKey() + "-" + entry.getValue() ); }
Map接口常用方法:
-
put:添加
-
remove:根据键删除映射关系
-
get:根据键获取值
-
size:获取元素个数
Map map = new HashMap(); map.size(); -
isEmpty:判断个数是否为0
-
clear:清除
-
containsKey:查找键是否存在
4.2 Map接口遍历元素方式
-
containsKey:查找键是否存在
-
keySet:获取所有的键
Map map = new HashMap(); //先取出所有的 key ,再通过 key 取出对应的 value Set keyset = map.keySet(); //(1)第一种方式:增强 for for(Object key : ketset){ System.out.println(key + "-" + map.get(key)); } //(2)第二种方式:迭代器 Iterator iterator = keyset.iterator(); while(iterator.hasNext()){ Object key = iterator.next(); System.out.println(key); } -
entrySet:获取所有的关系
Map map = new HashMap(); //通过 EntrySet 来获取 k-v Set entrySet = map.entrySet();// EntrySet<Map.Entry<K,V>> //(1)增强for for (Object entry : entrySet) { //将entry 转成 Map.Entry Map.Entry m = (Map.Entry) entry; System.out.println(m.getKey() + m.getValue()); //首先通过map中的entrySet方法将K-V封装成Entry,使用增强for循环遍历,需要将接收的entrySet向下转型为Map.Entry(因为Map.entry能够使用get方法获取K-v) } //(2)迭代器 Iterator iterator3 = entrySet.iterator(); while (iterator3.hasNext()){ Object entry= iterator3.next(); //System.out.println(next.getctass()); //HashHap$Node -实现-> Map.Entry (getKev,getvalue) //向下转型 Map.Entry(Node没有对应方法) Map.Entry m = (Map.Entry) entry; System.out.println(m.getKey() + "-" + m.getValue()); /* 编译阶段:编译器检查 entry 的编译时类型 Map.Entry。它发现 Map.Entry 接口里声明了 getKey() 方法,所以语法正确,允许通过。 运行阶段:JVM 找到 entry 变量实际指向的堆内存中的 HashMap$Node 对象,然后调用这个对象所属类(HashMap.Node)的 getKey() 方法。*/ } -
values:获取所有的值
Map map = new HashMap(); //把所有的 values 取出来 Collection values = map.values(); //这里可以使用所有Collection使用的遍历方式 //(1)增强for for (Object value : values) { System.out.println(value); } //(2)选代器 Iterator iterator2 = values.iterator(); while (iterator2.hasNext()) { Object value = iterator2.next(); System.out.println(value); }
练习:
使用HashMap添加3个员工对象,要求
键:员工id
值:员工对象
并遍历显示工资>18000的员工(遍历方式最少两种)
员工类:姓名、工资、员工id
public static MapExercise{
public static void main(String[] args){
//完成代码
Map hashMap = new HashMap();
hashMap.put(1, new Emp("jack", 300000, 1));
hashMap.put(2, new Emp("tom", 2300, 2));
hashMap.put(3, new Emp("john", 5000, 3));
//遍历
//并遍历显示工资>18000的员工(遍历方式最少两种)
//1.使用keySet -> 增强for
Set keySet = hashMap.keySet();
for (Object key : keySet) {
//先获取value
Emp emp =(Emp) hashMap.get(key);
if(emp.getSal() > 18000){
System.out.println(emp);
}
}
//2.使用EntrySet -> 迭代器
Set entrySet = hashMap.entrySet();
Iterator iterator = entrySet.iterator();
while (iterator.hasNext()){
Map.Entry entry = (Map.Entry)iterator.next();
//通过entry 取得key 和 value
Emp emp = (Emp) entry.getValue();
if(emp.getSal() > 18000){
System.out.println(emp);
}
}
}
class Emp{
private String name;
private double sal;
private int id;
public Emp(String name, double sal, int id){
this.name = name;
this.sal = sal;
this.id = id;
}
//get/set,toString省略
}
4.3 HashMap小结
- Map接口的常用实现类:HashMap、 Hashtable 和 Properties。
- HashMap是Map 接口使用频率最高的实现类。
- HashMap 是以 key-val 对的方式来存储数据 ( HashMap$Node 类型 )
- key 不能重复,但是值可以重复,允许使用 null 键和 null 值。
- 如果添加相同的key,则会覆盖原来的key-val,等同于修改.(key不会替换,val会替换)
- 与HashSet一样,不保证映射的顺序,因为底层是以 hash 表的方式来存储的(jdk 8 的HashMap 底层 数组+链表+红黑树)
- HashMap没有实现同步,因此是线程不安全的,方法没有做同步互斥的操作,没有 synchronized
4.4 HashMap底层结构和源码分析
HashMap 底层机制:
- (k.v)是一个Node 实现了 Map.Entry<K,V>,查看HashMap 的源码可以看到.
- jdk 7.0 的 hashmap 底层实现[数组+链表],jdk8.0底层[数组+链表+红黑树]
HashMap 扩容机制:(和HashSet相同)
- HashMap底层维护了Node类型的数组 table ,默认为null
- 当创建对象时,将加载因子 ( loadfactor ) 初始化为 0.75
- 当添加 key - val 时,通过 key 的哈希值得到在table的索引。然后判断该索引处是否有元素,如果没有元素直接添加。如果该索引处有元素,继续判断该元素的 key 是否和准备加入的 key 相等:如果相等,则直接替换 val ;如果不相等,需要判断是树结构还是链表结构,做出相应处理。如果添加时发现容量不够,则需要扩容。
- 第 1 次添加,则需要扩容table容量为 16,临界值(threshold)为 12
- 以后再扩容,则需要扩容table容量为原来的 2 倍,临界值为原来的 2 倍,即24,依次类推
- 在 Java 8 中,如果一条链表的元素个数超过 TREEIFY _ THRESHOLD ( 默认是8 ),并且 table 的大小 >= MIN _ TREEIFY _ CAPACITY ( 默认64 ),就会进行树化 ( 红黑树 )
HashMap 源码解读:
Map hashMap = new HashMap();
map.put("java",10);
map.put("php",10);
map.put("java",20);
/*解读 + 图解:
1.执行构造器
初始化加载因子 loadfactor = 0.75
HashMap$Node[] table = null;
2.执行 put方法 调用 hash 方法,计算 key 的 hash值(h = key.hashCode()) ^ (h >>> 16)
public V put(K key, V value){ //k = "java" v = 10
return putVal(hash(key), key, value, false, true);
}
3.执行 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,或者 length = 0,就扩容到16
if((tab = table) == null || (n = tab.length) == 0)
n =(tab = resize()).length;
//取出 hash 值对应的 table 表的索引位置的 Node,如果为 null,就直接把加入的 k-v,创建成一个 Node ,加入到该位置即可
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e;K k;
////如果table 的索引位置的key 的hash相同和新的key的hash值相同
//并 满足(table现有的结点的key和准备添加的key是同一个对象 || equals 返回真)
//就认为不能加入新的k-v
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e=p;
else if (p instanceof TreeNode) //如果当前的table的已有的Node 是红黑树,就按照红黑树的方式处理
e=((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//如果找到的结点,后面是链表,就循环比较
for (int binCount = θ; ; ++binCount) {//死循环
if ((e = p.next)== null) { //如果整个链表,没有和它相同,就加到该链表的最后
p.next = newNode(hash,key, value, null);
//加入后,判断当前链表的个数,是否已经到8个,到8个后
//就调用 treeifyBin 方法进行红黑树的转换
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if(e.hash == hash && //如果在循环比较过程中,发现有相同,就break,只是替换value
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oLdValue == null)
e.value= value; //替换,key对应的value
afterNodeAccess(e);
return oldValue;
}
++modCount; //每增加一个Node,就size++
if (++size > threshold) //如果size > 临界值,就扩容
resize();
afterNodeInsertion(evict);
return null;
}
5.关于树化
//如果table 为null,或者大小还没有到64,暂时不树化,而是进行扩容
//否则才会真正的树化 -> 剪枝
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
}
*/
HashMap 扩容树化触发:
public class HashMapSource{
public static void main(String[] args){
HashMap hashMap = new HashMap();
for(int i=1; i<=12;1++){
hashMap.put(new A(i),"hello");
}
System.out.println(hashMap); //12个k-v
}
}
class A{
private int num;
public A(int num){
this.num = num;
}
//所有的A对象的hashCode都是100,hash值相同代表挂在同一个位置
@override
public int hashCode() {
return 100;
}
}
//循环到9时,table扩容但不树化(单条链表到达8,但是table大小没到64)
//大小到64后树化
4.5 Hashtable底层结构和源码分析
Hashtable基本介绍:
- 存放的元素是键值对:即 K - V
- Hashtable 的键和值都不能为 null,否则会抛出 NullPointerException
- Hashtable 使用方法基本上和 HashMap 一样
- Hashtable 是线程安全的,HashMap 是线程不安全的
HashTable底层结构:
Hashtable table = new Hashtable();
table.put("john",100);
table.put(null, 100);//异常
table.put("john", null);//异常 NuLLPointerException
table.put("Lucy",100);//ok
table.put("uic",100);
table.put("uic",88);//替换
//1.底层有数组 Hashtable$Entry[] 初始化大小为 11
//2.临界值 threshold = 数组大小 * 0.75
//3.扩容:按自己的机制
HashTable扩容机制:
/*
4.执行方法 addEntry(hash, key value, index);添加K-V封装到Entry
5.当 if(count >= threshold) 满足时就进行扩容
6.按照 int newCapacity = (oldCapacity << 1) + 1 的大小扩容,即 原大小* 2 + 1
*/
HashTable 和 HashMap 对比:
| 版本 | 线程安全(同步) | 效率 | 允许 null键 null值 | |
|---|---|---|---|---|
| HashMap | 1.2 | 不安全 | 高 | 可以 |
| Hashtable | 1.0 | 安全 | 较低 | 不可以 |

浙公网安备 33010602011771号