Java中有那些集合以及其数据结构?
首先有两大类集合接口:Collection接口和Map接口,在其下面Collection下面又有List,常用的有Linklist,ArrayList,queue队列比如说priorityqueue和ArrayDeque,还有set,常见用的有hashset,TreeSet等。在Map接口下有,最常用的HashMap,TreeMap,Hashtable。
-
List:
-
- ArrayList底层是数组;
- Linklist底层数据结构是双向链表。
- set:
-
- Treeset的底层是红黑树;
- Hashset底层是基于hashmap实现;
- queue
-
- priorityqueue底层是数组实现的小顶堆;
- ArrayDeque底层是可扩容的双向数组;
- Map
-
- HashMap底层是jdk1.8之前是数组+链表,jdk1.8之后是数组+链表+红黑树;
- LinkedHashMap底层是jdk1.8之前是数组+链表+双向链表,jdk1.8之后是数组+链表+红黑树+双向链表;
- TreeMap底层是红黑树;
- Hashtable是数组+链表。
如何选择使用什么集合?为什么要使用集合?
这个从不同集合的特点考虑:List集合有序可重复,set集合无序不可重复,queue集合队列,特定的排队规则,可重复,Map集合存储键值对。
当然如果说还要考虑线程安全问题:就可以使用ConcurrentHashMap,CopyOnWriteArrayList之类的集合。
集合说白点就是容器,便于存储和处理数据。
无序性和不可重复性
无序性是:存储数据不是按照数组索引的顺序添加,而是通过计算hashcode插入
不可重复性,我的理解是经过equals判断,为false,通过重写equals和hashcode实现。
vector和stack
底层都是数组,vecter是线程安全的数组,方法用synchronized关键字进行了同步处理,stack继承了vecter实现了先进后出的栈,现在基本都不用了。
LinkedList
底层是通过双向链表实现,线程不安全。
数据增加:如果是在链表首或者链表尾时间复杂度为O(1),如果插入指定位置,时间复杂度为O(n);
数据删除:如果是在链表首或者链表尾时间复杂度为O(1),如果指定位置,时间复杂度为O(n)。
不可以快速访问,没有实现RandomAccess接口,因为底层是链表。
空间占用比ArrayList大,因为节点还要存放上下节点的信息,一般不怎么使用。
ArrayList
特点
底层是通过数组实现,线程不安全;
数据增加:如果是在首或者尾时间复杂度为O(1),如果插入指定位置,时间复杂度为O(n);
数据删除:如果是在首或者尾时间复杂度为O(1),如果指定位置,时间复杂度为O(n)。
可以快速访问,实现了RandomAccess接口,因为底层是数组有下标。
ArrayList和Array对比
存储类型不同:ArrayList只可以存储引用类型,基本数据类型会被自动装箱,Array还可以存储基本数据类型;
泛型支持:ArrayList支持泛型,Array不支持;
长度不同:ArrayList长度可以动态扩容,Array在定义后就不可以再更改;
API不同:ArrayList提供添加,删除等API,Array没有;
扩容机制
在最开始创建ArrayList的时候,调用无参构造方法的时候,生成的是空的数组,直到进行第一步add操作后,真正分配容量为十,然后先判断在扩容,也就是说如果在添加第十一个元素的时候,ArrayList判断大于当前长度,然后就进行扩容,扩容的长度为原来的1.5倍,int newCapacity = oldCapacity + (oldCapacity >> 1)。
Comparable和Comparator的区别?
类需要实现Comparable接口,然后重写其中的comparaTo方法,定义类的默认比较方法,实现比较;
而Comparator是比较器,他不需要再类实现接口,而是根据需求直接定义类的比较方法,就像使用
// 定制排序的用法
Collections.sort(arrayList, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2.compareTo(o1);
}
});
Treeset,Hashset,LinkedHashset的区别
三个集合均实现了set接口,能够实现去重,他们最大的不同就是底层数据结构不同,然后导致使用的场景不同;
Hashset数据结构采用的是:hashmap,他的很多内部方法都是直接用调用的hashmap,比如说他的add()方法调用的是put方法,用于存储键值对,对添加数据、取出数据没有顺序要求的场景;
LinkedHashset:双向链表和哈希链表,数据添加和取出满足FIFO;
Treeset:底层数据是红黑树,适用于需要去重且排序的场景。
补充:HashSet如何检查重复
当把对象加入HashSet时,HashSet会先计算对象的hashcode值来判断对象加入的位置,同时也会与其他加入的对象的hashcode值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同hashcode值的对象,这时会调用equals()方法来检查hashcode相等的对象是否真的相同。如果两者相同,HashSet就不会让加入操作成功。
Queue和Deque的区别
Queue是一个队列,数据的取出和插入,支持FIFO,提供了查看队头元素,取出队头元素,插入队尾元素等操作,比如说PrioityQueue和BlockQueue。
Deque是一个双端队列,FIFO,也可以进行栈相关的操作,先进后出,比如ArrayDeque。
简单说一下ArrayDeque,PrioityQueue和BlockingQueue
ArrayDeque:底层数据结构是可扩容双向数组;
PrioityQueue:优先级队列,是一个优先级队列,根据优先级出队,通常,元素需要实现 Comparable 接口或提供自定义的 Comparator 来定义优先级;
BlockingQueue:阻塞队列,支持在队伍为空或者队伍已满的时候阻塞。
HashMap和Hashtable的区别
底层数据结构不同:Hashmap在Jdk8之前是数组+链表,后来是数组+链表+红黑树,Hashtable是数组+链表;
安全性不同:HashMap不安全,Hashtable线程安全;
是否能存null:HashMap可以存储一个key为null的值,Hashtable不能存储null值;
扩容不同:Hashmap初始值16,两倍扩容,hashtable初始值11,扩容为2n+1;
性能不同:HashTable性能更差。
HashMap和TreeMap
底层数据结构不同:Hashmap在Jdk8之前是数组+链表,后来是数组+链表+红黑树,TreeMap底层是红黑树;
安全性来说:都不安全
存储数据顺序来说:TreeMap存储的顺序有序,Hashmap是无顺序的;
能否存储null:HashMap的key可以是null,而TreeMap的key不能为null;
HashMap和Hashset
底层数据结构不同:Hashmap在Jdk8之前是数组+链表,后来是数组+链表+红黑树,Hashset是基于hashmap实现,底层是哈希表;
存储元素不同:Hashmap存储的键值对,hashset存储的是单个对象;hashMap的value可以相同,key不能相同,Hashset的存储的元素不能重复;
实现的父接口不同:HashMap继承于Map,Hashset继承自Collection。
HashMap
HashMap的数据结构?
这个得分jdk1.8之前和之后,jdk1.8之前使用的是数组+链表,jdk1.8之后采用的是数组+链表+红黑树。
红黑树:为什么使用红黑树?怎么保持平衡的?
红黑树是平衡二叉树,进行增删操作的时间复杂度都是O(logn),性能好。
红黑树的特点:
-
- 根节点是黑色;
- 叶子节点是黑色;
- 红节点的子节点是黑色;
- 黑高度相等:任一一节点到其子树节点中的黑节点的数量相同;
- 保持平衡
保持平衡:通过旋转和染色保持平衡。
怎么进行put的?get怎么运行的?
hashmap执行put操作:
- 通过扰动函数对key计算hash值,然后hash & (size - 1)计算出桶位置;
- 如果没有发生哈希碰撞,则直接插入数据;
- 如果发生了哈希碰撞以链表存在桶的位置,插入数据;
- 如果链表长度大于8,并且数组长度大于64,则转变了红黑树;
- 此时节点如果已经存在,则覆盖掉old值;
- 如果出现了容量不够,进行hashmap扩容(这里是指jdk1.8,jdk8先插入,后扩容);
hashmap的get操作?
- 通过扰动函数对key计算hash值,然后hash & (size - 1)计算出桶位置;
- 然后去找这个节点,如果能直接找到,则返回;
- 如果是红黑树或者链表就需要遍历寻找。
- 找到匹配的值,返回对应的值;
- 没找到返回null。
为什么HashMap的容量是2的倍数?故意初始化为17怎么初始化?
我认为有两个原因:一个是为了方便哈希寻址,hashmap的地址是通过对key值通过扰动函数得到哈希值,然后在和地址-1按位与,得到的结果,那么如果是2的倍数就可以直接进行位运算,比hash%2效率更高;
另一个就是,减少哈希碰撞,因为2的倍数的二进制的值,只有一位是1,进行计算后能充分散列函数。
故意初始化不是2的倍数,在HashMap中会自动向上取值到2的倍数,比如初始化为17,实际初始化的值为32.
什么时候扩容?为什么扩容因子为0.75?
当满足扩容因子为0.75时,也就是,如果现在长度为16,当有到12的时候,就是进行扩容。
之所以设计为0.75,是考虑到,时间和空间的平衡问题:
如果扩容因子太大:就会频繁的产生哈希碰撞,查找时间更多;
如果扩容因子太小:相对不容易发生哈希碰撞,但是可能会浪费太多的空间。
说一下扩容机制?(重点在JDK1.8之后)
在JDK1.7之前,达到扩容因子0.75后,就开始扩容,然后调用rehash,重新进行hash寻址放入新的位置。
在JDK1.8之后
- 达到扩容因子,进行二倍扩容;
- 然后优化了rehash,将数据分为两种情况,一种是位置不变,另一种就是数据的原本位置+数据的原来的容量。
hashmap在jdk1.8做了哪些优化?
数据结构优化:数据结构由数组+链表,到数组+链表+红黑树;
rehash算法优化:之前rehash会将所有的元素重排,优化之后重排部分算法;
链表的插入方式:之前是头插法,后来改为了尾插法;
扩容时间改变:之前是线扩容后插入,后来是插入后再判断是否需要扩容。
ConcurrentHashMap 线程安全的具体实现方式/底层具体实现?
JDK1.7之前:ConcurrentHashMap的底层数据结构是数组+链表,保持安全使用的是segment锁,这个继承于RTLock,有16个,每个锁锁一个HashEntry数组,HashEntry本身就是一个链表的结构,如果要处理信息必须得先拿到这个锁才可以。
jdk1.8之后,改成了用syn+CAS锁保持安全,每个锁锁住的是链表头,或者是根节点,锁粒度更细。
ConcurrentHashMap 为什么 key 和 value 不能为 null?
避免二义性的出现,要求更加严格。