集合
学习内容
Map集合
Map中的集合都是以键值对的方式存储元素
Map集合继承关系
Map接口
- 包路径:java.util.Map<K,V>
- 方法
Map接口中的重点方法
方法 | 功能 |
---|---|
boolean containsKey(Object) boolean containsValue(Object) V remove(Object key) | 使用这三个方法时,要求集合中存放的类型要重写equals() |
Set<K> keySet() | 返回此映射中包含的键的 Set 视图。该 set 受映射支持1,所以对映射的更改可在此 set 中反映出来,反之亦然。如果对该 set 进行迭代的同时修改了映射(通过迭代器自己的 remove 操作除外),则迭代结果是不确定的。set 支持元素移除,通过 Iterator.remove、Set.remove、removeAll、retainAll 和 clear 操作可从映射中移除相应的映射关系。它不支持 add 或 addAll 操作 上面时JDK帮助文档的原话,简单来说就是,Map集合调用keySet()时会返回一个Set集合,对这个Set集合修改会导致Map集合中的内容一同修改,反之对Map集合修改也会导致Set集合内容一同修改;并且对Set集合进行遍历时,支持删除元素(remove),不支持添加元素(add或addAll),并且使用迭代器对Set集合遍历时必须使迭代器的remove()删除元素,否则迭代结果时不确定的 |
Collection values() | 返回此映射中包含的值的 Collection 视图。该 collection 受映射支持,所以对映射的更改可在此 collection 中反映出来,反之亦然。如果对该 collection 进行迭代的同时修改了映射(通过迭代器自己的 remove 操作除外),则迭代结果是不确定的。collection 支持元素移除,通过 Iterator.remove、Collection.remove、removeAll、retainAll 和 clear 操作可从映射中移除相应的映射关系。它不支持 add 或 addAll 操作。 |
Set<Map.Entry<K,V>> entrySet() | 返回此映射中包含的映射关系的 Set 视图。该 set 受映射支持,所以对映射的更改可在此 set 中反映出来,反之亦然。如果对该 set 进行迭代的同时修改了映射(通过迭代器自己的 remove 操作,或者通过对迭代器返回的映射项执行 setValue 操作除外),则迭代结果是不确定的。set 支持元素移除,通过 Iterator.remove、Set.remove、removeAll、retainAll 和 clear 操作可从映射中移除相应的映射关系。它不支持 add 或 addAll 操作。 |
Map集合的遍历/迭代
Map集合不能直接遍历,可以通过遍历受映射支持的Set集合/Collection集合来间接遍历Map集合
- 遍历keySet()返回的Set集合
package com.jsoft.collection;
import java.util.*;
public class TestMap {
public static void main(String[] args) {
TreeMap<String, String> stringTreeMap = new TreeMap<>();
stringTreeMap.put("1","20");
stringTreeMap.put("5","120");
stringTreeMap.put("20","250");
stringTreeMap.put("1","22");
stringTreeMap.put("12","20");
stringTreeMap.put("A","260");
stringTreeMap.put("z","720");
// 遍历keySet()返回的Set集合
Set<String> keySet = stringTreeMap.keySet();
for (String key : keySet) {
// 通过key拿到value
String value = stringTreeMap.get(key);
// 向控制台中打印Map集合的key和value
System.out.println(key + "=" + value);
}
}
}
- 遍历entrySet()返回的Set集合
package com.jsoft.collection;
import java.util.*;
public class TestMap {
public static void main(String[] args) {
TreeMap<String, String> stringTreeMap = new TreeMap<>();
stringTreeMap.put("1","20");
stringTreeMap.put("5","120");
stringTreeMap.put("20","250");
stringTreeMap.put("1","22");
stringTreeMap.put("12","20");
stringTreeMap.put("A","260");
stringTreeMap.put("z","720");
// 遍历entrySet()返回的Set集合
Set<Map.Entry<String, String>> entrySet = stringTreeMap.entrySet();
for(Map.Entry<String,String> entry : entrySet) {
// 通过Map.Entry拿到key
String key = entry.getKey();
// 通过Map.Entry拿到value
String value = entry.getValue();
// 向控制台打印Map集合key和value
System.out.println(key + "=" + value);
}
}
}
- 遍历values()返回的Collection集合
package com.jsoft.collection;
import java.util.*;
public class TestMap {
public static void main(String[] args) {
TreeMap<String, String> stringTreeMap = new TreeMap<>();
stringTreeMap.put("1","20");
stringTreeMap.put("5","120");
stringTreeMap.put("20","250");
stringTreeMap.put("1","22");
stringTreeMap.put("12","20");
stringTreeMap.put("A","260");
stringTreeMap.put("z","720");
// 遍历values()返回的Collection集合,只能获取Map集合的value部分
Collection<String> values = stringTreeMap.values();
for(String value : values) {
System.out.println("value: " + value);
}
}
}
对于方式1和方式2来遍历Map集合的key和value,建议使用方式2,因为方式2可以直接从entry对象中获取属性值,效率高;方式1需要通过key去获取value(挨个遍历),效率低
HashMap类
- HashMap集合底层采用了哈希表/散列表(数组+链表) 这种数据结构
- HashMap集合中可以存储null(同理,HashSet集合也可以)
- HashMap默认初始化容量为16
- HashMap在初始化时,指定初始化的容量必须是2的幂(官方要求),目的时为了达到散列均匀,提高HashMap存储效率
- HashMap加载因子0.75,表示当底层数组容量达到75%时,将数组进行扩容
- HashMap中数组一次扩容自身的2倍
- 在Java8,HashMap采用了数组+链表+红黑树这种数据结构,HashMap中当链表的节点超过8个时,会将单向链表转换成红黑树,当红黑树中的节点低于6个时候会将红黑树重新转换成单向链表,这种方式是为了提高检索效率,使用红黑数可以再次缩小扫描范围,
- 哈希表这种数据结构的特点
- 随机增删效率高,因为元素的增删都是在链表上完成的,不涉及到元素的位移操作
- 检索元素效率高,因为通过(数组+链表)这种结构,我们在检索某个元素时,不需要全表扫描,只需要部分扫描(缩小了扫描范围)
put(K,V)方法的实现原理
- 首先会将key和value封装成一个Node对象
- 调用key的hashCode() 获取hash值
- 通过哈希算法将hash值转换成数组下标,通过数组下标定位到指定位置,
- 如果当前位置没有内容(没有链表),会将新Node对象直接放入(此时新Node对象为该链表的头节点)
- 如果当前位置存在节点(存在链表),则会调用key的equals() 从头节点依此遍历比较节点的key部分,
- 如果有所有的equals()都为false,则将新Node放到链表的末尾
- 如果有一个equals()是true,则找到的该节点的value覆盖
使用put()存元素时什么情况下不会调用equals()?
数组下标位置为null时,equals()不需要执行。
get(K)方法的实现原理
- 调用key的hashCode() 获取hash值
- 通过哈希算法将hash值转换成数组下标,通过数组下标定位到指定位置
- 如果当前位置没有内容,则get()返回null
- 如果当前位置存在节点,则会调用key的equals() 从头节点依此遍历比较节点的key部分,
- 如果所有的equals()都返回false,则get()返回null
- 如果有一个equals撒返回了true,则get()返回的值就是这个找到的节点的value
使用get()获取元素时什么情况下不会调用equals()?
数组下标位置为null时,equals()不需要执行
HashMap的是如何保证无序不可重复特点
无序:使用put()方法往HashMap集合中存储元素时,不一定会将元素放在哪一个链表上
不可重复:使用put()方法往HashMap集合中存元素时,如果有相同的key,则会覆盖value
通过上面对HashMap集合中put()和get()的分析,可以看出无论是使用put()往集合中存储元素,还是使用get()获取集合中的value,都需要调用集合key部分equals()和hashCode(),这是为什么?----> 为了保证哈希表这种数据结构的特点
HashMap集合是通过同时重写集合中key部分的equals()和hashCode()来保证无序不可重复特点,通过重写hashCode定位相同的数组下标(同一链表),通过重写equals来找到(同一链表上)相同的key。
那么如果只重写equals(),不重写hashCode()会怎样?我们来看下面的代码
@Test
public void testHashMap() {
// User用户,Integer年龄
Map<User,Integer> users = new HashMap<>();
User user1 = new User("admin","123456");
User user2 = new User("admin","123456");
System.out.println("user1和user2是否是同一个用户:" + user1.equals(user2)); //user1和user2是否是同一个用户:true
// 不重写equals能否保证HashMap中元素不可重复特点?
users.put(user1,19);
users.put(user2,20);
System.out.println("users集合中元素的个数是:" + users.size()); //users集合中元素的个数是:2
}
通过结果可以看出,如果只重写equals()补充些hashCode()是无法保证元素不可重复的特点,因为集合的key未重写hashCode(),则会调用父类Object的hashCode(),此处User实例调用Object类的hashCode(),返回的是对象的内存地址,而不同对象的内存地址是一定不同的,所以在使用put()添加元素时,通过hash算法转化数组下标时,对于所有的不同对象都会定位到不同的数组下标,从而无法保证HashMap中存储元素不可重复的这种特点。如果重写了hashCode()可以将“内容相同的”对象定位到同一数组下标上(同一链表上),这样可以保证HashMap集合中元素不可重复的特点
结论:
我们需要同时重写存储在HashMap集合key部分的equals()和hashCode(),以保证HashMap集合中存储元素的特点,当然对于HashSet集合也是如此,因为HashSet底层是一个HashMap
那么可能有的童鞋会有疑问,HashMap集合中同一链表上所有的节点的key部分的hash值一定是相同的么,(也就是说不同节点的key部分通过调用hashCode()转换的hash值一定时相同的么)?
答案是不一定的,因为在调用hashCode()方法时,不等值(equals()结果为false)的对象,也可能会生成相同的hash值,这种情况我们称之为哈希碰撞/散列冲突,这与底层的模运算结果有关,有想了解的童鞋可以去问下度娘,这里就不赘述了(其实是我也不太懂😓),但是就算发生了哈希碰撞也不影响HashMap存储元素的特点,因为key部分hash值相同的的节点一定会定位到同一数组下标(同一链表中)
对于两个同一类型的对象
hashCode()相同,equals()不一定相等(哈希碰撞)
equals()相等,hashCode()一定相同
哈希表HashMap使用不当时无法发挥性能
- 假设所有的hashCode()都返回一个相同的值会怎样?
会导致哈希表变成单项链表,这种情况是散列不均匀的 - 假设所有的hashCode()都返回不同的值会怎样?
会导致哈希表变成一维数组,这种情况是散列不均匀的 - 什么是散列均匀
假设有100个元素,10个单项链表,那么每个单项链表上都有10个节点,这种情况便是散列均匀的 - 为什么HashMap要尽量达到散列均匀?
越是散列均匀越是可以将哈希表HashMap的性能发挥到极致,使HashMap存储元素效率提到最高
Hashtable类
- Hashtable集合底层采用了哈希表/散列表(数组+链表) 这种数据结构
- Hashtable集合中不可以存储null
- Hashtable集合默认初始化容量为11
- Hashtable集合的加载因子为0.75,表示底层数组在容量达到75%时,对数组进行扩容
- Hashtable集合底层数组一次扩容原来的2倍加1
- Hashtable集合和HashMap集合区别 ----> Hashtable是线程安全的(Hashtable中的方法都有synchronized关键字修饰),现在保证线程安全有更好的方式了,Hashtable已过时
Properties类
- Properties也是线程安全的
- Properties集合中的key和value部分只能是String类型
- Properties又被称为属性类
Properties的使用
@Test
public void testProperties() {
Properties pros = new Properties();
// 存
pros.setProperty("driver","com.mysql.jdbc.Driver");
pros.setProperty("url","jdbc:mysql://localhost:8080/jsoft?charecterEncoding=UTF-8");
pros.setProperty("user","root");
pros.setProperty("password","123");
// 取
String driver = pros.getProperty("driver");
String url = pros.getProperty("url");
String user = pros.getProperty("user");
String password = pros.getProperty("password");
}
Properties与IO流联合使用
@Test
public void testPropertiesAndIO() throws IOException {
Properties pros = new Properties();
// 让pros加载info.properties
pros.load(new FileInputStream("info.properties"));
// 从info.properties中通过key取value
String driver = pros.getProperty("driver");
String url = pros.getProperty("url");
String user = pros.getProperty("user");
String password = pros.getProperty("password");
}
SortedMap接口
- SortedMap接口继承了Map接口的特点(无序不可重复)
- SortedMap集合中元素都是可排序的(根据key部分自动排序)
TreeMap类
- TreeMap接口继承了SortedMap接口的特点,集合中的元素根据key部分自动排序
TreeSet集合和TreeMap集合中的存放的类型必须实现Comparable接口,否则在程序运行期间会出现ClassCaseException。
UML类图
对映射(Map集合)的更改可以在Set中反映出来,反之亦然(即对Set集合的更改也可以在映射中反映出来) ↩︎