目录
1. Map接口
1.1 Map简介
Map 集合是以 Key-Value 键值对作为存储元素实现的哈希结构, Key是不可重复的, Value 则是可以重复的。 Map 类提供三种 Collection 视图,在集合框架图中, Map 指向 Collection 的箭头仅表示两个类之间的依赖关系 。 可以使用keySet()
查看所有的 Key,使用 values()
查看所有的 Value ,使用entrySet()
查看所有的键值对。 HashMap线程是不安全的。 ConcurrentHashMap 是线程安全的。在多线程并发场景中,优先推荐使用ConcurrentHashMap 。 TreeMap 是内容有序。
1.2 Map结构
- Map与Collection并列存在,用于保存具有映射关系的数据,Key-value
- Map中的key和value都可以是任何引用类型得到数据
- Map中的key用set来存放,不允许重复,所以Map对象所对应的类,必须重写hasCode()和equals()方法
- key 和 value 之间存在 key 找到value的关系
map.get(key)
value用Collection存储 - Map实现类由: HashMap、 TreeMap、 LinkedHashMap 其中HashMap使用频率最高
1.3 Map方法
- 添加、 删除、修改操作:
Object put(Object key,Object value)
:将指定key-value添加到(或修改)当前map对象中void putAll(Map m)
:将m中的所有key-value对存放到当前map中Object remove(Object key)
:移除指定key的key-value对,并返回valuevoid clear()
:清空当前map中的所有数据
- 元素查询的操作:
Object get(Object key)
:获取指定key对应的valueboolean containsKey(Object key)
:是否包含指定的keyboolean containsValue(Object value)
:是否包含指定的valueint size()
:返回map中key-value对的个数boolean isEmpty()
:判断当前map是否为空boolean equals(Object obj)
:判断当前map和参数对象obj是否相等
- 元视图操作的方法:
Set keySet()
:返回所有key构成的Set集合Collection values()
:返回所有value构成的Collection集合Set entrySet()
:返回所有key-value对构成的Set集合
public class MapDemo {
public static void main(String[] args) {
Map map = new HashMap();//存
map.put("aaaa", 11);
map.put("aaaa", 1111);//key不可重复,所以直接将上面的覆盖掉
map.put("cccc", 33);
map.put("dddd", 44);
map.put("ffff", 666);
map.put("4444", 666);
map.put("2222", 666);
// map.remove("cccc");//移除某个键值对
System.out.println(map.get("aaaa"));//取值
System.out.println(map.containsKey("4444"));//是否包含指定的key
System.out.println(map.containsValue(666));//是否包含指定的value
System.out.println(map.size());//键值对的个数
System.out.println(map.keySet());//返回所有key组成的set集合
System.out.println(map.values());//返回所有value组成的collection集合
Set set = map.entrySet();//返回键值对构成的set集合 输出形式为即:key=value
// 循环方式一
for (Object o : set) {
System.out.println(o);
}
运行结果
2222=666
4444=666
aaaa=1111
cccc=33
dddd=44
ffff=666
//方式2:使用Entry循环(常用)
for (Object obj : set) {
Map.Entry entry = (Map.Entry) obj;
System.out.println(entry.getKey() + "\t" + entry.getValue());
}
//方式三iterator
Iterator iterator = set.iterator();
while (iterator.hasNext()) {
Map.Entry mp = (Map.Entry) iterator.next();
System.out.println(mp.getKey() + "\t" + mp.getValue());
}
//方式四 使用map的keyset()方法(常用)
for (Object obj : map.keySet()) {
System.out.println(obj + "\t" + map.get(obj));
}
}
}
2222 666
4444 666
aaaa 1111
cccc 33
dddd 44
ffff 666
2. HashMap
2.1 简介
● 初始大小为0,添加第一个元素时扩容到16;
● 允许使用null键和null值,与HashSet一样,存储无序,按照hashcode值存储
● 所有的key构成的集合是Set:无序的、不可重复的。所以, key所在的类要重写:equals()和hashCode()
● 所有的value构成的集合是Collection
:无序的、可以重复的。所以, value所在的类要重写: equals()
● 一个key-value构成一个entry
● 所有的entry构成的集合是Set:无序的、不可重复的
● HashMap 判断两个 key 相等的标准是:两个 key 通过 equals() 方法返回 true,hashCode 值也相等。
● HashMap 判断两个 value相等的标准是:两个 value 通过 equals() 方法返回 true。
2.2 HashMap细节
JDK 7及以前版本
HashMap是数组+链表结构
- 当实例化一个HashMap时,系统会创建一个长度为Capacity的Entry数组, 这个长度在哈希表中被称为容量(Capacity), 在这个数组中可以存放元素的位置我们称之为“桶” (bucket), 每个bucket都有自己的索引, 系统可以根据索引快速的查找bucket中的元素。
- 每个bucket中存储一个元素, 即一个Entry对象, 但每一个Entry对象可以带一个引用变量, 用于指向下一个元素, 因此, 在一个桶中, 就有可能生成一个Entry链。而且新添加的元素作为链表的head。
添加元素的过程
map.put(key1,value1)
首先会调用key1所在类的hashCode()方法计算key1的哈希值,然后通过某种算法计算出key1在Entry数组中的存放位置。
如果此位置上没有存放数据,则(key1-value1)添加成功。
如果此位置上有数据,则比较key1和已经存放的数据的哈希值
如果key1的哈希值和已经存放的数据的哈希值都不相同,则(key1-value1)添加成功。
如果key1的哈希值和已经存放的某一个数据(key2-value2)的哈希值相同,则调用key1所在类的equals(key2)方法
如果equals返回false,则(key1-value1)添加成功。
如果equals返回true,则使用value1替换value2。
(总的来说每次添加都会成功)
JDK8 存储结构
JDK 8版本发布以后: HashMap是数组+链表+红黑树实现
当实例化一个HashMap时, 会初始化initialCapacity和loadFactor, 在put第一对映射关系时, 系统会创建一个长度为initialCapacity的Node数组,
这个长度在哈希表中被称为容量(Capacity), 在这个数组中可以存放元素的位置我们称之为“桶” (bucket), 每个bucket都有自己的索引, 系统可以
根据索引快速的查找bucket中的元素。
添加元素的过程
每个bucket中存储一个元素, 即一个Node对象, 但每一个Node对象可以带一个引用变量next, 用于指向下一个元素, 因此, 在一个桶中,
就有可能生成一个Node链。 也可能是一个一个TreeNode对象, 每一个TreeNode对象可以有两个叶子结点left和right, 因此, 在一个桶中,
就有可能生成一个TreeNode树。 而新添加的元素作为链表的last, 或树的叶子结点。
HashMap的扩容
- 当HashMap中的元素越来越多的时候, hash冲突的几率也就越来越高, 因为数组的长度是固定的。 所以为了提高查询的效率, 就要对HashMap的数组进行扩容, 而在HashMap数组扩容之后, 最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置, 并放进去, 这就是resize。
- 那么HashMap什么时候进行扩容呢? 当HashMap中的元素个数超过数组大小loadFactor 时 , 就 会 进 行 数 组 扩 容 , loadFactor 的 默 认 值为0.75。 也就是说, 默认情况下, 数组大小为16, 那么当HashMap中元素个数超过16 * 0.75=12 的时候, 就把数组的大小扩大一倍, 然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作, 所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。
HashMap树化和链化
当HashMap中的其中一个链的对象个数如果达到了8个,此时如果capacity没有达到64,那么HashMap会先扩容解决,如果已经达到了64,
那么这个链会变成树,结点类型由Node变成TreeNode类型。当然,如果当映射关系被移除后,下次resize方法时判断树的结点个数低于6个,也会把树再转为链表。
扩容和树化举个例子
○ 初始情况下,hashmap 的capacity为16,因子为0.75。
○ 当hashmap桶内元素小于等于8,且size小于12时,不进行扩容和树化的操作。
○ 当hashmap桶内元素为9,因为capacity为16,因此不进行树化,而选择扩容,将capacity扩容为32。
○ 当hashmap桶内元素为10,因为capacity为32,因此不进行树化,而选择扩容,将capacity扩容为64。
○ 当hashmap桶内元素大于10,由于capacity已经达到64,此时进行树化(用红黑树存储)。
○ 最后当HashMap中元素个数超过48(64*0.75=48),进行扩容
JDK1.8HashMap新变化
- HashMap map = new HashMap();//默认情况下大小为0
- 当首次调用map.put()时,再创建长度为16的数组
- 数组为Node类型,在jdk7中称为Entry类型
- 形成链表结构时,新添加的key-value对在链表的尾部
● table: 存储元素的数组,总是2的n次幂
● entrySet: 存储具体元素的集合
● size: HashMap中存储的键值对的数量
● modCount: HashMap扩容和结构改变的次数。
● threshold: 扩容的临界值, =容量*填充因子
● loadFactor: 填充因子
● resize:扩容
● treeifyBin:树化
● untreeify:链化
2.3 哈希取模算法
● 调用对象的hashCode(),确定数组中的槽位,确定数组中的槽位,采用的哈希取模算法
● put(key) —> (n-1) & hash
● 要求容量必须是2的n次方
2.4 面试题
HashMap的容量,为什么必须是2的n次幂
hashMap存储元素时要使用hashCode值来确定放在哪一个桶上,而计算方法为(n-1 & hash)即:hash%n;使用与计算速度要快,但前提是n必须是2的n次幂,否则 HashMap会自动转换为大于这个数的2的n次幂。
3.LinkedHashMap
● LinkedHashSet和LinedHashMap的关系,从逻辑上这两个集合实现方式完全一致,只是LinkedHashSet使用LinkedHashMap实现,只有key,而value是一个静态的空对象
● 底层使用链表实现
● 有顺序(插入顺序),没有重复的集合(看上去有序)
4. TreeMap
● TreeSet使用TreeMap实现。
● TreeMap存储 Key-Value 对时, 需要根据 key-value 对进行排序。TreeMap 可以保证所有的 Key-Value 对处于有序状态。
● TreeSet底层使用红黑树结构存储数据
● TreeMap 的 Key 的排序:
4.1 自然排序
自然排序: TreeMap 的所有的 Key 必须实现 Comparable
接口,而且所有的 Key 应该是同一个类的对象,否则将会抛出 ClasssCastException
public class TreeMapDemo {
public static void main(String[] args) {
User u1 = new User("ggg", 35);
User u2 = new User("ggg", 35);
User u3 = new User("ccc", 83);
User u4 = new User("ccc", 30);
User u5 = new User("ccc", 74);
User u6 = new User("eee", 39);
User u7 = new User("fff", 40);
User u8 = new User("aaa", 40);
User u9 = new User("bbb", 40);
TreeMap map = new TreeMap();
map.put(u1, "a");
map.put(u2, "a");
map.put(u3, "a");
map.put(u4, "a");
map.put(u5, "a");
map.put(u6, "a");
map.put(u7, "a");
map.put(u8, "a");
map.put(u9, "a");
for (Object o : map.entrySet()) {
System.out.println(o);
}
}
}
public class User implements Comparable {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
//按照姓名从大到小排列,年龄从小到大排列
@Override
public int compareTo(Object o) {
if(o instanceof User){
User user = (User)o;
// return -this.name.compareTo(user.name);
// int compare = -this.name.compareTo(user.name);
int compare = this.name.compareTo(user.name);
if(compare != 0){
return compare;
}else{
return Integer.compare(this.age,user.age);
}
}else{
throw new RuntimeException("输入的类型不匹配");
}
}
}
User{name='aaa', age=40}=a
User{name='bbb', age=40}=a
User{name='ccc', age=30}=a
User{name='ccc', age=74}=a
User{name='ccc', age=83}=a
User{name='eee', age=39}=a
User{name='fff', age=40}=a
User{name='ggg', age=35}=a
4.2 定制排序
定制排序:创建 TreeMap 时,传入一个 Comparator
对象,该对象负责对TreeMap 中的所有 key 进行排序。此时不需要 Map 的 Key 实现Comparable 接口
- TreeMap判断两个key相等的标准:两个key通过compareTo()方法或者compare()方法返回0,1,-1
- 0:对象相等,添加失败
- -1:比对象小,添加到左边
- 1:比对象打,添加到右边
public class TreeMapDemo {
public static void main(String[] args) {
User u1 = new User("ggg", 35);
User u2 = new User("ggg", 35);
User u3 = new User("ccc", 83);
User u4 = new User("ccc", 30);
User u5 = new User("ccc", 74);
User u6 = new User("eee", 39);
User u7 = new User("fff", 40);
User u8 = new User("aaa", 40);
User u9 = new User("bbb", 40);
TreeMap map = new TreeMap(new Comparator() {//匿名内部类
@Override
public int compare(Object o1, Object o2) {
if ((o1 instanceof User) && (o2 instanceof User)) {
User user1 = (User) o1;
User user2 = (User) o2;
int compare = user1.name.compareTo(user2.name);
if (compare != 0) {
return compare;
} else {
return Integer.compare(user2.age, user1.age);
}
} else {
throw new RuntimeException("输入的类型不匹配");
}
}
});
map.put(u1, "a");
map.put(u2, "a");
map.put(u3, "a");
map.put(u4, "a");
map.put(u5, "a");
map.put(u6, "a");
map.put(u7, "a");
map.put(u8, "a");
map.put(u9, "a");
for (Object o : map.entrySet()) {
System.out.println(o);
}
}
}
User{name='aaa', age=40}=a
User{name='bbb', age=40}=a
User{name='ccc', age=83}=a
User{name='ccc', age=74}=a
User{name='ccc', age=30}=a
User{name='eee', age=39}=a
User{name='fff', age=40}=a
User{name='ggg', age=35}=a