集合
1. 集合和数组
我们知道,数组是长度固定,内存连续的。他存放的是相同类型的元素。
而集合相当于优化了数组的一些缺点:
- 集合中可以存储不同数据类型的元素,当然也可以指定泛型存储相同的数据类型。
- 集合的长度是可变的,当存储的元素达到一个阈值时就会自动扩容。
- 集合有更加丰富的数据结构,使用方式可以根据不同应用场景来切换不同的集合类型
2. 泛型
2.1 基本使用
泛型用于强制规定集合统一数据类型的方式,如果不加泛型add()和get()获取的都是Object,需要强转才能正常使用。如果添加了泛型add()和get()获取的就是泛型所规定的类型(省略强转)。
-
泛型方法
-
非静态方法:内部的泛型会根据类的泛型去匹配(创建对象时)
-
静态方法:必须声明出自己独立的泛型,在方法调用时确定
public static <E> void print(E[] e) { for (E e1 : e) { System.out.print(e1 + " "); } }
-
-
泛型接口
-
实现接口的时候指定具体的类型
-
让接口的泛型跟着类的泛型去匹配
public class ArrayList<E> extends AbstractList<E> implements List<E>
-
-
通配符
-
?:可以是任意类型
-
? extend E:E或是其子类
public static void test1(ArrayList<? extends B> list) -
? super E:E或其父类
public static void test2(ArrayList<? super B> list)
-
2.2 泛型擦除
在jdk1.5之前,java是没有泛型这个概念的,在很多需要使用集合的地方都要进行强制类型转化,很容易出错 ,因此引入了泛型。但是java的泛型是一种“假泛型”,java到编译器会进行泛型擦除,在jvm层面自始至终都没有泛型这个概念,底层一直是在做强转。反射的时候也无法获取到集合泛型的类型
ArrayList<Integer> l1 = new ArrayList<>();
ArrayList<String> l2 = new ArrayList<>();
System.out.println(l1.getClass() == l2.getClass());//true
3. 集合体系结构
3.1 集合的分类
单列集合
-
Collection单列集合父类接口 提供迭代器
-
List接口
- ArrayList
- LinkedList
- Vector:数组初始长度10,存满扩容,扩到原数组2倍。(早期java版本的一部分,今不推荐使用)
-
Set接口
- HashSet ->LinkedHashSet
- TreeSet
-
Queue
- LinkedList
-
双列集合
-
Map双列集合父类接口
- HashMap ->LinkedHashMap
- TreeMap
- HashTable
- ConcurrentHashMsp
其中,List存储的元素是 有序,可重复,有索引的。set存储的元素是 无顺序,不重复,无索引的。Map比较特殊,是基于key-value存储的,key是唯一的 不能重复,value是可以重复的,而且每组key和value都是无序的。
3.2 List和Set和数组 相互转化
-
数组转list或set
注意:Arrays.asList()返回的是Arrays内部实现的固定大小的列表,不能增删,修改数组会影响到列表,反之亦然。与subList()类似。如果要修改外边再套一层ArrayList
Integer[] nums = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; List<Integer> list = Arrays.asList(nums); HashSet<Integer> set = new HashSet<>(Arrays.asList(nums)); int[] a = {1, 2, 3, 4, 5}; List<Integer> list = Arrays.stream(a).boxed().collect(Collectors.toList()); -
list或set转数组
Object[] objects = list.toArray(); Integer[] nums1 = list.toArray(new Integer[0]); Integer[] nums2 = set.toArray(new Integer[0]); -
list和set相互转化
ArrayList<Integer> list = new ArrayList<>(set); HashSet<Integer> s = new HashSet<>(list);
3.3 工具类
Collections是集合的工具类,提供了很多集合操作的方法 比如:sort,shuffle,addAll,binarySearch,max,min,swap(交换指定元素的位置)
4. 单列集合
4.1 List接口
4.1.1 实现类
List接口下主要有ArrayList和LinkedList两个实现类。他们两个的区别和底层实现如下:
-
ArrayList:底层是数组实现的;查询快,增删慢;线程是不安全的,执行效率高;
-
查询快的原因:内存是连续的,可以通过下标快速查找元素
-
增删慢的原因:数组的长度是固定的,新增和删除元素的时候长度改变了,需要定义长度更长的数组进行元素的前移/后移
-
扩容原理:
- 利用空参构造创建的集合,会在底层创建一个默认长度为0的数组
- 添加第一个元素时会创建一个新的长度为10的数组
- 当长度达到数组最大值的时候扩容1.5倍
- 如果一次性添加多个元素,1.5倍还放不下,则新的数组长度会以实际长度为准。
-
-
LinkedList:底层是双向链表实现的,查询慢,增删快;线程不安全,执行效率高;链表是靠结点指向下一个结点的,内存是不连续的,理论上是没有长度限制的
- 查询慢原因:链表结构,值通过结点指向下一个结点的,而且内存不是连续的,只能通过前一个结点才能找到下一个结点,所以只能从头结点遍历尾结点,效率低下。但是查询头结点/尾结点效率很高。
- 增删快原因:链表结构,想增删只需要修改指向下一个结点的地址 (换成其他结点)这样其他结点元素不受影响,不需要前移后移,效率比较高。(问到了再答)
4.1.2 集合运算
-
交集:获取两个集合相同的元素
list1.retainAll(list2);//list1就会保存相同的元素 -
并集:两个集合的元素合并保存(不去重)
list1.addAll(list2);//2.获取两个集合合并的结果 -
差集:获取两个集合不同的元素
list1.removeAll(list2);//3.获取两个集合不同的元素
4.1.3 集合排序
List 的排序方式
- 使用 Collections.sort 默认正序,可以传第二个参数自定义排序
- 自定义bean实现 Comparable 接口。
- 实现Comparator接口自定义比较器
4.2.Set接口
主要实现类有HashSet、LinkedHashSet和TreeSet
-
HashSet:底层就是HashMap。HashMap底层是哈希表数据结构,jdk1.8开始哈希表等于数组+链表+红黑树
-
数组默认长度是16,并且有一个加载因子(0.75) 当存储元素超过了长度的75% 才开始扩容 扩容2倍
-
存储元素(put方法)的原理:
- 先根据存储的key值调用hashCode()方法来计算哈希码。这个哈希码会经过再次计算得出最终的哈希码
- 在根据哈希码做位运算,求出数组下标 这样就知道了元素在该存储数组的什么位置中
- 如果数组存储下标对应的空间没有值,就直接存入
- 如果有再去调用equals()方法比较数组中的元素是否相等。如果相等说明是重复的元素 则进行替换(保证元素唯一) ,如果不相等就是哈希冲突 就通过next指向下一个结点来保存所有hashCode相同而值不同的元素(属于链地址法)
- 当链表长度超过8 且数组元素长度超过64 会影响性能 所以会把链表转化成红黑树 。如果红黑树高度小于6 就会在转化成链表
ps: 先获取原始的哈希值,再将原始哈希值向右移动16位做哈希扰动(希望高位的数据也能参与运算,可一定程度减少链表的挂载数量),再和原始哈希异或做二次哈希。得到哈希值后再和数组长度-1做与运算(相当于取模) 算出应存入的索引位置
HashMap的扩容机制:
-
扩容数组
- 当数组中元素个数达到了数组长度*加载因子(0.75) 扩容原数组2倍的大小
- 链表长度超过8个且数组长度没有超过64
-
链表转红黑树
- 链表挂载元素超过8个且数组长度达到了64
-
-
TreeSet:底层实现TreeMap,数据结构是红黑树,支持自然排序(对象排序需要实现比较器对象)
红黑树:是一个特殊自我平衡的二叉树。普通二叉树是根据高度不超过1来控制平衡,而红黑树是通过修改结点成红色和黑色达到颜色的自我平衡
- 根节点必须是黑色的
- 每个存储null 的叶子结点也是黑色的
- 如果有一个节点是红色的 那么他的子节点 只能是黑色的。因为他不影响出现两个连续的红节点存在
- 保证从根节点开始到达任意一个叶子节点 他们黑色的数量是一样的 从而达到了颜色自我平衡(否则要实现左旋和右旋 比较复杂)
4.3 遍历方式
-
标准for(List集合才能用)
-
增强for(底层就是迭代器实现)
-
迭代器遍历
-
java的迭代器是Collection接口提供的。通过游标(下标)跟踪和一种快速失败的机制(Fail-Fast) 来实现的,他提供了一种安全且统一的集合遍历方式。可以使集合的遍历和集合的实现过程解耦,同时还可以防止并发修改导致的数据不一致问题,(list和set才能直接使用迭代器变量,map需要间接使用)。
-
底层原理:
- 游标跟踪:迭代器通过一个类似于下标 跟踪 遍历集合元素的位置,每遍历一次下标+1,并且每次变量元素的时候都会现做一个快速失败机制验证,为了防止并发问题
- 快速失败机制(Fail-Fast):集合内部会维护一个modCount(修改计数器)每次集合结构发生变化都会递增。而迭代器每次创建时都会记录当前的modCount,是为了以后每次调用next()或者remove()会去验证两者是否一致 如果不一致则抛出并发修改异常
-
5. 双列集合
5.1 实现类
Map接口下主要有HashMap、HashTable、TreeMap、LinkedHashMap、ConcurrentHashMap
- HashMap:基于哈希表实现的,允许key和value为null。
默认长度16,加载因子0.75 扩容2倍。线程不安全,执行效率高。 - HashTable:基于哈希表实现的,不允许key和value为null。
默认长度11,加载因子0.75,扩容2倍+1。线程安全,执行效率低。 - TreeMap:是TreeSet的底层实现,可以根据key值做自然排序。不能针对value排序
- LinkedHashMap:相比HashMap是有序的 (使用较少)
- ConcurrentHashMap:相当于线程安全的HaspMap。底层不仅保证了安全,执行效率还比一些传统的线程安全的集合(HashTable)效率高一些。底层实现是通过分段锁来保证线程安全(会把一些数据分成很多数据段,每个数据段都加了一把子锁),当一个线程获取到了一把子锁,其他线程对其他数据操作是不影响的
5.2 遍历方式
for (String key : map.keySet()) {
System.out.println(key + "=" + map.get(key));
}
for (Map.Entry<String, Integer> e : map.entrySet()) {
System.out.println(e.getKey() + "=" + e.getValue());
}
//迭代器
Iterator<String> it = map.keySet().iterator();
while (it.hasNext()) {
String key = it.next();
System.out.println(key + "=" + map.get(key));
}
Iterator<Map.Entry<String, Integer>> it2 = map.entrySet().itera
while (it2.hasNext()) {
Map.Entry<String, Integer> e = it2.next();
System.out.println(e.getKey() + "=" + e.getValue());
}
map.forEach((k, v) -> System.out.println(k + "=" + v));
//forEach
6. 集合面试常见面试题
- Collection和Collections区别?
- list,set,map的区别?
- ArrayList和LinkedList的区别?
- ArrayList扩容原理
- HashSet 或 HaspMap底层原理?
- HashMap和HashTable的区别?
- ConcurrentHashMap如何保证线程安全?
- 怎么获取Map的k和v?