剖析面试最常见问题之Java集合
1.说说list、set和map的区别?
1.list是有序可重复;</br>
2.set是无序不可重复;</br>
3.map是key-value的形式,value可以重复,但key是唯一的。
2.Arraylist 与 LinkedList 区别?
1.从数据结构上看,ArrayList是由Object数据组成,而LinkedList是由双向链表(JDK1.6之前为双向循环链表)组成。</br>
2.从线程安全上看,这两个都不是同步的,属于线程不安全;</br>
3.插入和删除是否受元素位置的影响:因为ArrayList是底层数据结构是Object数组,因此当插入或者删除末尾的元素的时候时间复杂度是O(1),可以如果插入和删除元素是位于中间的话,那么时间复杂度是O(n),位于该元素的之后的元素都要向后移动或者往前移动;而LinkedList底层数据结构是双向链表,插入和删除的时间复杂度都是O(1),只需要修改其前驱节点和后驱节点即可。</br>
4.是否支持快速随机访问:ArrayList支持快速随机访问而LinkedList链表不支持,因为ArrayList的底层数据结构是数组,而数组是由索引的,因此ArrayList可以使用get(int index)方法来获取指定的元素。</br>
5.内存空间占用:ArrayList空间的浪费主要是list列表结尾会预留一定的容量空间,而LinkedList主要是创建每个元素的时候,需要比较大的空间,因为他需要存储直接前驱和直接后继以及数据。
3.RandomAccess接口
该接口只是一种标识并不需要进行实现,他只是代表实现的类有快速随机访问的能力。
4.总结一下list的遍历方式:
1.实现了RandomAccess接口的类,建议优先使用普通for循环,其次增强for循环(foreach);</br>
2.而没有实现RandomAccess接口的类,建议使用iterator迭代器(增强for循环其底层也是使用iterator迭代器),对于Size比较多的,千万不要使用普通for循环。
5.ArrayList 与 Vector 区别呢?为什么要用Arraylist取代Vector呢?
1.ArrayList不是线程安全的(不同步的)而Vector是线程安全(同步的)的 ,但是性能比较低。
6.HashMap 和 Hashtable 的区别
1.从线程安全上面来看,HashMap不是线程安全的,HashTable是线程安全的,其底下都是synchronize方法,性能上面有点低,一般不建议使用HashTable;</br>
2.对于NuLL的key和value,HashMap允许其中一个键为null,键所对应的值可以多个为null,而HashTable不允许其中的某个键或者某个值为null,如果为空会报错(NULLPointException)。</br>
3.底层数据结构:HashTable是由链表散列组成,也是数组+链表结构,但是HashMap自从JDK1.8之后是也是由链表散列组成,但是当链表之后的数组大于8的时候也就是阈值大于8的时候,链表将会变成红黑树,以减少搜索的时间。
4.初始化容量大小和每次扩容的大小的不同:hashMap初始化容量大小为16,每次扩容都是以2的幂次方进行扩容(2的两倍进行扩容),而HashTable初始化容量大小是11,每次扩容都是2n+1,同时他们也可以自定义初始化的容量,HashTable默认是初始化时自定的大小,之后以2n+1的大小进行扩容,而HashMap会将自定义的大小通过tableSizeFor(int cap)将自定义的大小转换成最小的2的幂次方,以方便当阈值大于8的时候更容易转换成红黑树。</br>
7.HashMap 和 HashSet区别
1.HashSet的底层就是基于HashMap实现的;</br>
2.HashMap是实现了Map接口,并且存储的是key-value对;而HashSet实现的是Set接口,存储的是不重复且无序的对象的集合;</br>
3.HashSet是根据对象的hashCode()方法获取哈希码,并且通过equals()方法来比较对象是否相等的,而HashMap是根据key来获取哈希码;
4.HashMap是使用put(K key, V value)方法来存储key和value,而HashSet是使用add(E e)方法来像集合中添加对象。
8.HashSet是如何检查重复的?
HashSet会使用hashCode()方法生成哈希码,通过哈希码去寻找是否有相同的元素,如果没有哈希码相同的元素的话就将该对象存储HashSet中去,否则就要调用equals()方法比较哈希码相同的元素是否是相同的,如果相同的话就不将该对象存入HashSet中去,否则将该对象存入该HashSet中去,因此使用了hashCode()方法可以减少调用equals方法,可以提高代码的执行效率和性能。
9.HashMap的底层实现
1.JDK1.8之前HashMap的底层是由链表加上数组组成的,也就是链表散列。HashMap通过key的HashCode经过扰动函数处理过得到的hash值,然后通过(n-1)&hash判断当前元素的存放的位置(这里的n指的是集合的长度),如果当前位置存在元素的话,就判断当前位置存在元素的话,就将当前位置的元素和该元素的hash值和key进行对比,如果相同就直接覆盖,否则就将使用拉链法解决冲突。</br>
2.扰动函数指的就是HashMap中的hash()方法,使用hash()方法也就是扰动函数就是防止一些实现比较差的hashCode()方法,换句话说就是使用扰动函数可以减少碰撞。</br>
3.所谓的“拉链法”就是链表和数组的结合,也就是创建一个链表数组,数组中的每一格都是一个链表,如果遇到哈希冲突就将冲突的元素放在链表中。</br>
4.JDK1.8之后,HashMap中在解决哈希冲突的时候,如果链表的长度也就是阈值大于8的话,会将链表转换成红黑树,以减少搜索时间。</br>
5.TreeMap、TreeSet、HashMap的底层都是使用了红黑树,红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在一定情况下会退化成线性结构。
10.什么是二叉查找树和红黑树?
1.二叉查找树就是如果左子树不为空的时候,左子树都小于等于根节点;如果右子树不为空的话,右子树大于等于根节点,也就是说是树形结构而且从左到右依次递减并且其左右子树都是二叉查找树。</br>
2.
11.HashMap的长度为什么是2的幂次方?
1.为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。,Hash 值的范围值-2147483648到2147483647,前后加起来大概40亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个40亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“ (n - 1) & hash”。(n代表数组长度)。这也就解释了 HashMap 的长度为什么是2的幂次方。</br>
2.这个算法应该如何设计呢?</br>
我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。
12.HashMap多线程操作导致死循环问题?
1.主要原因是在多并发条件下的rehash(Hash表这个容器当有数据要插入时,都会检查容量有没有超过设定的thredhold(总容量),如果超过,需要增大Hash表的尺寸,但是这样一来,整个Hash表里的无素都需要被重算一遍。这叫rehash)会造成元素之间形成一个循环链表。不过在JDK1.8条件下解决了,但是还是可能会造成数据丢失因此在多线程的环境下不建议使用HashMap而是使用ConcurrentHashMap。
13.ConcurrentHashMap 和 Hashtable 的区别?
1.ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。</br>
2.底层数据结构:JDK1.8之前ConcurrentHashMap使用的是数组+链表,JDK1.8(包括)之后都是使用数组+链表/红黑树,HashTable使用的是数组+链表。</br>
3.实现线程安全的方式:在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。Hashtable使用synchronize保证线程安全,效率非常低下(多线程访问会造成其他线程的阻塞和轮询)。
14.comparable 和 Comparator的区别?
1.comparable接口实际上是出自java.lang包 它有一个 compareTo(Object obj)方法用来排序;我们可以使用Comparable接口中的compareTo方法使原本无法比较的对象通过某种自身条件来排序(类实现comparable接口并重写compareTo(Object obj)方法)。</br>
2.comparator接口实际上是出自 java.util 包它有一个compare(Object obj1, Object obj2)方法用来排序,利用
Collections.sort(arrayList, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
//默认是从小到大的,但是这个是从大到小
return o2.compareTo(o1);
}
});
进行排序。</br>
15.集合框架底层数据结构总结:
1.List:有序可重复</br>
Arraylist: Object数组;</br>
Vector: Object数组;</br>
LinkedList: 双向链表(JDK1.6之前为循环链表,JDK1.7取消了循环)。</br>
2.Set:</br>
HashSet:基于HashMap实现,无序不重复;</br>
LinkedHashSet:继承与HashSet,并且其内部是通过LinkedHashMap来实现的,有点类似于我们之前说的LinkedHashMap 其内部是基于 Hashmap 实现一样,不过还是有一点点区别的。</br>
TreeSet:红黑树(自平衡的二叉排序树),有序不重复;</br>
3.Map:</br>
HashMap:DK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。</br>
LinkedHashMap: LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。</br>
HashTable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的。</br>
TreeMap:红黑树(自平衡的排序二叉树)。
16.集合的选用
主要根据集合的特点来选用,比如我们需要根据键值获取到元素值时就选用Map接口下的集合,需要排序时选择TreeMap,不需要排序时就选择HashMap,需要保证线程安全就选用ConcurrentHashMap.当我们只需要存放元素值时,就选择实现Collection接口的集合,需要保证元素唯一时选择实现Set接口的集合比如TreeSet或HashSet,不需要就选择实现List接口的比如ArrayList或LinkedList,然后再根据实现这些接口的集合的特点来选用。
参考引用:来自JavaGuide公众号,侵删!