Java集合框架
集合概述
除了map结尾的类之外,其他类都实现了Collection接口,以map结尾的类都实现了Map接口
List、Set、Map的区别
-
List,元素有序,可重复
- ArrayList底层是object[]数组
- vector底层是object[]数组
- Linkelist底层是双向链表
-
Set,元素无序,不可重复
- HashSet(无序,唯一)基于HashMap实现,底层采用HashMap存储元素
- LinkedHashSet是HashSet的子类,并且内部通过LinkedHashMap来实现的
- TreeSet(有序,唯一)底层是红黑树,自平衡的二叉排序树
-
Map,元素无序,键不可重复,值可重复
- HashMap是由数组+链表组成,数组是HashMap的主体,链表主要是为了解决哈希冲突而存在的(拉链法)
- LinkedHashMap继承自HashMap,底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成,LinkedHashMap在上述结构的基础上增加了一条双向链表,使其可以保持键值对的插入顺序,同时对链表进行相应的操作,实现访问顺序相关逻辑
- Hashtable由数组加链表组成,数组是Hashtable的主体,链表是为了解决哈希冲突而存在的
- TreeMap底层采用红黑树,自平衡的二叉排序树
-
如何选用集合
- 如果需要根据键值获取到元素值就选用Map接口下的集合,需要排序选择TreeMap,不需要排序则使用HashSet,需要保证线程安全使用ConcurrentHashMap
- 如果只需要存放元素值时就选用collection接口的集合,需要保证元素唯一就使用Set接口的集合如HashSet、TreeSet;不需要就实现Lsit接口下的集合如ArrayList或LinkedList,再根据实现这些接口的集合的特点来选用
-
为什么要使用集合
- 当我们需要保存一组类型相同的数据的时候,应该用一个容器来保存,这个容器就是数组,但是在实际开发中,存储的数据类型是多种多样的,数组的缺点是一旦声明之后,长度就不可变了,同时声明数组时的数据类型也决定了该数组存储的数据的类型,而且数组存储的数据是有序的,可重复的,特点单一
Collection子接口之List
ArrayList和vector的区别
- ArrayList是线程不安全的,vector是线程安全的但是vector的性能差
ArrayList和LinkedList的区别
- ArrayList底层是使用object[]数组实现的
LinkedList底层是使用双向链表实现的
两者都是线程不安全的 - ArrayList的插入删除受到元素位置的影响,使用add(E e)在集合尾部添加元素时时间复杂度为O(1),在指定位置进行插入和删除操作时,由于需要将受影响的数据前移或者后移,所以时间复杂度为O(n-1),支持快速随机访问
LinkedList在头尾插入或者删除元素不受元素位置的影响,如果在指定位置进行插入的话需找到该位置在进行节点指向的修改,不支持快速随机访问 - ArrayList内存浪费主要体现在list列表会预存一定的容量空间
LinkedList内存浪费主要体现在每个节点都需要保存本节点的前驱和后继还有自身的值
Collection子接口之Set
无序性和不可重复性
- 无序不等于随机性,无序是指存储的数据并非按照数组的索引的数据添加的,而是根据数据的哈希值觉得的
- 不可重复性是指添加的元素按照equals判断,防止值重复,需要同时重写equals和hashcode方法
HashSet、LinkedHashSet、TreeSet的区别
- HashSet是Set接口的主要实现,底层是HashMap,线程不安全,可以存储null值
- LinkedHashSet是HashSet的子类,能够按照添加元素的顺序遍历
- TreeSet底层使用红黑树,能够按照添加元素的顺序进行遍历,排序的方式有自然排序或者自定义排序
Map接口
HashMap和Hashtable的区别
- Hashtable是线程安全的,效率低,基本被淘汰,不支持键值对为null,会抛出空指针异常,创建时不指定容量默认大小为11,每次扩充会变成原来的2n+1,底层使用数组+链表实现,数组是主体,链表为了解决哈希冲突而存在
- HashMap是线程不安全的,效率相对hashtable高,键值对都允许空值,底层数据结构为数组+链表,当链表长度大于阈值(默认为8)时,会判断当前数组长度是否大于等于64,如果小于则进行数组扩容,如果大于则将链表转换成红黑树,减少搜索时间
HashMap和HashSet的区别
- HashMap实现了Map接口,存储键值对,调用put方法增加元素,使用键key计算hashcode
- HashSet实现了Set接口,存储对象,调用add方法向集合中添加元素,计算成员对象的hashcode值,对于两个对象来说hashcode可能相同,所以使用equals方法来判断对象的相等性,相等则放弃添加
HashMap和TreeMap的区别
- 都继承自AbstractMap,TreeMap还实现了NavigableMap和sortedMap接口,前者使TreeMap有了对集合内元素搜索的能力,后者让TreeSet有了对集合内元素排序的能力,默认按key的升序排序,也可以自定义排序的比较器
HashSet如何检查重复
-
当往HashSet集合中添加元素时,会计算当前元素的哈希值判断元素插入的位置,同时与其他已在集合中的数据的哈希值进行判断,如果有没有相等,直接在该位置进行插入;如果哈希值相等,则调用equals方法比较哈希值相等的两个对象的值是否相等,如果两者的值相同则放弃插入
-
HashCode与equals方法
- 如果两个对象相等,则hashcode一定相同
- 两个对象的值相等,equals方法返回true
- 两个对象有着相同的哈希值,它们不一定相等
- 覆盖了equals方法,则hashcode方法也必须被覆盖
HashMap的底层实现
- JDK1.8以前
底层是数组和链表结合在一起的链表散列,HashMap通过key的hashcode经过扰动函数处理过后的哈希值,判断当前元素存放的位置,如果当前位置存在元素的话,就判断该元素与新加入元素的hash值和key是否相同,如果相同则直接覆盖,不同就通过拉链法解决冲突 - JDK1.8之后
在解决哈希冲突有了较大的变化,当链表的长度达到阈值时默认为8,判断当前数组长度是否大于等于64,如果小于64则进行数组扩容,否则将链表转换成红黑树,减少搜索时间
HashMap的长度为什么是2的幂次方
- 取余(%)操作中如果除数是 2 的幂次则等价于与其除数减一的与(&)操作(也就是说hash%length==hash&(length-1)的前提是 length 是 2 的 n 次方;)。并且采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是 2 的幂次方。
HashMap多线程操作会导致死循环问题
- 由于HashMap的线程不安全的集合,在并发下的Rehas会造成元素之间会形成一个循环链表。建议在多线程下使用ConcurrentMap
遍历HashMap的方式
-
迭代器方式遍历
先获得Iterator对象-
EntrySet方式遍历
- EntrySet中包含键值对所有信息,通过getKey、getValue获取键值对
-
KeySet方式遍历
- KeySet中包含所有的Key,通过map.get(key)获取该key所对应的值从而获取键值对
-
-
foreach方法遍历
-
foreach EntrySet 方式遍历
- EntrySet中包含键值对所有信息,通过getKey、getValue获取键值对
-
foreach KeySet方式遍历
- KeySet中包含所有的Key,通过map.get(key)获取该key所对应的值从而获取键值对
-
-
lambda表达式遍历
- map.forEach((key,value) -> {
System.out.println(key);
System.out.println(value);
});
- map.forEach((key,value) -> {
-
Streams API遍历
-
使用单线程方式遍历
- map.entrySet().stream().forEach((entry) -> {
System.out.println(entry.getKey());
System.out.println(entry.getValue());
});
- map.entrySet().stream().forEach((entry) -> {
-
使用多线程方式遍历
- map.entrySet().parallelStream().forEach((entry) -> {
System.out.println(entry.getKey());
System.out.println(entry.getValue());
});
- map.entrySet().parallelStream().forEach((entry) -> {
-
ConcurrentHashMap与Hashtable的区别
-
底层数据结构
- JDK1.7的ConcurrentMap底层采用分段的数组+链表实现
JDK1.8采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑树 - Hashtable底层数据结构采用的是数组+链表的形式,数组是主体,链表主要为了解决哈希冲突
- JDK1.7的ConcurrentMap底层采用分段的数组+链表实现
-
实现线程安全的方式
- ConcurrentHashMap
JDK1.7,使用的分段锁对整个桶数组进行了分割分段,每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。到了JDK1.8摒弃了分割分段概念,而是直接使用Node数组+链表+红黑树的数据结构来数显,并发控制使用synchronized和CAS来操作 - Hashtable
同一个把锁,使用synchronized来保证线程安全,效率低下,当一个线程访问同步方法时,其他线程也访问同步方法,会进入阻塞或轮询状态,如使用put添加元素,另一个线程不能使用put添加元素,也不能使用get,竞争会也来越激烈
- ConcurrentHashMap
Collections工具类
排序
- 反转reverse(lsit)
随机排序shuffle(lsit)
按自然排序的升序排序sort(lsit)
定制排序,由comparator控制sort(lsit,comparator)
交换两个索引位置的元素swap(list,i,j)
旋转当distance为正数时,将list后distance个元素整体移到前面。当distance为负数时,将 list的前distance个元素整体移到后面
rotate(list,distance)
查找替换操作
- 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)//用新元素替换旧元素