Java-Day-18( Map 接口、各实现类 )
Java-Day-18
Map
接口
- Map 存放是 K - V ( 双列 ) 元素,K 和 V 都是输入的具体的对象
- Set 也是 K - V 键值对的形式,只不过除了 K 都是表示值,V 是用常量 PRESENT 来替代的
-
Map 接口实现类的特点 ( 这里讲的是 JDK8 的接口特点 )
-
Map 与 Collection 并列存在 ( 两大类之间无关 )。用于保存具有具有映射关系的数据:Key-Value
Map map = new HashMap(); map.put("no1", "zyz"); map.put("no2", "duang"); System.out.println("map=" + map); // map={no2=duang, no1=zyz},仍是无序,key 实际还是 hash,仍是0,1,2...里计算的索引处存放着:no1=zyz
-
Map 中的 key 和 value 可以是任何引用类型的数据,会封装到 HashMap$Node 对象中 ( 同 Set 讲过的 )
-
Map 中的 key ( K ) 不允许重复 ( 原因和 HashSet 一样 ),如果重复了就等价于整体替换旧的
-
Map 中的 value ( V ) 是可以重复的
- 类似于 K - V 是楼内,门牌号 K 是不会有重复的,但里面住所的装饰 V 是可以相同的
-
Map 中的 key、value 都可以为 null,但 key 仍只能有一个 null,value 可多个
-
常用 String ( 要求是 Object 就可 ) 来作为 Map 的 key
-
key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到对应的 value
System.out.println(map.get("no2")); // duang (传key得value)
-
Map 存放数据:一对 key - value 是存放在一个 Node 中的 ( HashMap$Node:静态的,存有 hash、key、value、next 的 Node ),又因为 Node 实现了 Entry 接口,有些书上也说一对 k-v 就是一个 Entry
-
但 Entry 里可理解为含 Set 和 Collection 两列分别存 k 和 v 的集合,实际只是简单的引用,还是指向的 Node,只不过为了便于程序员遍历,总和成了 EntrySet 集合
transient Set<Map.Entry<K,V>> entrySet;
存放 Entry 元素类型 (EntrySet<Entry<K, V>>
)// 这里举例用HashMap来实现Map接口 Map map = new HashMap(); map.put("no1", "zyz"); map.put("no2", "duang"); map.put(null, "dongdongdong"); // getclass查看运行类型 Set set = map.entrySet(); System.out.println(set.getClass()); // class java.util.HashMap$EntrySet for (Object obj : set) { // 向下转型 Map.Entry entry = (Map.Entry) obj; System.out.println(entry.getKey() + "-" + entry.getValue()); // 内含get方法,便于程序员遍历 } // Map接口定义了返回值:Set<K> keySet(); 和 Collection<V> values(); Set set1 = map.keySet(); System.out.println(set1.getClass()); // class java.util.HashMap$KeySet Collection values = map.values(); System.out.println(values.getClass()); // class java.util.HashMap$Values
-
EntrySet 定义的类型是 Map.Entry
Set<Map.Entry<K, V>> entrySet();
,但是实际上存放的还是 HashMap$Node,因为是 Node 实现了 Entry (HashMap$Node implements Map.Entry
,因此 Node 可以扔进 Entry 里 )注意,entry 只是个接口而已,node 来实现它,并不是 node 带着数存进 entryset,而是带有 node 的数据信息的 entry 存进了 entryset
-
debug 可知 entrySet 里就是多个简单的指向,存的是真正数据的地址,并不是数据
-
HashMap implements Map,Map 实际上就是有一个 table 表,存有 node,这个表由数组 + 链表 + 红黑树,并为了方便管理做了一个控制,将 node 封装成了一个 entry,组合形成后放进集合 entrySet 里,又为了方便使用,通过 set 方法将 key 都给放进了集合的 KeySet ( Set ) 里,将 value 都放进了 Values ( Collection ) 里,这样就可以用 map 的 entrySet 取出 entry,再用 getKey 和 getValue 操作了
-
简例:
- 一个学生 ( 数据 ) 有学号 k,姓名 v,前后桌等基本信息 ( Node )
- 老师把基本信息 ( Node ) 做成电子名片 ( Entry ) 的形式 ( 实际还是 Node )
- 然后把电子名片 ( Entry ) 通过 entrySet() 方法,记录在学生教育系统 ( EntrySet ) 中
- 这样做,是因为学生教育系统 ( Set 接口类型 ) 有遍历电子名片的功能 ( 迭代器 )
- 电子名片 ( Entry ) 有 getKey() 和 getValue() 功能就可以方便查询学生的学号 k 和姓名 v 了
-
-
常用方法
-
Map 接口的常用方法
Map map = new HashMap(); // 添加 put map.put("no1", "zyz"); map.put("no2", "duang"); // 根据键删除映射关系 map.remove("no1"); System.out.println(map); // 根据键获取值 Object val = map.get("no2"); System.out.println(map); // 获取元素的个数 System.out.println("k-v 有" + map.size() + "个"); // k-v 有1个 // 判断个数是否为空 System.out.println(map.isEmpty()); //false // 清除k-v map.clear(); System.out.println("map = " + map); // map = {} map.put("1", "dongdongdong"); // 查找键是否存在 System.out.println(map.containsKey("1")); // true
-
Map 接口的遍历方法
public static void main(String[] args) { Map map = new HashMap(); // 添加 put map.put("no1", "zyz"); map.put("no2", "duang"); map.put(null, "dongdongdong"); // 第一组 Set keyset = map.keySet(); // 存放所有key的集合 // 1. 增强 for 循环 System.out.println("通过key取到值的方法一:"); for (Object key : keyset) { System.out.println(key + "-" + map.get(key)); } // 2. 迭代器 Iterator iterator = keyset.iterator(); System.out.println("通过key取到值的方法二:"); while (iterator.hasNext()) { Object key = iterator.next(); System.out.println(key + "-" + map.get(key)); } System.out.println("****************上为EntrySet里的KeySet***************"); System.out.println("****************下为EntrySet里的Values***************"); // 第二组 Collection values = map.values(); // 存放所有value的集合 // 1. 增强 for 循环 System.out.println("方法一的直接取值"); for (Object val : values) { System.out.println(val); } // 2. 迭代器 Iterator iterator2 = keyset.iterator(); System.out.println("方法二的直接取值"); while (iterator2.hasNext()) { Object val = iterator2.next(); System.out.println(map.get(val)); } // 第三组 System.out.println("**********下为通过EntrySet来获取 k-v"); Set entrySet = map.entrySet(); // EntrySet<Map.Entry<K,V>> // 1. 增强 for 循环 System.out.println("方法一"); for (Object entry : entrySet) { // 向下转型掉 Object 为 Map.Entry Map.Entry m = (Map.Entry) entry; System.out.println(m.getKey() + "-" + m.getValue()); } // 2. 迭代器 System.out.println("方法二"); Iterator iterator3 = entrySet.iterator(); while (iterator3.hasNext()) { Object entry = iterator3.next(); // System.out.println(next.getClass()); // class java.util.HashMap$Node // 向下转型为 Map.Entry Map.Entry m = (Map.Entry) entry; // m.getValue()才是值,单独接收可以再下转一次 System.out.println(m.getKey() + "-" + m.getValue()); } }
-
小练习:使用 HashMap 添加了三个员工对象,要求键:员工id,值:员工对象
并遍历显示工资大于10000的员工
员工类:姓名、工资、员工 id
public class test1 { public static void main(String[] args) { Map hashmap = new HashMap(); hashmap.put(1, new Emp("zyz", 20000, 1)); hashmap.put(2, new Emp("duang", 13000, 2)); hashmap.put(3, new Emp("dongdongdong", 8000, 3)); System.out.println("第一种遍历方式"); Set keySet = hashmap.keySet(); for (Object key : keySet) { // System.out.println(obj); Emp emp = (Emp) hashmap.get(key); if (emp.getSalary() > 10000) { System.out.println(emp); } } System.out.println("第二种遍历方式"); Set entrySet = hashmap.entrySet(); Iterator iterator = entrySet.iterator(); while (iterator.hasNext()) { Map.Entry entry = (Map.Entry) iterator.next(); Emp emp = (Emp) entry.getValue(); if (emp.getSalary() > 10000) { System.out.println(emp); } } } } class Emp { private String name; private double salary; private int id; public Emp(String name, double salary, int id) { this.name = name; this.salary = salary; this.id = id; } @Override public String toString() { return "Emp{" + "name='" + name + '\'' + ", salary=" + salary + ", id=" + id + '}'; } public String getName() { return name; } public void setName(String name) { this.name = name; } public double getSalary() { return salary; } public void setSalary(double salary) { this.salary = salary; } public int getId() { return id; } public void setId(int id) { this.id = id; } }
- EntrySet 方法的难点在于理解其包装,k-v 包装进 Node ( Emp ) 节点,中间 Entry 过渡,再包装到 EntrySet 里
- 所以取出来就是先转 Map.Entry,再 getValue() 取转为节点使用
- 勿忘了 entry 真正的运行类型:HashMap$Node
HashMap
了解小结
-
Map 接口的常用实现类:HashMap、Hashtable、Properties
-
HashMap 是 Map 接口使用频率最高的实现类
-
HashMap 是以 k-v ( 键值 ) 对的方式来存储数据
-
key 不能重复,但是值可以重复,允许 null
-
如果添加相同的 key,则会覆盖原来的 k-v,等同于修改替换
-
与 HashSet 一样,不保证映射顺序 ( HashSet 本质就是 HashMap:数组 + 链表 + 红黑树 ),因为底层是以 hash 表的方式来存储的
-
HashMap 没有实现同步,因此线程是不安全的,方法无同步互斥方法 ( synchronized )
源码底层分析
-
仍是 table ( Node 类型数组 ),里面重复索引的用链表形式,到时机树化
-
如果添加相同的 key,则会覆盖原来的 k-v,等同于修改替换
// 在 putVal(): if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) // 知重复的话上述判断成立 e = p; // 直接到: if (e != null) { // existing mapping for key V oldValue = e.value; // onlyIfAbsent 是从上一步传来的固定参数:false // * @param onlyIfAbsent if true, don't change existing value // 就是用来判断是否要进行替换 if (!onlyIfAbsent || oldValue == null) // 把value值进行了替换 e.value = value; afterNodeAccess(e); return oldValue; }
- 最初 Hashmap 构造器,获取 loadFactor = 0.75,table 表为 null
- 存储的 put() 方法,返回
return putVal(hash(key), key, value, false, true);
,在其中计算静态 hash 值 (return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
) - 执行核心 putVal():
- 最初空 table 走 resize(),拿到临界点等值 ( 初始化大小 16,临界 12 等 ),
Node<k, v>[] newTab
给 table ( 内皆 null ),携长度,out 出来得 n - 计算索引值看此处是否为空
(p = tab[i = (n - 1) & hash]) == null
,空就创建并放入 Nodetab[i] = newNode(hash, key, value, null);
,不达临界,返回 null 表示成功 - 之后步骤都同先前 Java-Day-17 所述
- 最初空 table 走 resize(),拿到临界点等值 ( 初始化大小 16,临界 12 等 ),
-
大体核心代码各判断
-
表空否
-
计算索引处空否
-
上述都 F,存 Node e
- 索引处 hash 等 + 同对象或同值否
- 红黑树否
- 上述都 F,索引处死循环链表
- 链表内无同 k,e 为 null
- 树化否
- 链表内 hash 等 + 同对象或同值否
- 链表内无同 k,e 为 null
- e 空否 ( 都要经过 )
- 同 k 替换 v 否
-
修改成功 + 1
- 扩容否
-
-
自己编写代码模拟扩容:
public class test1 { public static void main(String[] args) { HashMap hashMap = new HashMap(); for (int i = 0; i <= 12; i++) { hashMap.put(new Z(i), "zyz"); } // 起初table内16个(下标到15),当到put到第9个时,table扩成了32个(下标到31),当前列表仍是Node非树化,所有数据仍都在一条链表上 // 再put到第10个,table扩成了64个(下标到63),数组table大小变化会重新计算索引位置,所有数据仍都在一条链表上 System.out.println("hashMap = " + hashMap); // 只修改了hashCode,equals没改,仍是12个k-v } } class Z { private int num; public Z(int num) { this.num = num; } // 为展现扩容debug: // 使得所有的Z对象的hashCode值都是100,这样就会都落在table的同一个索引位置了 @Override public int hashCode() { return 100; } @Override public String toString() { return "\nZ{" + "num=" + num + '}'; } }
LinkedHashMap
- LinkedHashSet 底层是 LinkedHashMap 再底层是 HashMap,核心都在 HashMap 里
Hashtable
了解介绍
- 存放元素仍是键值对
- 键和值都不能为 null,否则会空指针异常
- hashtable 是线程安全的 ( hashMap 线程不安全 )
- 仍存在等键替换
源码底层分析
- 底层有数组初始化:table = Hashtable$Entry[11]
- 底层就是 Entry 数组,添加方法:
addEntry(hash, key, value, index);
- 底层就是 Entry 数组,添加方法:
- 临界值仍是 11 * 0.75 取的 8,put 到 9 时扩容到 23 ( 下标 22 )
- 扩容机制是:int newCapacity = (oldCapacity << 1) + 1 —— 乘二加一
- put:Node —> Entry —> table
HashMap 和 Hashtable 对比
版本 | 线程安全 ( 同步 ) | 效率 | null 键值 | |
---|---|---|---|---|
HashMap | 1.2 | 不安全 | 高 | 可以 |
Hashtable | 1.0 | 安全 | 较低 | 不可以 |
Properties
了解认识
- Properties 类继承自 Hashtable 类并且实现了 Map 接口 ( 间接的 ),也是使用一种键值对的形式来保存数据
- 使用特点和 Hashtable 类似
- Properties 还可以用于从某一 xxx.properties 文件中,加载数据到 Properties 类对象,并进行读取和修改 ( 如:存储连接数据库信息等,就不用将信息写进程序里了 )
- 说明:工作后 xxx.properties 文件常作为配置文件,IO 流时举例讲解
代码举例分析
-
Properties 类继承自 Hashtable
-
可以通过 k-v 存放数据,键值都不能为 null
-
无序,有替换
public static void main(String[] args) { Properties properties = new Properties(); // 增加 properties.put("zyz", 100); properties.put("duang", 500); properties.put("duang", 300); System.out.println(properties); // 通过 key 获取值 System.out.println(properties.get("duang")); // 300 // 删除 properties.remove("zyz"); // 修改 properties.put("zyz", 820820); System.out.println(properties); }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义