面试整理:java的集合框架

 

 

1.集合接口?

Collection为集合层级的根接口。一个集合代表一组对象,这些对象即为它的元素。

Collection接口的子接口包括:Set接口和List接口。Set是一个不能包含重复元素的集合。

List是一个有序集合,可以包含重复元素。

Map是一个将key映射到value的对象。一个Map不能包含重复的key:每个key最多只能映射一个value。

 

集合的实现类?

Map接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及LinkedHashMap等

Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等

List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等

 

2.List接口

Vector 和 ArrayList 的内部结构是以数组形式存储的,因此非常适合随机访问,但非尾部的删除或新增性能较差,比如我们在中间插入一个元素,就需要把后续的所有元素都进行移动。

LinkedList 插入和删除元素效率比较高,但随机访问性能会比以上两个动态数组慢。

假如元素的大小是固定的,而且能事先知道,我们就应该用Array而不是ArrayList。

 

查找速度问题?

ArrayList是基于索引的数据接口,它的底层是数组。它可以以O(1)时间复杂度对元素进行随机访问。LinkedList是以元素列表的形式存储它的数据,每一个元素都和它的前一个和后一个元素链接在一起,在这种情况下,查找某个元素的时间复杂度是O(n)。

 

插入/删除问题?

相对于ArrayList,LinkedList的插入,添加,删除操作速度更快,因为当元素被添加到集合任意位置的时候,不需要像数组那样重新计算大小或者是更新索引。

指定位置进行插入时,LinkedList的速率要优于ArrayList。ArrayList是基于数组的增加,当在指定位置进行一个插入时需要移动原有数据位置;LinkedList是基于双向链表的增加,数据在插入时只需要把指针移到对应的节点即可。

数据在尾部追加时,由于ArrayList是扩容的方式,LinkedList是需要新建立节点。当数据量很大的时侯new节点的时间会大于扩容的时间,所以ArrayList插入的速度就会优于LinkedList。

 

线程安全与扩容问题?

Vector 是 Java 早期提供的动态数组,它使用 synchronized 来保证线程安全,如果非线程安全需要不建议使用,毕竟线程同步是有性能开销的;

ArrayList 是最常用的动态数组,本身并不是线程安全的,因此性能要好很多,与 Vector 类似,它也是动态调整容量的,只不过 Vector 扩容时会增加 1 倍,而 ArrayList 会增加 50%;

LinkedList 是双向链表集合,因此它不需要像上面两种那样调整容量,它也是非线程安全的集合。

扩容步骤:1、扩容,把原来的数组复制到另一个内存空间更大的数组中

2、添加元素,把新元素添加到扩容以后的数组中。

int newCapacity = oldCapacity + (oldCapacity >> 1);使用右移运算符,使原来长度的一半 再加上原长度也就是每次扩容是原来的1.5倍

 

3.Set接口

HashSet是由一个hash表来实现的,它的元素是无序的。add(),remove(),contains()方法的时间复杂度是O(1)。

TreeSet是由一个树形的结构来实现的,它里面的元素是有序的。add(),remove(),contains()方法的时间复杂度是O(logn)。

LinkedHashSet 底层数据结构由哈希表和链表组成,链表保证了元素的有序即存储和取出一致,哈希表保证了元素的唯一性。

 

HashSet的底层实现?

HashSet的底层实现就是HashMap,HashSet只是实现了Set接口并且把数据作为K值, 而value是一个static final的Object对象标识。

HashSet存储元素的顺序是按照哈希值来存的,哈希值是通过元素的hashcode方法获得,HashSet通过计算hashCode值来确定元素在内存中的位置。一个hashCode位置上可以存放多个元素。

 

HashSet是如何保证数据不可重复的?

HashSet中add方法调用的是底层HashMap中的put()方法,而如果是在HashMap中调用put,首先会判断key是否存在,如果key存在则修改value值,如果key不存在这插入这个key-value。

而在set中,因为value值没有用,不存在修改value值,因此往HashSet中添加元素,首先判断元素(key)是否存在,如果不存在这插入,如果存在不插入,这样HashSet中就不存在重复值。

 

TreeSet的底层实现?

TreeSet是基于TreeMap实现的,对新add()的对象按照指定的顺序排序(升序、降序),每增加一个对象都会进行排序,将对象插入的二叉树指定的位置。

存入元素的时候,它创建了一个树,第一个元素就是树的根节点,后面的元素依次从树的根节点开始向后比较(创建比较器,利用comparator()方法进行比较),小的就往左边放,大的就往右边放,而相同的就不放进去(实现了唯一性)。取出元素的时候,它采用前序遍历的方法(根节点 左子树 右子树)遍历整个树,达到有序。

 

TreeSet的排序原理?

自然排序(元素具备比较性)。根据元素的自然顺序对元素进行排序 。自然排序需要实现comparable接口中的compareTo()方法,在compareTo方法中定义规则

构造器排序(集合具备比较性)。根据TreeSet的构造方法接收一个比较器接口的子类对象Comparator。

具体情况取决于使用的构造方法。无参就是自然排序,否则就是比较器排序

 

4.Map接口

HashMap是数组和链表的组合结构,数组是一个Entry数组,entry是k-V键值对类型。

数组的特点是:寻址容易,插入和删除困难;链表的特点是:寻址困难,但插入和删除容易;

HashMap采用哈希表的存储结构,所以里面的数据是无序但是唯一的,实现唯一的方式就是重写 Hashcode和equals方法。

HashMap的增删改查方式比较简单,都是遍历,替换。有一点要注意的是key相等时,替换元素,不相等时连成链表。链表主要是为了解决哈希冲突而存在的。

链表法就是将相同hash值的对象组织成一个链表放在hash值对应的槽位;

 

TreeMap采用的是二叉树的存储方式里面的数据是唯一而且有序的,而且一般是按升序的方式排列,要实现comparable接口并且重写compareTo的方法用来实现它的排序。

HashTable和HashMap的实现原理几乎一样,差别是HashTable不允许key和value为null;HashTable是线程安全的。

LinkedHashMap是HashMap的一个子类,在原来hashmap基础上将所有的节点依据插入的次序另外连成一个双链表,用来保持顺序,可以使用它实现LRU缓存。

可以认为是HashMap+LinkedList,即它既使用HashMap操作数据结构,又使用LinkedList维护插入元素的先后顺序。

LinkedHashMap的特点和访问顺序?

特点:比HashMap速度更快而且还可以保证唯一性和有序性,它的实现方式是哈希表和链表的结合。比HashMap增加了双链表的结果(即节点中增加了前后指针),其他处理逻辑与HashMap一致。

LinkedHashMap支持两种顺序插入顺序 、 访问顺序:LinkedHashMap有5个构造方法,其中4个都是按插入顺序,只有一个是可以指定按访问顺序。

插入顺序:先添加的在前面,后添加的在后面。修改操作不影响顺序

访问顺序:所谓访问指的是get/put操作,对一个键执行get/put操作后,其对应的键值对会移动到链表末尾,所以最末尾的是最近访问的,最开始的是最久没有被访问的,这就是访问顺序。

 

HashMap的工作原理是什么?

HashMap是以键值对(key-value)的形式存储元素的。

当需要存储一个Entry对象时,会根据hash算法来决定在其数组中的位置,在根据equals方法决定其在该数组位置上的链表中的存储位置;

当需要取出一个Entry对象时,也会根据hash算法找到其在数组中的存储位置, 在根据equals方法从该位置上的链表中取出Entry。

当调用put()方法的时候,HashMap会计算key的hash值,然后把键值对存储在集合中合适的索引上。如果key已经存在了,value会被更新成新值。

 

HashMap的put与get过程?

put:

先计算key的hash值,看数组中有没有这个key;

如果没有,看是否需要resize,然后新加数组元素;

如果有,说明key值重复(hash碰撞),判断是树还是链表;

如果是链表,替换原来的value,把新值放在头部;

如果长度达到就把链表换成树,存在树结构中;

get:

先计算key的hash值,看数组中有没有这个key;

没有值返回null,有值就去查链表或者树;

利用equals方法比较判断值。

 

HashMap和Hashtable有什么区别?

线程是否安全: HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本都经过 synchronized 修饰。

效率: 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它;

对Null key 和Null value的支持: HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛出 NullPointerException。

初始容量大小和每次扩充容量大小的不同 : ①创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小。

底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。

 

jdk1.8之后变动?

jdk1.8之前:

存储结构:数组+链表,时间复杂度取决于链表的长度为 O(n)。

扩容之后的插入方式:头插法,有可能会使链表成环。

jdk1.8之后:

存储结构:数组+链表+红黑树,当链表中的元素超过了 8 个以后, 会将链表转换为红黑树,可以降低时间复杂度为 O(logN)。

扩容之后的插入方式:尾插法,会避免链表成环。

 

HashMap数组长度为什么保证是2的幂次方呢?

当length是2的幂次方时,h%length=h&(length-1)是数组下标。

为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。

算法的设计采用%取余的操作来实现。取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)

并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。

 

hashMap扩容(resize方法)

(1)计算hash值有两个因素:Capacity:HashMap当前长度。LoadFactor:负载因子,默认值0.75f。当存储的容量大于负载因子与长度乘积时,需要扩容。

(2)分为两步:扩容:创建一个新的Entry空数组,长度是原数组的2倍。ReHash:遍历原Entry数组,把所有的Entry重新Hash到新数组。

java8之前头插法:使用单链表的头插入方式,同一位置上新元素总会被放在链表的头部位置,原有的值就顺推到链表中去,认为后来的值被查找的可能性更大一点,提升查找的效率。

java8之后尾插法:使用头插会改变链表的上的顺序,但是如果使用尾插,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了。

 

线程安全问题?

线程不安全问题:HashMap在put的时候,插入的元素超过了容量(由负载因子决定)的范围就会触发扩容操作,就是rehash,这个会重新将原数组的内容重新hash到新的扩容数组中,在多线程的环境下,存在同时其他的元素也在进行put操作,如果hash值相同,可能出现同时在同一数组下用链表表示,造成闭环,导致在get时会出现死循环,所以HashMap是线程不安全的。

Java7在多线程操作HashMap时可能引起死循环,原因是扩容转移后前后链表顺序倒置,在转移过程中修改了原来链表中节点的引用关系。

Java8在同样的前提下并不会引起死循环,原因是扩容转移后前后链表顺序不变,保持之前节点的引用关系。

即使不会出现死循环,但是通过源码看到put/get方法都没有加同步锁,多线程情况最容易出现的就是:无法保证上一秒put的值,下一秒get的时候还是原值,所以线程安全还是无法保证。

 

ConcurrentHashMap

JDK1.8之前,ConcurrentHashMap采用Segment + HashEntry的方式进行实现,该类包含两个静态内部类 HashEntry 和 Segment ;前者用来封装映射表的键值对,后者用来充当锁的角色;

Segment 是一种可重入的锁 ReentrantLock,每个 Segment 守护一个HashEntry 数组里得元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 锁。

HashEntry 和 HashMap 非常类似,唯一的区别就是其中的核心数据如 value ,以及链表都是 volatile 修饰的,保证了获取时的可见性。

JDK8中ConcurrentHashMap的实现使用的是锁分离思想,只是锁住的是一个node,而锁住Node之前的操作是基于在volatile和CAS之上无锁并且线程安全的。

put 过程:

尝试自旋获取锁。

如果重试的次数达到了 MAX_SCAN_RETRIES 则改为阻塞锁获取,保证能获取成功

将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry。

遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。

不为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容。

最后会解除在 1 中所获取当前 Segment 的锁。

get 过程:

只需要将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上。

 

JDK1.8中放弃了Segment臃肿的设计,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。

put 过程:

根据 key 计算出 hashcode 。

判断是否需要进行初始化。

即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。

如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。

如果都不满足,则利用 synchronized 锁写入数据。

如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。

get 方法:

根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。

如果是红黑树那就按照树的方式获取值。

就不满足那就按照链表的方式遍历获取值。

 

5.补充内容

Comparable 和Comparator区别?

Comparable是在集合内部定义的方法实现的排序,位于java.lang下。只包括一个方法compareTo。

int compareTo(T o);

Comparator是在集合外部实现的排序,位于java.util下。有compare和equals两个方法。

int compare(T o1, T o2); boolean equals(Object obj);

 

Collection 和 Collections 的区别?

Collection 是集合类的上级接口,继承它的主要有 List 和 Set;

Collections 是针对集合类的一个帮助类,它提供了一些列的静态方法实现,如 Collections.sort() 排序、Collections.reverse() 逆序等。

 

迭代器(Iterator)?

Iterator接口提供了很多对集合元素进行迭代的方法。每一个集合类都包含了可以返回迭代器实例的

迭代方法。迭代器可以在迭代的过程中删除底层集合的元素,但是不可以直接调用集合remove(Object Obj)删除,可以通过迭代器的remove()方法删除。

Iterator可用来遍历Set和List集合,Iterator对集合只能是前向遍历。

 

快速失败(fail-fast)和安全失败(fail-safe)的区别是什么?

Iterator的安全失败是基于对底层集合做拷贝,因此,它不受源集合上修改的影响。java.util包下面的所有的集合类都是快速失败的,而java.util.concurrent包下面的所有的类都是安全失败的。快速失败的迭代器会抛出ConcurrentModificationException异常,而安全失败的迭代器永远不会抛出这样的异常。

 

快速失败机制 “fail-fast”?

java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fast 机制。

例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。

原因:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。

解决办法:

1. 在遍历过程中,所有涉及到改变modCount值得地方全部加上synchronized。

2. 使用CopyOnWriteArrayList来替换ArrayList

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值