集合是面试的一个重灾区,每面必问!!!
废话不多说直奔主题,下面就针对具体的问题进行解释。
一、常用的集合有哪些?
常用的集合主要有List,Set和Map三大类。其中List和Set是实现了Collection接口,Map另立门户。
二、既然提到了Collection 那它和Collections有什么区别?
Collection集合类的上级接口,继承与他有关的接口主要有List和Set。
Collections是针对集合类的一个帮助类,他提供一系列静态方法实现对各种集合的搜索、排序、线程安全等操作。
三、Collections常用的方法有哪些?
1) 排序(Sort)
Collections.sort(list);使用sort方法可以根据元素的自然顺序 对指定列表按升序进行排序。列表中的所有元素都必须实现 Comparable 接口。2) 混排(Shuffling)
Collections.Shuffling(list)混排算法所做的正好与 sort 相反: 它打乱在一个 List 中可能有的任何排列的踪迹。也就是说,基于随机源的输入重排该 List, 这样的排列具有相同的可能性(假设随机源是公正的)。这个算法在实现一个碰运气的游戏中是非常有用的。例如,它可被用来混排代表一副牌的 Card 对象的一个 List 。另外,在生成测试案例时,它也是十分有用的。
3) 反转(Reverse)
Collections.reverse(list)使用Reverse方法可以根据元素的自然顺序 对指定列表按降序进行排序。
4) 替换所以的元素(Fill)
Collections.fill(li,"aaa");使用指定元素替换指定列表中的所有元素。
5) 拷贝(Copy)
Collections.copy(list,li): 后面一个参数是目标列表 ,前一个是源列表用两个参数,一个目标 List 和一个源 List, 将源的元素拷贝到目标,并覆盖它的内容。目标 List 至少与源一样长。如果它更长,则在目标 List 中的剩余元素不受影响。
6) 返回Collections中最小元素(min)
Collections.min(list)根据指定比较器产生的顺序,返回给定 collection 的最小元素。collection 中的所有元素都必须是通过指定比较器可相互比较的
7) 返回Collections中最小元素(max)
Collections.max(list)根据指定比较器产生的顺序,返回给定 collection 的最大元素。collection 中的所有元素都必须是通过指定比较器可相互比较的
8) lastIndexOfSubList
int count = Collections.lastIndexOfSubList(list,li);返回指定源列表中最后一次出现指定目标列表的起始位置
9) IndexOfSubList
int count = Collections.indexOfSubList(list,li);返回指定源列表中第一次出现指定目标列表的起始位置
10) Rotate
Collections.rotate(list,-1);如果是负数,则正向移动,正数则方向移动根据指定的距离循环移动指定列表中的元素
四、List中常用的以及底层实现
常用的List有ArrayList、LinkedList;
ArrayList底层是一个数组队列,相当于 动态数组。与Java中的数组(Array)相比,它的容量能动态增长,还实现了RandomAccess接口。
基于这种底层实现,所以进行插入删除的时候,需要进行移动,所以插入效率比较低。但是查询和遍历的时候效率就比较高。比较适合用foreach这种遍历方式
LinkedList底层是一个链表表,还实现了Deque接口。
基于这种底层实现,所以进行插入的时候只需要修改链表指针,所以插入效率相对高一些。但是查询的时候需要一个一个指针指过去所以效率比较低。比较适合用iterator迭代器进行遍历。因为他还实现了双向队列接口,所以可以用他实现队列和栈。
五、Map中常用的以及底层实现
常用的Map有HashMap,LinkedHashMap,TreeMap;
HashMap
HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。
是一个最常用的Map,它根据键的HashCode 值存储数据,根据键可以直接获取它的值,具有很快的访问速度。HashMap最多只允许一条记录的键为Null;允许多条记录的值Null;
HashMap不支持线程的同步,即任一时刻可以有多个线程同时写HashMap;可能会导致数据的不一致。如果需要同步,可以用 Collections的synchronizedMap方法使HashMap具有同步的能力。
LinkedHashMap
LinkedHashMap也是一个HashMap,但是内部维持了一个双向链表,可以保持顺序,可以保证插入顺序有序。
TreeMap
TreeMap基于红黑树(Red-Black tree)实现。该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法。
TreeMap的基本操作 containsKey、get、put 和 remove 的时间复杂度是 log(n) 。
另外,TreeMap是非同步的。 它的iterator 方法返回的迭代器是fail-fastl的。
六、HashMap为什么线程不安全
HashMap 的实例有两个参数影响其性能:“初始容量” 和 “加载因子”。容量 是哈希表中桶的数量,初始容量 只是哈希表在创建时的容量。加载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。
通常,默认加载因子是 0.75, 这是在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap 类的操作中,包括 get 和 put 操作,都反映了这一点)。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少 rehash 操作次数。如果初始容量大于最大条目数除以加载因子,则不会发生 rehash 操作。
HashMap的实现里没有锁的机制,因此它是线程不安全的。
如果在HashMap内部加锁让它变成线程安全,这样会增加单线程访问的资源消耗,即使没有多线程访问,也要每次检查、加锁、解锁。(HashTable线程安全就是因为内部加锁)
线程不安全的表现1:
多线程情况下,两个线程A(取数据)B(存数据),如果A线程在刚到达获取的动作还没执行的时候,线程执行的机会又跳到线程B,此时线程B又对modelHashMap赋值,这样就会导致Map中存放的值一直丢失。简单说就是两个线程在同一个位置添加数据,后面添加的数据就覆盖住了前面添加的。
线程不安全的表现2:
如果在默认情况下,一个HashMap的容量为16,加载因子为0.75,那么阀值就是12,所以在往HashMap中put的值到达12时,它将自动扩容两倍,如果两个线程同时遇到HashMap的大小达到12的倍数时,就很有可能会出现在将oldTable转移到newTable的过程中遇到问题,从而导致最终的HashMap的值存储异常
线程不安全的表现3:
在多线程环境中,使用HashMap进行put操作时会引起死循环,导致CPU使用接近100%。当HashMap扩容时需要将原链表数据的数组拷到新的链表数组中,在进行拷贝的过程中会形成环链造成死循环。
七、ConcurrentHashMap为什么就线程安全?
ConcurrentHashMap采用了分段锁的设计,只有在同一个分段内才存在竞态关系,不同的分段锁之间没有锁竞争。相比于对整个Map加锁的设计(HashTable的设计),分段锁大大的提高了高并发环境下的处理能力。但同时,由于不是对整个Map加锁,导致一些需要扫描整个Map的方法(如size(), containsValue())需要使用特殊的实现,另外一些方法(如clear())甚至放弃了对一致性的要求。
ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。ConcurrentHashMap中的HashEntry相对于HashMap中的Entry有一定的差异性:HashEntry中的value以及next都被volatile修饰,这样在多线程读写过程中能够保持它们的可见性
和JDK6一样,ConcurrentHashMap的put方法被代理到了对应的Segment(定位Segment的原理之前已经描述过)中。与JDK6不同的是,JDK7版本的ConcurrentHashMap在获得Segment锁的过程中,做了一定的优化 - 在真正申请锁之前,put方法会通过tryLock()方法尝试获得锁,在尝试获得锁的过程中会对对应hashcode的链表进行遍历,如果遍历完毕仍然找不到与key相同的HashEntry节点,则为后续的put操作提前创建一个HashEntry。当tryLock一定次数后仍无法获得锁,则通过lock申请锁。
需要注意的是,由于在并发环境下,其他线程的put,rehash或者remove操作可能会导致链表头结点的变化,因此在过程中需要进行检查,如果头结点发生变化则重新对表进行遍历。而如果其他线程引起了链表中的某个节点被删除,即使该变化因为是非原子写操作(删除节点后链接后续节点调用的是Unsafe.putOrderedObject(),该方法不提供原子写语义)可能导致当前线程无法观察到,但因为不影响遍历的正确性所以忽略不计。
之所以在获取锁的过程中对整个链表进行遍历,主要目的是希望遍历的链表被CPU cache所缓存,为后续实际put过程中的链表遍历操作提升性能。
在获得锁之后,Segment对链表进行遍历,如果某个HashEntry节点具有相同的key,则更新该HashEntry的value值,否则新建一个HashEntry节点,将它设置为链表的新head节点并将原头节点设为新head的下一个节点。新建过程中如果节点总数(含新建的HashEntry)超过threshold,则调用rehash()方法对Segment进行扩容,最后将新建HashEntry写入到数组中。
put方法中,链接新节点的下一个节点(HashEntry.setNext())以及将链表写入到数组中(setEntryAt())都是通过Unsafe的putOrderedObject()方法来实现,这里并未使用具有原子写语义的putObjectVolatile()的原因是:JMM会保证获得锁到释放锁之间所有对象的状态更新都会在锁被释放之后更新到主存,从而保证这些变更对其他线程是可见的。
八、为什么concurrentHashMap的get方法不需要加锁?
因为concurrentHashMap中的HashEntry中的value以及next都被volatile修饰,在读操作的时候不需要对value进行修改,写操作的时候加锁保证了不会被其他线程进行修改,然后volatile又可以保证数据实时,所以不需要在进行加锁操作。但是当值为Null的时候还是会加锁重读的。