Java集合框架
参考JavaGuide:https://snailclimb.gitee.io/javaguide
集合概述
1、说说List、Set和Map三者的区别?
- List:存储的元素是有序的、可重复的
- Set:存储的元素是无需的、不可重复的
- Map:使用键值对(Key-Value)存储,Key是无序的、不可重复的,每个键最多映射到一个值
2、集合框架底层数据结构总结
Collection接口下面的集合:
-
List
- ArrayList:Object[]数组
- Vector:Object[]数组
- LinkedList:双向链表(Jdk1.6之前为循环链表,Jdk1.7取消了循环)
-
Set
- HashSet(无序,唯一):基于HashMap实现的,底层采用HashMap来保存元素
- LinkedHashSet:LinkedHashSet是HashSet的子类,并且其内部是通过LinkedHashMap来实现的。
- TreeSet(有序,唯一):红黑树(自平衡的排序二叉树)
Map接口下面的集合:
- HashMap:Jdk1.8之前HashMap由数组+链表组成,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。Jdk1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间,将链表转换成红黑树前会判断,如果当前数组的长度小于64,那么会选择先进行数组扩容,而不是转换为红黑树
- LinkedHashMap:LinkedHashMap继承自HashMap,所以他的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap在上面结构的基础上,增加了一条双向链表,是的上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑
- HashTable:数组+链表组成的,数组是HashMap的肢体,链表则是为了解决哈希冲突而存在的
- TreeMap:红黑树(自平衡的排序二叉树)
List集合
1、ArrayList和Vector的区别?
- ArrayList是List的只要实现类,底层使用Object[]数组存储,适用于频繁的查找工作,线程不安全
- Vector是List的古老实现类,底层使用Object[]数组存储,线程安全的
2、ArrayList和LinkedList的区别?
- 是否保证线程安全:ArrayList和LinkedList都是不同步的,也就是不保证线程安全
- 底层数据结构:ArrayList底层使用的是Object[]数组;LinkedList底层使用的是双向链表(Jdk1.6之前为循环链表,Jdk1.7取消了循环)
- 插入和删除是否受元素位置的影响:
- ArrayList采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响
- LinkedList采用链表存储,所以对于add(E e)方法的插入,删除元素的时间复杂度不受元素位置的影响,近似O(1),如果是在指定位置插入和删除元素的话,时间复杂度近似为O(n),因为需要先移动到指定位置再插入
- 是否支持快速随机访问:LinkedList不支持高效的随机元素访问,而ArrayList支持。快速随机访问就是通过元素的序号快速获取元素对象
- 内存空间占用:ArrayList的空间浪费主要体现在List列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)
3、双向链表和循环链表
- 双向链表:包含两个指针,一个prev指向前一个节点,一个next指向后一个节点
- 循环链表:最后一个节点的next指向head,而head的prev指向最后一个节点,构成一个环
Set集合
1、Comparable和Comparator的区别
- Comparable接口实际上是出自java.lang包,他有一个compareTo(Object obj)方法用来排序
- Comparator接口实际上是出自java.util包,他有一个compare(Object obj1,Object obj2)方法用来排序
2、无序性和不可重复性的含义是什么
- 无序性不等于随机性,无序性是指存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据数据的哈希值决定的
- 不可重复性是指添加的元素按照equals()判断时,返回false,需要同时重写equals()和hashCode()方法
3、比较HashSet、LinkedHashSet和TreeSet三者的异同
- HashSet是Set接口的主要实现类,HashSet的底层是HashMap,线程不安全的,可以存储null值
- LinkedHashSet是HashSet的子类,能够按照添加的顺序遍历
- TreeSet底层使用红黑树,能够按照添加元素的顺序进行遍历,排序方式有自然排序和定制排序
Map
1、HashMap和HashTable的区别
- 线程是否安全:HashMap是非线程安全的,HashTable是线程安全的,因为hashTable内部的方法基本都经过synchronized修饰。如果需要保证线程安全可以使用ConcurrentHashMap
- 效率:因为线程安全问题,HashMap要比HashTable效率高一点。另外,HashTable基本被淘汰,不要在代码中使用它
- 对null Key和null Value的支持:HashMap可以存储null的Key和Value,但null作为键只能有一个,null作为值可以有多个;HashTable不允许有null键和null值,否则会抛出NullPointerException
- 初始容量大小和每次扩容大小的不同:
- 创建时如果不指定容量初始值,HashTable默认的初始大小为11,之后每次扩容,容量变为原来的2n+1。HashMap默认的初始大小为16,之后每次扩容为原来的2倍
- 创建时如果指定了容量初始值,那么HashTable会直接使用你给定的大小,而HashMap会将其扩充为2的幂次方大小 。也就是说HashMap总是使用2的幂作为哈希表的大小。
- 底层数据结构:JDK以后的HashTable在解决哈希冲突有了较大的变化,当链表的长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。HashTable没有这样的机制。
2、HashMap和TreeMap的区别
- HashMap和TreeMap都继承自AbstractMap,但是需要注意的是TreeMap还实现了NavigableMap和SortedMap接口
- 实现NavigableMap接口让TreeMap有了对集合内元素搜索的能力
- 实现SortedMap接口让TreeMap有了对集合内元素根据键排序的能力。默认是按key的升序排序,也可以自己指定比较器
3、HashSet如何检查重复
当把对象加入HashSet时,HashSet会先计算对象的hashCode值来判断对象加入的位置,同时也会与其他加入的对象的hashCode作比较,如果没有相符的hashCode,HashSet会假设对象没有重复出现。但是如果发现有相同的hashCode的对象,这是会调用equales()方法来检查hashCode相等的对象是否真的相同。如果两者相同,HashSet就不会让加入操作成功。
4、HashMap的底层实现
Jdk1.8之前:
HashMap底层是数组和链表结合在一起使用也就是链表散列。HashMap通过key的hashCode经过扰动函数处理过后得到hash值,然后通过(n-1)&hash判断当前元素存放的位置(这里的n是指数组的长度),如果当前位置存在元素的话,就判断该元素的hash值以及key是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
所谓的扰动函数指定就是HashMap的hash方法。使用hash方法也就是扰动函数是为了防止一些实现比较差的hashCode()方法,换句话说就是使用扰动函数之后可以减少碰撞
jdk1.7 HashMap中的hash方法:
static int hash(int h) {
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
jdk1.8的hash方法相比于jdk1.7中的hash方法更加简化,但是原理不变:
static final int hash(Object key) {
int h;
// key.hashCode():返回散列值也就是hashcode
// ^ :按位异或
// >>>:无符号右移,忽略符号位,空位都以0补齐
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
相比于jdk1.8的hash方法,jdk1.7的hash方法的性能会稍微差一点,因为扰动了4次
Jdk1.8之后:
相比较于之前的版本,jdk1.8之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间,将链表转化为红黑树之前会进行判断,如果当前数组的长度小于64,那么会先进行数组扩容,而不是转换为红黑树
5、HashMap的7种遍历方式
-
使用迭代器EntrySet方式遍历
Iterator<Map.Entry<Integer, String>> iterator = map.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry<Integer, String> entry = iterator.next(); System.out.print(entry.getKey()); System.out.print(entry.getValue()); }
-
使用迭代器KeySet方式遍历
Iterator<Integer> iterator = map.keySet().iterator(); while (iterator.hasNext()) { Integer key = iterator.next(); System.out.print(key); System.out.print(map.get(key)); }
-
使用foreach EntrySet方式遍历
for (Map.Entry<Integer, String> entry : map.entrySet()) { System.out.print(entry.getKey()); System.out.print(entry.getValue()); }
-
使用foreach KeySet方式遍历
for (Integer key : map.keySet()) { System.out.print(key); System.out.print(map.get(key)); }
-
使用Lambda表达式遍历
map.forEach((key, value) -> { System.out.print(key); System.out.print(value); });
-
使用Streams API单线程的方式遍历
map.entrySet().stream().forEach((entry) -> { System.out.print(entry.getKey()); System.out.print(entry.getValue()); });
-
使用Streams API多线程的方式遍历
map.entrySet().parallelStream().forEach((entry) -> { System.out.print(entry.getKey()); System.out.print(entry.getValue()); });
性能方面,除了最后一种多线程操作较高之外,其他方式基本相近。
6、ConcurrenthashMap和HashTable的区别
主要体现在实现线程安全的方式上的不同
- 底层数据结构:jdk1.7的ConcurrentHashMap底层采用分段的数组+链表实现,jdk1.8采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑树。HashTable和jdk1.8之前的HashMap的底层数据结构类似都是采用数组+链表的形式,数组是HashMap的主体,链表则是为了解决哈希冲突而存在的
- 实现线程安全的方式:
- 在jdk1.7的时候,ConcurrentHashMap(分段锁)对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。到了jdk1.8的时候已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用synchronized和CAS来操作。(Jdk1.6以后对synchrinized锁做了很多优化)整个看起来就像是优化过且线程安全的HashMap,虽然在jdk1.8中还能看到Segment的数据结构,但是已经简化了属性,只是为了兼容旧版本。
- HashTable(同一把锁):使用synchronized来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用put添加元素,另一个线程不能使用put添加元素,也不能使用get,竞争会越来越激烈,效率越低
Collections工具类
1、排序操作
void reverse(List list)//反转
void shuffle(List list)//随机排序
void sort(List list)//按自然排序的升序排序
void sort(List list, Comparator c)//定制排序,由Comparator控制排序逻辑
void swap(List list, int i , int j)//交换两个索引位置的元素
void rotate(List list, int distance)//旋转。当distance为正数时,将list后distance个元素整体移到前面。当distance为负数时,将 list的前distance个元素整体移到后面
2、查找、替换操作
int binarySearch(List list, Object key)//对List进行二分查找,返回索引,注意List必须是有序的
int max(Collection coll)//根据元素的自然顺序,返回最大的元素。 类比int min(Collection coll)
int max(Collection coll, Comparator c)//根据定制排序,返回最大元素,排序规则由Comparatator类控制。类比int min(Collection coll, Comparator c)
void fill(List list, Object obj)//用指定的元素代替指定list中的所有元素。
int frequency(Collection c, Object o)//统计元素出现次数
int indexOfSubList(List list, List target)//统计target在list中第一次出现的索引,找不到则返回-1,类比int lastIndexOfSubList(List source, list target).
boolean replaceAll(List list, Object oldVal, Object newVal), 用新元素替换旧元素
3、同步控制(不建议使用)
Collections提供了多个synchronizedXxx()方法,该方法可以将指定集合包装成线程同步的集合,从而解决多线程并发访问集合时的线程安全问题。
最好不要使用这些方法,效率非常低,需要线程安全的集合类型时请考虑使用JUC包下的并发集合
synchronizedCollection(Collection<T> c) //返回指定 collection 支持的同步(线程安全的)collection
synchronizedList(List<T> list)//返回指定列表支持的同步(线程安全的)List
synchronizedMap(Map<K,V> m) //返回由指定映射支持的同步(线程安全的)Map
synchronizedSet(Set<T> s) //返回指定 set 支持的同步(线程安全的)set