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.EntryMap.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 -> 增强forSet keySet = hashMap.keySet();for (Object key : keySet) {//先获取valueEmp 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 和 valueEmp 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.75HashMap$Node[] table = null;
2.执行 put方法	调用 hash 方法,计算 key 的 hash值(h = key.hashCode()) ^ (h >>> 16)public V put(K key, V value){ //k = "java" v = 10return putVal(hash(key), key, value, false, true);}
3.执行 putValfinal 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,就扩容到16if((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-vif (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 1sttreeifyBin(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 keyV oldValue = e.value;if (!onlyIfAbsent || oLdValue == null)e.value= value; //替换,key对应的valueafterNodeAccess(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值相同代表挂在同一个位置@overridepublic 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 | 安全 | 较低 | 不可以 | 
