java面试总结--【2】集合

一.collection

1.java的集合继承于接口collection,collection继承于接口Iterable,如下图:

二、List

list是继承于collection的接口,主要特点是存取是有序的,并且可存取重复的元素。常用的有两个实现类,ArrayList和LinkedList。

1)ArrayList

ArrayList就是基于数组的一个线性表,只不过数组的长度可以动态改变而已。

ArrayList类似数据的存取结构,因此随机访问速度快,但是随机插入和删除由于需要整个数组的移动,因此插入和删除效率低。

  1. ArrayList的初始长度是10;
  2. ArrayList扩容每次长度是(已有长度*3/2) + 1;
  3. ArrayList是线程不安全的,如果要安全的则使用

2)LinkedList

LinkedList就是一种双向循环链表的链式线性表,只不过存储的结构使用的是链式表而已。

  1. 适合频繁插入和删除操作
  2. 随机访问速度很慢,因为需要需要从头开始一个一个查找
  3. 由于LinkedList实现了接口Dueue,所以LinkedList可以被当做堆栈来使用

      问题:ArrayList和LinkedList怎么实现多线程安全?

可以使用Collections.synchronizedList(),如下:

List<Map<String,Object>> data=Collections.synchronizedList(new ArrayList<Map<String,Object>>());

LinkedList也类似。

三、Set

Set接口的实现类有HashSet和TreeSet,其中HashSet的子类又有LinkedHashSet。

Set是只能保存不重复的元素,并且是不能保证插入顺序的

1)HashSet

HashSet的底层实现其实就是基于HashMap,创建一个value为固定值的map,其他方法都是调用HashMap的方法。

2)TreeSet

TreeSet底层是使用二叉树实现,存储的元素是经过排序的,因此对象要实现compare方法。底层和HashSet类似,其实大部分方法使用TreeMap来实现的。

3)LinkedHashSet

LinkedHashSet可以保证存取顺序的。底层实现主要方法是基于LinkedHashMap的。

四、Map

1)HashMap

a. HashMap采用数组和链表的结果来保存数据。

b. put()方法是先对key使用hash()方法计算出hash值,根据hash值找到存储位置,如果存储位置已经被占,则发送hash冲突,此时会将新值以链表形式存储,即Entry要存储下一个元素的引用。

c. get()方法是先对key进行hash()计算得出存储位置,如果存储位置存在链表,则遍历链表,根据对象的equals方法来寻找需要的元素。

d. HashMap创建时初始大小为16,负载因子为0.75。即当元素个数达到当前数组大小*0.75时,开始扩容一倍。

e.扩容是很耗性能的操作,会把当前的所有元素存储地址计算一遍,所以如果提前知道整个map的大小,就创建时指定大小来提升性能。

2)TreeMap

TreeMap中的元素会自动根据key定义的排序方法进行排序,使用二叉树结构。

a.自然排序 :TreeMap中的key必须实现compareble接口

b.自定义排序:创建TreeMap时传入一个comparator对象

3)LinkedHahsMap

LinkedHahsMap可以保持两种顺序,分别是插入顺序和访问顺序,默认是按照插入顺序。

a. LinkedHahsMap是HashMap的亲儿子,增加了双向链表的结构(即增加了前后指针),其他处理逻辑和HashMap一样。

4)ConcurrentHashMap

1. 谈谈你理解的 HashMap,讲讲其中的 get put 过程。

2.1.8 做了什么优化?

答:1.7的结构采用位桶+链表结构,而1.8的结构采用位桶+链表+红黑树结构,即当链表超过指定的阀值时,自动转成红黑树

3. 是线程安全的嘛?

答:不是线程安全的。

4.不安全会导致哪些问题?

答:不是线程安全的,主要是以下两点:

a.hash碰撞,当多个线程同时在同一个桶的链表添加元素时,同时获取了head,会导致只有一个成功。

b.map扩容,当多个线程同时进行扩容时,会新建一个数组,重新计算hash值,这时就会

5.如何解决?有没有线程安全的并发容器?

6.ConcurrentHashMap 是如何实现的? 1.7、1.8 实现有何不同?为什么这么做?

        HashTable容器在竞争激烈的并发环境下表现出效率低下的原因,是因为所有访问HashTable的线程都必须竞争同一把锁,那假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁。这里“按顺序”是很重要的,否则极有可能出现死锁。

       JDK1.8的实现已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap,虽然在JDK1.8中还能看到Segment的数据结构,但是已经简化了属性,只是为了兼容旧版本。

get操作可以无锁是由于Node的元素val和指针next是用volatile修饰的,在多线程环境下线程A修改因为hash冲突修改结点的val或者新增节点的时候是对线程B可见的。

猜你喜欢

转载自blog.csdn.net/zht245648124/article/details/90549833