双列集合:Map
1.双列集合的特点:
- 双列集合一次需要存一对数据,分别为键和值
- 键不能重复,值可以重复
- 键和值是一一对应的,每一个键只能找到自己对应的值
- 键 + 值这个整体,称之为“键值对”或者“键值对对象”,在Java中叫做“Entry对象”
Map中常见的API
1.Map是双列集合的顶层接口,它的功能是全部双列集合都可以继承使用的。
2.创建Map的集合时,需要传递两个泛型 K V,表示键以及值。
HashMap<String,String> h = new HashMap<>();
3.常用方法:K V 都是泛型,这里分别代表键 和 值
方法名 | 说明 |
V put(K key,V value) | 添加/覆盖元素,返回被覆盖的值 |
V remove(Object key) | 根据键删除键值对元素,返回被删除的值 |
void clear() | 移除所有的键值对元素 |
boolean containsKey(Object key) | 判断集合是否包含指定的键 |
boolean containsValue(Object value) | 判断集合是否包含指定的值 |
boolean isEmpty() | 判断集合是否为空 |
int size() | 返回集合的长度,也就是键值对的个数 |
K keySet<> | 获取所有的键,把键放到一个单列集合Set中返回 |
V get(K key) | 获取集合中键对应的值 |
Set<Map.Entry<K,V>> entrySet() | 获取所有的键值对对象,返回一个Set集合 |
4.put方法:
- 在添加数据时,如果键不存在,那么直接把键值对对象添加到map集合中
- 如果键是存在的,那么会把原有的键值对对象覆盖,会把被覆盖的值返回
Map的遍历方式
1.通过键找值进行遍历:
- 通过keySet方法,获取所有的键把键放到一个单列集合Set中返回
- 通过单列集合的遍历方法,同时使用get方法,获取到对应的值,最后将键和值进行输出
Set<String> keys1 = m.keySet();
//使用增强for遍历Set集合
for(String key : keys1){
//此时的key即为Set集合中的一个键,通过get方法获取其对应的值并输出
String value = m.get(key);
System.out.println(key + "=" + value);
}
2.通过键值对进行遍历:
- 通过map的entrySet方法去获取所有的键值对对象,返回一个Set集合
- 要注意该方法返回的是一个Set<Map.Entry<K,V>>,而Map.Entry<K,V>本身也是一个数据类型,这里属于嵌套了泛型
- 其中的Entry是Map里的内部接口,如果不写成Map.Entry,只写Entry那就需要手动导包import java.util.Map.Entry 在接收返回值的时候最好使用快捷键 CTRL + ALT + V(Entry就是键值对的意思)
- Entry中有getKey以及getValue方法,可以获取键和值
- 使用Set集合的遍历方法,将键和值获取并打印
//3.1 增强for遍历map集合
//3.1.1 获取键值对对象
Set<Map.Entry<String, String>> entries = m.entrySet();
for(Map.Entry<String,String> entry : entries){
//3.1.2 获取键值对对象中的键与值
String key = entry.getKey();
String value = entry.getValue();
System.out.println(key + "=" + value);
}
3.通过Lambda表达式进行遍历:
- map集合自带的forEach方法与Collection集合自带的方法有一些不一样,map集合底层实际是使用了键值对进行遍历的方法,先通过entrySet获取到键值对对象的Set集合,然后对Set集合进行增强for遍历
- map中的Lambda表达式实现的是BiConsumer接口,而Collection集合中实现的是Consumer接口,重写的方法都是accept,不一样的是Biconsumer接口中的accept有两个形参,一个是键,一个是值,而Consumer接口中的accept只有一个形参,代表的就是元素
//使用Map中的lambda表达式遍历Map集合
m.forEach((key, value) -> System.out.println(key + "=" + value));
//map中的forEach
default void forEach(BiConsumer<? super K, ? super V> action) {
Objects.requireNonNull(action);
for (Map.Entry<K, V> entry : entrySet()) {
//此处的entrySet()是方法,返回的是存储键值对对象的Set集合
K k;
V v;
try {
k = entry.getKey();
v = entry.getValue();
} catch(IllegalStateException ise) {
// this usually means the entry is no longer in the map.
throw new ConcurrentModificationException(ise);
}
action.accept(k, v);
}
}
//Collection中实现的Iterable接口中的forEach
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
4.不同的遍历方式各有用处,通过键值对和键找值的方式比较相似,而通过Lambda表达式的使用场景又不太一样,当涉及到使用外部类的变量时,最好不要使用Lambda表达式,因为Lambda表达式是用于简化匿名内部类的,而匿名内部类不可以随意修改外部类的变量,除非经过final修饰的常量。具体为什么,参考:
https://blog.youkuaiyun.com/qq_45086308/article/details/121704501
HashMap
1.HashMap的特点:
- HashMap是Map里面的一个实现类
- 没有额外需要学习的特有方法,直接使用Map里面的方法就可以了
- 特点都是由键决定的:无序(存取顺序有可能不一样)、不重复、无索引
- HashMap跟HashSet底层原理是基本一样的,都是哈希表结构
2.put方法的一些底层原理:
- 利用键计算哈希值,跟值无关
- 当计算出来的索引处没有元素,将键值对添加进去
- 当对应索引处有元素,那么使用equals方法比较键是否相同,如果相同,则会覆盖,如果不相同,则会将键值对添加进去,形成链表/红黑树
3.HashMap的一些注意事项:
- 以hashCode(通过键计算哈希值)方法和equals(比较键是否相同)方法保证键的唯一
- 如果键存储的是自定义对象,需要重写hashCode和equals方法,如果值存储的是自定义对象,不需要重写hashCode和equals方法(因为不使用值去计算哈希值以及比较)
LinkedHashMap
1.由键决定:有序(存储和取出的元素顺序一致)、不重复、无索引。
2.底层数据结构依然是哈希表,只是每个键值对元素又额外的多了一个双链表地机制记录存储的顺序(类似于HashSet和LinkedHashSet)。
TreeMap
1.TreeMap跟TreeSet底层原理一样,都是红黑树结构的,增删改查性能都较好。
2.由键决定特性:不重复、无索引、可排序(对键进行排序)。
3.排序默认按照键从小到大排序,也可以自己规定键的排序规则。
4.制定排序规则(跟TreeSet一致):
- 实现Comparable接口,指定比较规则
- 创建集合时传递Comparator比较器对象,指定比较规则
- 如果两种方法都指定了规则,以第二种为准
使用Map集合进行统计
1.当统计的东西比较多时,使用计数器进行统计非常地不方便,因为不知道会出现多少个不同的需要统计的内容,因此很难在一开始就确定计数器的个数。
2.Map集合是使用键值对去进行的存储,能够保证键的唯一,因此,可以将键视为需要计数的内容,而值用于计数,当出现新的需要计数的内容时通过存入新的键值对就可以实现,不用提前确定计数器的个数。
3.如果题目中没有要求对结果进行排序,默认使用HashMap,因为HashMap的效率是最快的。
4.如果题目中要求 对结果进行排序,使用TreeMap。
Map源码分析
1.Ctrl + F12可以查看当前类/接口的内部结构,C代表当前类/内部类,m代表类中方法,f代表当前类的属性;锁住表示私有(private),开锁表示公开(public),钥匙表示受保护(protected),既没有锁也没有钥匙那就是没有添加修饰符。
2.方法中的返回值放方法名和形参后用:隔开。
3.方法中返回值后面的箭头,向上表示重写了接口里的方法,接口名在箭头后显示。
4.如果方法本身是灰色的,在返回值后面跟着一个右箭头,说明当前方法来自于对应接口里的方法,接口名在箭头后。
HashMap源码
1.底层的元素存在内部类Node<K,V>和TreeNode<K,V>中。
2.Node<K,V>:
- Node<K,V>实现了Map.Entry<K,V>接口,说明一个键值对也是一个Entry对象
- Node中存储了哈希值、键、值还有下一个结点的地址(链表形式)
- 其中哈希值和键都用final修饰了,因此都是一个常量,只能被赋值一次,而值可以多次修改,这就是为什么值能被覆盖
3.TreeNode<K,V>:
- TreeNode<K,V>继承了LinkedHashMap.Entry<K,V>,而LinkedHashMap.Entry<K,V>继承了Node<K,V>,因此TreeNode<K,V>中也有哈希值,键、值以及下一结点的地址值(但由于红黑树,所以不是这个值意义不大)
- TreeNode中存储了父结点的地址值,左子结点和右子结点的地址值,还有记录了一个布尔值的变量red,这个变量用于记录当前结点是否为红
4.HashMap中的数组名即为table,默认的长度为16,默认的加载因子为0.75。
5.数组在HashMap被创建后第一次使用put时创建,在put中会调用一个名为putVal的方法:
源码中put中的内容:
return putVal(hash(key),key,value,(onlyIfAbsent:)false,(evict):true);
其中(onlyIfAbsent:)与(evict):仅为形参的提示,传过去的是false与true,而onlyIfAbsent控制的是当前重复的值是否覆盖,false即为不保留,会覆盖。
而hash(key)是将键传给hash方法,计算出对应的哈希值并返回。
6.实际上数组的创建和扩容并不是由put方法,也不是由putVal方法完成的,是由resize()方法完成的:
- 当第一次添加数据时,resize方法会创建一个默认长度为16,加载因子为0.75的数组
- 如果不是第一次添加数据,会判断数组中的元素是否达到了扩容的条件(存储的数据 = 数组长度 * 加载因子)
- 如果没有达到扩容条件,resize方法不会做任何操作
- 如果达到了扩容条件,resize方法会把数组扩容为原先数组长度的两倍,并把数据全部转移到新的哈希表中
- 数据转移的过程中,需要重新计算索引值。索引值公式回顾:索引值 = (数组长度 - 1)& 哈希值,再把数据放到新的数组中的新索引位置
7.当添加键值对时:
- putVal中先根据索引值判断对应索引位置是否有元素
- 如果有,用一个第三方变量接收当前元素,判断变量的键的哈希值与添加的键哈希值是否相同;如果没有,直接添加键值对
- 如果哈希值相同,那么会继续判断键是否相同,如果键相同,那么会将待添加元素的值覆盖掉旧键值对的值(而不是将新的键值对替换掉旧的键值对,因此链表中记录的仍是旧键值对的地址);如果键不同,将键值对进行添加
- 如果哈希值不同,那么说明键必定不同,然后判断变量是否为红黑树结点,如果是,则将添加的键值对使用红黑树的添加规则添加进红黑树下;如果不是,则形成链表,使用链表的添加规则,将键值对添加到链表尾部
- 如果当前键值对要添加进链表,将键值对添加进链表后,会判断当前链表的长度是否超过8,如果超过8,会调用 treefyBin 方法,treefyBin 方法底层还会继续判断数组的长度是否大于等于64,如果大于等于64,就会把链表转成红黑树
TreeMap源码
1.TreeMap中有内部类Entry<K,V>,其实现了Map.Entry<K,V>接口,类中有成员属性:键、值、左子结点的地址、右子结点的地址、父结点的地址以及红黑树的颜色color(布尔类型,BLACK为true,RED为false)。
2.TreeMap中的成员变量:
- private final Comparator<? super K> comparator; 排序规则
- public transient Entry<K,V> root; 根结点
- private transient int size = 0; 结点数目
- private transient int modCount = 0; 处理并发的计数
3.TreeMap中的put方法:
- 调用put方法一般是传入键与值,只有两个参数,而该put方法中会调用另一个带有三个形参的put方法:privatr V put(K, key,V value,boolean replaceOld)(实际上我在使用IDEA2020版本并没有发现有调用三个形参的put方法)
- replaceOld记录了当键重复的时候,是否需要覆盖值,true表示覆盖,false表示不覆盖
- put中会对键值对中有无比较器对象Comparator进行判断,如果当前键值对对象传递了比较器对象,那么就会优先执行比较器中的排序规则,如果没有,那就按照默认排序或者是实现了Comparable的接口中的排序规则进行排序
- 排序完成后会调用 addEntry() 方法进行添加,传入键、值、父结点以及布尔值是否添加至左边,同时在方法内会调用方法 fixAfterInsertion(Entry<K,V> x) 对添加完之后的红黑树进行调整
关于Map集合的一些常见面试问题
1.TreeMap添加元素的时候,键是否需要重写hashCode方法和equals方法?
答:此时是不需要重写的,因为TreeMap的底层是使用红黑树的数据结构,里面并没有用到hashCode方法和equals方法。
2.HashMap是哈希表结构的,JDK8开始由数组,链表,红黑树组成,既然有红黑树,是否需要利用Comparable指定排序规则?是否需要传递比较器comparator指定比较规则?
答:不需要,因为在HashMap的底层,默认是利用哈希值的大小关系来创建红黑树的。且HashMap的特点就是无序的。
3.TreeMap和HashMap谁的效率更高?
答:最坏情况下,添加了8个元素,这8个元素形成了链表,此时TreeMap的效率要更高,但是这种情况出现的机率非常的少。一般而言,还是HashMap的效率要更高。
4.你觉得在Map集合中,Java会提供一个如果键重复了,不会覆盖的put方法吗?
答:不会。因为HashMap与TreeMap中都有各自的变量去控制是否覆盖,分别是onlyIfAbsent以及replaceOld,当onlyIfAbsent为false时,会覆盖,当replaceOld为true时,也是会覆盖,若想要不覆盖,只要修改布尔值即可。(其实如果想不覆盖,HashMap中与TreeMap中都有对应的不覆盖的方法 putIfAbsent,不同就是传递的onlyIfAbsent与replaceOld与put中传递的布尔值相反)
5.三种双列集合,应该如何选择?
答:无特殊情况,使用HashMap,因为其效率最高;要保证存取有序,使用LinkenHashMap;要进行排序,使用TreeMap。
可变参数
1.方法的形参个数可变,不需要确定形参个数。
2.格式:属性类型...名字,如 int...args
3.底层原理:是一个数组,只不过不需要我们自己去创建,Java会帮我们创建好。
4.可变参数的小细节:
- 在方法的形参中,最多只能写一个可变参数,因为他默认只要第一个参数传入可变参数后,往后所有传入的参数都是传至可变参数处
- 在方法当中,如果除了可变参数以外,还有其它的形参,那么可变参数要写在最后
Collections
1.Collections定义在java.util包下,Collections不是集合,是集合的工具类。
2.常用API:
方法名 | 说明 |
public static <T> boolean addAll(Collection<T> c,T...elements) | 批量添加元素 |
public static void shuffle(List<?> list) | 打乱List集合元素的顺序 |
public static <T> void sort(List<T> list) | 排序(默认规则) |
public static <T> void sort(List<T> list,Comparator<T> c) | 根据指定的规则进行排序 |
public static <T> int binarySearch(List<T> list,T key) | 以二分查找法查找元素 |
public static <T> void copy(List<T> dest,List<T> src) | 拷贝集合中的元素 |
public static <T> int fill(List<T> list, T obj) | 使用指定的元素填充集合 |
public static <T> void max/min(Collection<T> coll) | 根绝默认的自然排序获取最大值/最小值 |
public static <T> void swap(List<?> list,int i,int j) | 交换集合中指定位置的元素 |
要注意,部分方法只适用于单列集合Collection,而部分方法只适用于List集合
不可变集合(JDK9以上使用)
1.不可变集合:不可以被修改的集合。
2.应用场景:
- 如果某个数据不能被修改,把它防御性地拷贝到不可变集合中是个很好的实践
- 当集合对象被不可信的库调用时,不可变形式是安全的
- 简单理解:不想让别人修改集合中的内容时可以使用不可变集合
3.创建不可变集合的书写格式:
- 在List、Set、Map接口中,都存在静态的of方法,可以获取一个不可变的集合,这个集合不能添加、不能删除、不能修改
方法名 | 说明 |
static <E> List<E> of(E...elements) | 创建一个具有指定元素的List集合对象 |
static <E> Set<E> of(E...elements) | 创建一个具有指定元素的Set集合对象 |
static <K,V> Map<K,V> of(E...elements) | 创建一个具有指定元素的Map集合对象(形参中默认是一个键、一个值地传递过去) |
- 当使用Set.of来获取一个Set集合时,里面的参数也必须不能重复
- 同样当使用Map.of来获取一个Map集合时,键是不能重复的,且Map的of方法传递的参数是有上限的,最多只能传递10个键值对(因为Map的of方法实际上没有使用可变参数)
- 如果想要传递多个键值对,那就需要把键值对看成一个整体,然后将形参类型设置为Entry对象,使用可变参数。而Map中这个方法叫做ofEntries
static<K,V> Map<K,V> ofEntries(Entry<? extends K,? extends V>... entries)
1)如果想要使用这种方法,首先保证传过去的就是键值对对象,或者是一个键值对对象的数组,因为可变参数本身就是一个数组。
2)首先先创建好一个Map集合,加入想要加入的数据;
3)通过Map集合的entrySet方法,获取到键值对对象的Set集合 entries;
4)再把键值对对象的Set集合变成一个键值对对象的数组 entries.toArray(T[] arr),形参中传递想要获得的数组的数据类型,因为想要的是一个键值对对象,因此就可以传递一个键值对对象的类型(Map.Entry)数组,即entries.toArray(new Map.Entry[0]);
5)定义一个Map.Entry类型的数组接收返回的数组;
在这个过程中,如果toArray中不传递数组,那么会默认返回一个Object类型的数组。同时如果传递的数组长度小于集合的长度,此时会根据实际数据的个数,重新创建数组,如果数组长度大于集合的长度,则不会创建新数组,直接使用。因此可以传递过去时可以new一个长度为0的数组。
链式编程实现:
Map<K,V> map = Map.ofEntries(hm.entrySet().toArray(new Map.Entry[0]));
注意:Map.Entry其实不是一个数据类型,是Map中的一个接口,这里说成数据类型是方便理解
- 还有Map.copyOf(Map<K,V> map)方法实现该功能,如果传递过去的是一个可变集合,那么底层就会执行 Map<K,V> map = Map.ofEntries(hm.entrySet().toArray(new Map.Entry[0])); 并将该集合进行返回,如果是不可变集合,那就直接返回。但是该方法只在JDK10之后可用
- 三种方式的使用注意事项:List集合,直接用;Set集合,元素不能重复;Map集合,元素不能重复、键值对数量最多是十个、超过10个用ofEntries方法