JAVA面试—容器(2021面试必备)(最全+详细答案)学好容器一篇就够了!!
文章目录
集合容器概念
集合是什么?
- 在java中也存在各种各样的‘容器’,我们把java中所有的‘容器’的总称叫做集合
- 数组存储的是同一个类型的数据,集合可以存储不同类型的数据。
集合的好处
1.容量是可以增长的;
2.java给容器内部提供了高性能的数据结构和算法,提高了程序速度和质量。
3.集合是java封装好的,有很高的代码复用性和可操作性。
集合和数组的区别
1.数组是固定长度的,一旦开辟,无法扩容;集合可随着元素增多而自动扩容的。
2.数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用数据类型。
3.数组存储的元素必须是同一个数据类型;集合存储的对象可以存在不同数据类型。
常用的集合有哪些?
1.Collection接口下有:Set接口和List接口
2.Map接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap,LinkedHashMap等
3.Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet,SortedSet等
4.List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等
List,Set,Map三者有什么区别?
数据结构结构上的区别(后面会详细解析各个容器):
1.List和Set是存储单列数据的集合,Map是存储键值对这样的双列数据的集合;
2.List中存储的数据是有顺序的,并且值允许重复;Map中存储的数据是无序的,它的键是不允许重复的,但是值是允许重复的;Set中存储的数据是无顺序的,并且不允许重复,但元素在集合中的位置是由元素的hashcode决定,即位置是固定的(Set集合是根据hashcode来进行数据存储的,所以位置是固定的,但是这个位置不是用户可以控制的,所以对于用户来说set中的元素还是无序的)。
List接口有三个实现类:
1.LinkedList
基于链表实现,所以有链表的特性,增删快,查找慢;
2.ArrayList
基于数组实现,所以有数组的特性,线程不安全,增删慢,查找快;
3.Vector(不常用)
基于数组实现,方法都加了synchronized关键字来保证同步,所以线程是安全的但是效率相应的降低低了,增删慢,查找慢;
Map接口有四个实现类:
1.HashMap
线程不安全,支持 null 值和 null键。JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8之后HashMap由数组+链表+红黑树组成的
2.HashTable
方法都加了synchronized关键字来保证同步,线程安全,效率低,不支持 null 值和 null 键;
3.LinkedHashMap
是 HashMap 的一个子类,在HashMap的基础上加入了链表,所以保存插入顺序;
4.TreeMap接口
TreeMap,它保存的记录会根据键值排序,默认是键值的升序排序。也可以重写comparator方法来根据value进行排序。
Set接口有三个实现类:
1.HashSet
HashSet 实现了 Set 接口。基于 HashMap 来实现的,是一个不允许有重复元素的集合。允许有 null 值。HashSet 是无序的,即不会记录插入的顺序。HashSet 是线程不安全的。
2.LinkedHashSet
继承于 HashSet,同时又基于 LinkedHashMap 来进行实现,底层使用的是 LinkedHashMap。
3.TreeSet
使用红黑树的数据结构,底层使用TreeMap来实现。
Collection的各个接口以及他们之间的区别
迭代器
Iterator 接口java中提供遍历 Collection 的接口。我们可以从一个 Collection 中使用迭代器方法来获取迭代器并对相应的容器进行遍历。
ArrayList<String> a=new ArrayList<>();
a.add("我");
a.add("们");
a.add("学");
a.add("java");
Iterator it=a.iterator();
while(it.hasNext())
{
System.out.println(it.next());//输出我们学java
}
Iterator 和 ListIterator 有什么区别?
1.两种方法都能够用来遍历集合元素。
2.Iterator在使用过程中,不能对集合元素进行添加操作。
3.Iterator可以在所有Collection 集合中使用,而ListIterator只能在List类型与子类型。
4.listIterator有add方法,可以向集合中添加元素,而iterator不支持。
5.listiterator和iterator都可以顺序遍历元素, 但是listiterator可以逆向遍历元素。
6.listiterator可以定位当前的索引位置 iterator没有此功能。
7.listiterator可以对对象的修改,iterator只能用来遍历,不能修改对象。
List接口
ArrayList
什么是ArrayList?
ArrayList的实现原理其实就是数组但是与Java中的数组相比,ArrayList的容量能动态地随着元素的增长而进行扩容。
ArrayList的优点:
1.ArrayList 底层以数组实现,可以进行随机访问。因此查找速度非常快。
2.ArrayList能动态地随着元素的增长而进行扩容。
ArrayList 的缺点:
1.删除元素的时候,需要做进行元素复制操作。如果要复制的元素很多,那么就会消耗大量时间,值得注意的是,如果只在ArrayList的尾部进行删除,效率会非常高。
2.插入元素的时候,也需要做一次元素复制操作,如果要复制的元素很多,那么就会消耗大量时间,值得注意的是,如果只在ArrayList的尾部进行插入,效率会非常高。
所以,ArrayList比较使用于一些插入删除较少,查询较多的场景。
ArrayList数组扩容问题(重要)
假设我们有一个长度为10的数组,此时我们还需要新增元素,ArrayList会进行扩容。
1.重新定义一个1.5倍的数组。
2.然后把原数组的数据原封不动的复制到新数组中,然后把ArrauList的地址指向新数组(由于这一步的导致扩容效率严重降低)。
总结:
1.ArrayList创建对象时,若未指定集合大小初始化大小为0;若已指定大小,集合大小为指定的大小;
2.当第一次调用add方法时,集合大小扩容为10(若使用addAll方法添加元素,则初始化大小为10和添加集合长度的较大值)。
3.之后再调用add方法,先将集合扩大1.5倍,如果仍然不够,新长度为传入集合大小;
ArrayList是线程安全的么?
不是。ArrayList的方法里面没有synchronized等关键字保证线程安全。所以效率较高但是线程不安全。
LinkedList
什么是LinkedList?
LinkedList 的底层结构是一个带头/尾指针的双向链表,可以快速的对头/尾节点 进行操作,它允许插 入所有元素,包括 null。LinkedList同时实现了List接口和Deque接口,也就是收它既可以看作一个顺序容器,又可以看作一个队列(Queue),同时又可以看作一个栈(stack)。
LinkedList的优点:
1.LinkedList底层以链表实现,插入和删除时只需要添加或删除前后对象的引用,插入较快。
ArrayList 的缺点:
1.查询时,LinkedList需要遍历整个链表,不能进行随机查询。效率较低。
所以,ArrayList比较使用于一些插入删除较多,查询较少的场景。
LinkedList是线程安全的么?
不是。为追求效率LinkedList没有实现同步(synchronized)。
Vector
什么是Vector?
Vector类实现了一个动态数组。和ArrayList和相似这里不在赘述,区别后面会详细说明。
Vector是线程安全的吗?
是。Vector的方法都实现同步(synchronized)。所以是线程安全的。
ArrayList和LinkedList的区别?(重要)
1.数据结构实现:ArrayList 是动态数组的数据结构,而 LinkedList 是双向链表的数据结构实现,LinkedList比ArrayList更占内存,因为LinkedList的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。
2.随机访问效率:ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 ArrayList是基于数组的存储方式,可以直接通过下标进行访问,LinkedList是基于链表的存储方式,无法随机访问。
3.增加和删除效率:在非尾部的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其他数据的下标,但是只在尾部插入数据时,LinkedList由于要多new 一个节点的原因,效率上会低于 ArrayList 。
4内存空间占用:LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。
5.线程安全:ArrayList 和 LinkedList 都是没有实现同步(synchronized),也就是不保证线程安全;
总结:在需要频繁读集合中的元素的场景,推荐使用 ArrayList,而在插入和删除操作较多的场景,推荐使用 LinkedList。
Vector和ArrayList的区别?
这两个类都是基于数组实现并且可以动态扩容。
线程安全:Vector 使用了 Synchronized 来实现线程同步,是线程安全的,而 ArrayList 是非线程安全的。
性能:由于Vector 使用了 Synchronized 的原因所以ArrayList 在性能方面要优于 Vector。
扩容:ArrayList 和 Vector 都会根据实际的需要动态扩容,只不过在 Vector 扩容每次会变为原来的2倍,而 ArrayList 只会变为原来的1.5倍。
总结:Vector类的所有方法都是同步的。可以由两个线程安全地访问一个Vector对象、但是Vector的效率会降低。
Arraylist不是同步的,所以在不需要保证线程安全时时建议使用Arraylist。
需要注意的是:在当前,单线程使用Arraylist效率较高,在多线程时,我们有其他更好的容器,所以Vector不常用。
Set接口
HashSet
什么是HashSet?
在打开源码的一瞬间就找到了答案,注释里面作了清楚的写到了:This class implements the Set interface, backed by a hash table (actually a HashMap instance) ,HashSet内部包含了一个HashMap,HashSet和HashMap很相似,只是HashSet是一个不允许有重复元素的集合。但是set为什么能和map相似呢?
我们可以发现HashSet定义了一个成员变量PRESENT作为所有添加到HashSet中元素的值,即以添加到HashSet中的元素为Key,以此处定义的成员变量PRESENT为Value,组成Key-Value键值对,然后添加到HashMap中,这就是HashSet的原理。因为基于HashMap,所以HashSet的优点是查找,查询,删除都是非常快的,缺点就是不保证有序。
HashSet如何检查重复?HashSet是如何保证数据不可重复的?(重要)
这个问题需要我们打开源码:
调用HashMap的add方法会调用HashMap的put方法。
//HashSet实际就是对HashMap的操作。
public HashSet() {
map = new HashMap<>();
}
// 调用HashMap的add方法会调用HashMap的put方法。
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
然后我们打开HashMap的put方法
//HashMap的put方法调用了putVal方法。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
我们再跟踪putVal方法。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 当前对象的数组是null 或者数组长度时0时,则需要初始化数组
if ((tab = table) == null || (n = tab.length) == 0)
// 得到数组的长度 16
n = (tab = resize()).length;
// 如果通过hash值计算出的下标的地方没有元素,则根据给定的key 和 value 创建一个元素
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else { // 如果hash冲突了
Node<K,V> e; K k;
// 如果给定的hash和冲突下标中的 hash 值相等并且 (已有的key和给定的key相等(地址相同,或者equals相同)),说明该key和已有的key相同
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 那么就将已存在的值赋给上面定义的e变量
e = p;
// 如果以存在的值是个树类型的,则将给定的键值对和该值关联。
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 如果key不相同,只是hash冲突,并且不是树,则是链表
else {
// 循环,直到链表中的某个节点为null,或者某个节点hash值和给定的hash值一致且key也相同,则停止循环。
for (int binCount = 0; ; ++binCount) {
// 如果next属性是空
if ((e = p.next) == null) {
// 那么创建新的节点赋值给已有的next 属性
p.next = newNode(hash, key, value, null);
// 如果树的阀值大于等于7,也就是,链表长度达到了8(从0开始)。
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 如果链表长度达到了8,且数组长度小于64,那么就重新散列,如果大于64,则创建红黑树
treeifyBin(tab, hash);
// 结束循环
break;
}
// 如果hash值和next的hash值相同且(key也相同)
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 结束循环
break;
// 如果给定的hash值不同或者key不同。
// 将next 值赋给 p,为下次循环做铺垫
p = e;
}
}
// 通过上面的逻辑,如果e不是null,表示:该元素存在了(也就是他们呢key相等)
if (e != null) { // existing mapping for key
// 取出该元素的值
V oldValue = e.value;
// 如果 onlyIfAbsent 是 true,就不要改变已有的值,这里我们是false。
// 如果是false,或者 value 是null
if (!onlyIfAbsent || oldValue == null)
// 将新的值替换老的值
e.value = value;
// HashMap 中什么都不做
afterNodeAccess(e);
// 返回之前的旧值
return oldValue;
}
}
// 如果e== null,需要增加 modeCount 变量,为迭代器服务。
++modCount;
// 如果数组长度大于了阀值
if (++size > threshold)
// 重新散列
resize();
// HashMap 中什么都不做
afterNodeInsertion(evict);
// 返回null
return null;
}
花里胡哨,看了半天没看出来问题,其实我们只要定位关键代码即可(无关代码已删除主要看注释):
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//这个if是根据传入的hash值算出存储位置,如果该位置为空代表没有元素。肯定是可以直接添加的,通过tab[i] = newNode(hash, key, value, null);
添加。
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
/*
*走到这里代表该存储位置和需要存储的元素算出的储存位置重复,
*但是有可能并不是同一个元素,所以我们先比较2个元素的hash值,
*如果hash值不同(前面只是根据hash值算出的位置重复并不是hash值相同),
*则在后面p.next = newNode(hash, key, value, null);进行添加元素。
*如果hash值还是相同,则会调用key.equals(k)方法进行对比,如果equals的结果还是相同,则不会将该元素加入集合
*/
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
总结:
情况1: 如果通过hash值算出元素存储的位置目前没有任何元素存储,那么该元素可以直接存储到该位置上。
情况2: 如果算出该元素的存储位置目前已经存在有其他的元素了,那么会比较hash值是否相同,如果hash值还是相同(通常情况下,你通过hash值算出的位置相同,hash值大部分情况下是相同的,只不过保险起见比较一下),那么会调用该元素的equals方法与该位置的元素再比较一次,如果equals返回的是true,那么该元素与这个位置上的元素就视为重复元素,不允许添加,如果equals方法返回的是false,那么添加该元素。
LinkedHashSet
什么是LinkedHashSet?
LinkedHashSet继承了HashSet,LinkedHashSet可以看作HashSet的一个“扩展版本”,HashSet并不管什么顺序,不同的是LinkedHashSet会维护“插入顺序”。HashSet内部使用HashMap对象来存储它的元素,而LinkedHashSet内部使用LinkedHashMap对象来存储和处理它的元素。解决了HashSet不保证顺序的缺点。
LinkedHashSet是如何维护插入顺序的?(重要)
LinkedHashSet使用LinkedHashMap对象来存储它的元素,插入到LinkedHashSet中的元素实际上是被当作LinkedHashMap的键保存起来的。
LinkedHashMap的每一个键值对都是通过内部的静态类Entry<K, V>实例化的。这个 Entry<K, V>类继承了HashMap.Entry类。
这个静态类增加了两个成员变量,before和after来维护LinkedHasMap元素的插入顺序。这两个成员变量分别指向前一个和后一个元素,这让LinkedHashMap也有类似双向链表的表现。
简单来说,在HashSet的基础上增加了前后2个指针,使得LinkedHashSet能够记录下插入顺序。
private static class Entry<K,V> extends HashMap.Entry<K,V>
{
Entry<K,V> before, after;//在 HashMap的基础上新增的2个变量
Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
super(hash, key, value, next);
}
}
TreeSet
TreeSet
TreeSet也有着Set集合独特的元素唯一特点,因为使用了红黑树的结构。
TreeSet在保证元素唯一的前提下还能使元素自动进行排序。
优点是:查询等操作速度较快(比较与list等),而且保证了元素是有序的。
缺点是:查询等操作速度比同类较慢(比HashSet慢)。
TreeSet是如何保证有序的?排序规则是什么?
TreeMap 的实现就是红黑树数据结构,也就说红黑树是一棵自平衡的排序二叉树,这样就可以保证快速检索指定节点。(红黑树是什么需要读者自己去搜索)。
TreeSet有2种排序规则。
1.自然顺序(Comparable)
TreeSet类的add()方法中会把存入的对象转为为Comparable类型
调用对象的compareTo()方法和集合中的对象比较(当前存入的是谁,谁就会调用compareTo方法)
根据compareTo()方法返回的结果进行存储
2.比较器顺序(Comparator)
创建TreeSet的时候可以自己指定 一个Comparator
如果传入了Comparator的子类对象, 那么TreeSet就会按照比较器中的顺序排序
add()方法内部会自动调用Comparator接口中compare()方法排序
注意:一个是java内部提供的默认排序,一个是按照自己的需求进行排序。
Map接口
一定要学好HashMap!!!!
一定要学好HashMap!!!!
一定要学好HashMap!!!!
学好HashMap,其他map类都和HashMap差不多
HashMap
什么是HashMap?
HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。
HashMap 继承于AbstractMap,实现了Map、Cloneable等接口。
HashMap 的实现不是同步的,这意味着它不是线程安全的。它的key、value都可以为null。此外,HashMap中的映射不是有序的。
HashMap的数据结构?
HashMap在JDK1.7版本中是数组+链表,JDK1.8的是数组+链表+红黑树。
HashMap的底层原理(重要)
1.调用hashCode方法计算K的hash值,然后结合数组长度,计算得数组下标;
2.调整数组大小(当容器中的元素个数大于capacity*loadfactor时,容器会进行扩容resize为2n);
- (1)如果K的hash值在HashMap中不存在,则执行插入。
(2)如果K的hash值在HashMap中存在,且它们两者equals返回true,则更新键值对;
(3)如果K的hash值在HashMap中存在,且它们两者equals返回false,则插入链表的尾部或者红黑树中。
注意:在JDK1.7版本中使用的是头插法!!并且没有红黑树!!
在JDK1.8版本中使用的是尾插法!!
头插法有个致命的缺点:如果在扩容采用的是“头插法”,会导致同一索引位置的节点在扩容后顺序反掉,从而导致死循环。采用的是“尾插法”,扩容后节点顺序不会反掉,不存在死循环问题。
HashMap是怎么解决哈希冲突的?(重要)
什么是哈希冲突?当2个对象通过hashCode方法计算出的hash值相同时就会发生哈希冲突。HashMap是怎么解决哈希冲突的呢?
在JDK1.7版本中:
当多个对象的hash值相同时。HashMap会在发生冲突的位置建立一个链表来达到解决哈希冲突的。
这样做的弊端非常明显:当我们同一位置的链表太长时,我们进行查询就会非常的慢(每次都要遍历整个链表从而使查询退化成o(n)的复杂度)。
在JDK1.8版本中:
当多个对象的hash值相同时。HashMap会在发生冲突的位置建立一个链表来达到解决哈希冲突的。但是当同一个位置链表上的节点大于8的时候,如果此时数组长度大于等于 64,则会将链表节点转红黑树节点;而如果数组长度小于64,则不会将链表转红黑树,而是会进行扩容。当同一个位置的节点在删除后只有6个或者6个以下,并且该索引位置的节点为红黑树节点,会将红黑树节点退化成链表节点。下图有有个红黑树节点,有一个链表节点。
这么做的优点很明显。
当同一个位置有很多节点时,我们使用红黑树查询的速度是o(log n)的,在速度上是碾压链表的查询速度o(n)的。而且也具有之前链表存储节点的其他优点。
HashMap扩容的过程?
默认情况下,HashMap数组默认大小为16,那么当HashMap中的元素个数超过16×0.75=12(0.75为装载因子意思是数组中存储的元素只能占数组长度的百分之75)的时候,就把数组的大小扩展为2×16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这一步严重影响效率。当HashMap中的其中一个链表的对象个数如果达到了8个,此时如果数组长度没有达到64,那么HashMap会先扩容解决,因为扩容也能使元素分散一些,减少查询时间。
注意:HashMap默认大小为16,每次扩容为2倍,所以容量始终为2的n次方。因为在使用不是2的幂的数字的时候,Length-1的值是所有二进制位全为1,这种情况下,index的结果等同于HashCode后几位的值。 只要输入的HashCode本身分布均匀,Hash算法的结果就是均匀的。 这是为了实现均匀分布。
HashTable
什么是 HashTable?
HashMap是基于哈希表实现的,每一个元素是一个key-value对,其内部通过单链表解决冲突问题,容量不足时,同样会自动增长。与HashMap很相似,这里不再赘述。后面会详细介绍 HashTable和 HashMap的区别。
LinkedHashMap
什么是LinkedHashMap?
LinkedHashMap继承自HashMap,它的多种操作都是建立在HashMap操作的基础上的。同HashMap不同的是,LinkedHashMap维护了一个Entry的双向链表,保证了插入的Entry中的顺序。这也是Linked的含义。
HashMap和双向链表合二为一即是LinkedHashMap。LinkedHashMap是基于HashMap实现的,更准确地说,它是一个将所有Node节点链入一个双向链表的HashMap。LinkedHashMap和HashMap的扩容,处理冲突机制基本差不多,后面会详细说到他们之间的区别。
TreeMap
什么是TreeMap?
1.TreeMap存储K-V键值对,通过红黑树实现;
2.TreeMap继承了NavigableMap接口,NavigableMap接口继承了SortedMap接口,可支持一系列的导航定位以及导航操作的方法,当然只是提供了接口,需要TreeMap自己去实现;
3.TreeMap实现了Cloneable接口,可被克隆,实现了Serializable接口,可序列化;
4.TreeMap因为是通过红黑树实现,红黑树结构支持排序,默认情况下通过Key值的自然顺序进行排序;
5.TreeMap 的实现就是红黑树数据结构,也就说红黑树是一棵自平衡的排序二叉树,这样就可以保证快速检索指定节点。(红黑树是什么需要读者自己去搜索)。
6.TreeSet有2种排序规则。
1.自然顺序(Comparable)
TreeSet类的add()方法中会把存入的对象转为为Comparable类型
调用对象的compareTo()方法和集合中的对象比较(当前存入的是谁,谁就会调用compareTo方法)
根据compareTo()方法返回的结果进行存储
2.比较器顺序(Comparator)
创建TreeSet的时候可以自己指定 一个Comparator
如果传入了Comparator的子类对象, 那么TreeSet就会按照比较器中的顺序排序
add()方法内部会自动调用Comparator接口中compare()方法排序
注意:一个是java内部提供的默认排序,一个是按照自己的需求进行排序。
HashMap和Hashtable的区别
1:HashMap和Hashtable继承的父类不同
HashMap继承自AbstractMap类。但二者都实现了Map接口。
Hashtable继承自Dictionary类,Dictionary类是一个已经被废弃的类。父类都被废弃,子类的地位可想而知。
2、HashMap线程不安全,HashTable线程安全
在hashmap的put方法调用addEntry()方法,假如A线程和B线程同时对同一个数组位置调用addEntry,两个线程会同时得到现在的头结点,然后A写入新的头结点之后,B也写入新的头结点,多线程时,存在同时修改的操作就是线程不安全的。
3.是否允许null值
Hashmap是允许key和value为null值的HashTable键值对都不能为空,否则抛出空指针异常。
4.计算hash值方式不同
HashMap有个hash方法重新计算了key的hash值,因为hash冲突变高,所以对计算hash值进行了优化:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
注意:这里的HashMap计算hash值,先调用hashCode方法计算出来一个hash值,再将hash与右移16位后相异或,从而得到新的hash值。
Hashtable通过调用hashCode()得到hash值就为最终hash值。
5.扩容方式不同
HashMap 哈希扩容为原容量的2倍,而且结果一定是2的幂次倍,而且每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入;
而Hashtable扩容为原容量2倍加1;
6.解决哈希冲突方式不同
在HashTable中, 都是以数组+链表方式存储。
在jdk1.8版本之后HashMap,以数组+链表+红黑树存储详情可以看HashMap那里。
LinkedHashMap和HashMap的区别
HashMap根据键的HashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度,遍历时,取得数据的顺序是完全随机的。 HashMap最多只允许一条记录的键为Null,允许多条记录的值为 Null。
LinkedHashMap保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的。在遍历的时候会比HashMap慢,不过有种情况例外:当HashMap容量很大,实际数据较少时,遍历起来可能会比LinkedHashMap慢。因为LinkedHashMap的遍历速度只和实际数据有关,和容量无关,而HashMap的遍历速度和他的容量有关。
TreeMap和HashMap的区别
1、TreeMap和HashMap中元素的顺序
HashMap是通过hash值对其内容进行快速查找的;HashMap中的元素是没有顺序的;
TreeMap中所有的元素都是有一固定顺序的,如果需要得到一个有序的结果,就应该使用TreeMap;
2、HashMap和TreeMap都不是线程安全的;
3、TreeMap和HashMap继承的父类不同
HashMap继承AbstractMap类;覆盖了hashcode() 和equals() 方法,以确保两个相等的映射返回相同的哈希值;
TreeMap继承SortedMap类;保持键值的有序顺序;
4、TreeMap和HashMap的底层数据结构不同
HashMap:基于hash表实现的;使用HashMap要求添加的键类重写了hashcode() 和equals() ;
TreeMap:基于红黑树实现的;红黑树总是处于平衡的状态;
其他有关于容器的面试题
Collection 和 Collections 有什么区别?
Collection是接口,List和Set是其子接口。
Collections是算法,提供了对集合进行排序、遍历等多种算法的实现。
哪些集合类是线程安全的?
常见的集合:
List中的Vector
Map中的Hashtable
并发编程中的一些集合:
ConcurrentHashMap:线程安全的HashMap的实现
CopyOnWriteArrayList:线程安全且在读操作时无锁的ArrayList
CopyOnWriteArraySet:基于CopyOnWriteArrayList,不添加重复元素
ArrayBlockingQueue:基于数组、先进先出、线程安全,可实现指定时间的阻塞读写,并且容量可以限制
LinkedBlockingQueue:基于链表实现,在高并发读写操作多的情况下,性能明显优于ArrayBlockingQueue
如何实现数组和 List 之间的转换?
List转成为数组,调用ArrayList.toArray()方法,数组转化为list,调用Arrays.asList()方法。当然,你也可以手动转换。
Fail-fast和Fail-safe
快速失败(fail—fast):
-
在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出Concurrent Modification Exception。
-
迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。
安全失败(fail-safe):
-
采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。
原理: -
由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception,例如CopyOnWriteArrayList。