集合--Map、HashMap

4.1 Map接口和常用方法

Map接口实现类的特点:

注意:这里讲的是JDK8的Map接口特点

  1. Map与Collection并列存在。用于保存具有映射关系的数据:Key-Value(双列数据)

    Map map = new HashMap();
    map.put("no1","韩顺平"); //k-v
    map.put("no1","张三丰"); //当有相同的k,就等价于替换
    
  2. Map 中的 key 和 value 可以是任何引用类型的数据,会封装 HashMap$Node 对象中

  3. Map 中的 key 不允许重复,原因和 HashSet 一样

  4. Map 中的 value 可以重复

  5. Map 的key 可以为 null,value 也可以为null,注意 key 为null,只能有一个,value 为null,可以多个

  6. 常用String类作为 Map 的 key

  7. key 和value 之间存在单向一对一关系,即通过指定的 key 总能找到对应

    //通过get方法,传入 key,会返回对应的value
    System.out.println(map.get("no1"));
    
  8. 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小结

  1. Map接口的常用实现类:HashMap、 Hashtable 和 Properties。
  2. HashMap是Map 接口使用频率最高的实现类。
  3. HashMap 是以 key-val 对的方式来存储数据 ( HashMap$Node 类型 )
  4. key 不能重复,但是值可以重复,允许使用 null 键和 null 值。
  5. 如果添加相同的key,则会覆盖原来的key-val,等同于修改.(key不会替换,val会替换)
  6. 与HashSet一样,不保证映射的顺序,因为底层是以 hash 表的方式来存储的(jdk 8 的HashMap 底层 数组+链表+红黑树)
  7. HashMap没有实现同步,因此是线程不安全的,方法没有做同步互斥的操作,没有 synchronized

4.4 HashMap底层结构和源码分析

HashMap 底层机制:

  1. (k.v)是一个Node 实现了 Map.Entry<K,V>,查看HashMap 的源码可以看到.
  2. jdk 7.0 的 hashmap 底层实现[数组+链表],jdk8.0底层[数组+链表+红黑树]

HashMap 扩容机制:(和HashSet相同)

  1. HashMap底层维护了Node类型的数组 table ,默认为null
  2. 当创建对象时,将加载因子 ( loadfactor ) 初始化为 0.75
  3. 当添加 key - val 时,通过 key 的哈希值得到在table的索引。然后判断该索引处是否有元素,如果没有元素直接添加。如果该索引处有元素,继续判断该元素的 key 是否和准备加入的 key 相等:如果相等,则直接替换 val ;如果不相等,需要判断是树结构还是链表结构,做出相应处理。如果添加时发现容量不够,则需要扩容。
  4. 第 1 次添加,则需要扩容table容量为 16,临界值(threshold)为 12
  5. 以后再扩容,则需要扩容table容量为原来的 2 倍,临界值为原来的 2 倍,即24,依次类推
  6. 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基本介绍:

  1. 存放的元素是键值对:即 K - V
  2. Hashtable 的键和值都不能为 null,否则会抛出 NullPointerException
  3. Hashtable 使用方法基本上和 HashMap 一样
  4. 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 安全 较低 不可以
posted @ 2025-08-31 21:45  lu璐  阅读(15)  评论(0)    收藏  举报