java面试必备--JAVA基础篇(十六) 之 容器

      相信很多同行小伙伴会因为许多原因想跳槽,不论是干得不开心还是想跳槽涨薪,在如此内卷的行业,我们都面临着“面试造火箭,上班拧螺丝”的局面,鉴于当前形势博主呕心沥血整理的干货满满的造火箭的技巧来了,本博主花费2个月时间,整理归纳java全生态知识体系常见面试题!总字数高达百万! 干货满满,每天更新,关注我,不迷路,用强大的归纳总结,全新全细致的讲解来留住各位猿友的关注,希望能够帮助各位猿友在应付面试笔试上!当然如有归纳总结错误之处请各位指出修正!如有侵权请联系博主QQ1062141499!

目录

1 常见集合类的区别

2 List 和 Map、Set 的区别

3  Map、Set、List、Queue、Stack的特点与用法

4 Java中已经有数组类型,为什么还要提供集合?

5 数组和List之间的转换

6 Java 容器都有哪些?

7 集合的默认初始容量、加载因子、扩容增量

8 Collection和Collections区别

9 HashMap和Hashtable 区别

10 Map的实现类中,哪些是有序的,哪些是无序的,如何保证其有序性?

11 Map的遍历方式

12 如何决定使用HashMap还是TreeMap?

13 说一下 HashMap 的实现原理

14 hashmap 为什么初始化容量是2的幂次方

15 HashMap扩容机制

17 JDK7和JDK8中HashMap为什么是线程不安全的?

17 ConcurrentHashMap了解吗?说说实现原理。

18 为什么Hashtable、ConcurrentHashmap不支持key或者value为null

19 HashSet实现原理是什么?有什么特点?

20 TreeSet的原理是什么?使用需要注意什么?

21 LinkedList的pop()方法和push()方法

22 ArrayList和LinkedList区别

23 ArrayList和Vector 区别

24 Array(数组)和ArrayList区别

25 LinkedHashMap、LinkedHashSet、LinkedList哪个最适合当作Stack使用?

26 哪些集合类是线程安全的

27 迭代器 Iterator 是什么

28 Iterator 怎么使用?有什么特点?

29 Iterator 和 ListIterator 区别

30 怎么确保一个集合不能被修改

31 为什么基本类型不能做为HashMap的键值?

32 HashMap 排序题,上机题

33 请问 ArrayList、HashSet、HashMap  是线程安全的吗?如果不是我想要线程安全的集合怎么办?

34 并发集合和普通集合如何区别?

35 为什么HashMap是线程不安全的

36 ArrayList list = new ArrayList(20);中的list扩充几次?

37 foreach与正常for循环效率对比

38 为什么基类不能做为 HashMap 的键值,而只能是引用类型,把引用类型作为HashMap 的键值, 需要注意哪些地方?

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

40 Java中的队列都有哪些,有什么区别

41 Queue的add()和offer()方法有什么区别?

42 Queue中poll()和remove()区别

43 Queue的element()和peek()方法有什么区别?

44 阻塞队列

45 BlockingQueue相比普通的Queue最大的区别是什么?

46 BlockingQueue的实现类

47 请用两个队列模拟堆栈结构


1 常见集合类的区别

 

ArrayList  底层结构是数组,底层查询快,增删慢。

LinkedList  底层结构是链表型的,增删快,查询慢。

Voctor 底层结构是数组  线程安全的,增删慢,查询慢。

HashMap: 元素成对,元素可为空

HashTable: 元素成对,线程安全,元素不可为空

集合 : 集合对象:用于管理其他若干对象的对象

数组:长度不可变

List: 有顺序的,元素可以重复

遍历:for 迭代

排序:Comparable Comparator Collections.sort()

ArrayList:底层用数组实现的List

特点:查询效率高,增删效率低 轻量级 线程不安全

LinkedList:底层用双向循环链表 实现的List

特点:查询效率低,增删效率高

Vector: 底层用数组实现List接口的另一个类

特点:重量级,占据更多的系统开销 线程安全

Set:无顺序的,元素不可重复(值不相同)

遍历:迭代

排序:SortedSet

HashSet:采用哈希算法来实现Set接口

唯一性保证:重复对象equals方法返回为true

重复对象hashCode方法返回相同的整数

不同对象 哈希码 尽量保证不同(提高效率)

SortedSet:对一个Set排序

TreeSet:在元素添加的同时,进行排序。也要给出排序规则

唯一性保证:根据排序规则,compareTo方法返回为0,就可以认定两个对象中有一个是重复对象。

Map:元素是键值对 key:唯一,不可重复 value:可重复

遍历:先迭代遍历key的集合,再根据key得到value

HashMap:轻量级 线程不安全 允许key或者value是null

Hashtable:重量级 线程安全 不允许key或者value是null

Properties:Hashtable的子类,key和value都是String

2 List 和 Map、Set 的区别

 

结构特点

      List和Set是存储单列数据的集合,Map是存储键和值这样的双列数据的集合;List中存储的数据是有顺序,并且允许重复;Map中存储的数据是没有顺序的,其键是不能重复的,它的值是可以有重复的,Set中存储的数据是无序的,且不允许有重复,但元素在集合中的位置由元素的hashcode决定,位置是固定的(Set集合根据hashcode来进行数据的存储,所以位置是固定的,但是位置不是用户可以控制的,所以对于用户来说set中的元素还是无序的);

实现类

      List接口有三个实现类(LinkedList:基于链表实现,链表内存是散乱的,每一个元素存储本身内存地址的同时还存储下一个元素的地址。链表增删快,查找慢;ArrayList:基于数组实现,非线程安全的,效率高,便于索引,但不便于插入删除;Vector:基于数组实现,线程安全的,效率低)。

      Map接口有三个实现类(HashMap:基于hash表的Map接口实现,非线程安全,高效,支持null值和null键;HashTable:线程安全,低效,不支持null值和null键;LinkedHashMap:是HashMap的一个子类,保存了记录的插入顺序;SortMap接口:TreeMap,能够把它保存的记录根据键排序,默认是键值的升序排序)。

      Set接口有两个实现类(HashSet:底层是由HashMap实现,不允许集合中有重复的值,使用该方式时需要重写equals()和hashCode()方法;LinkedHashSet:继承自HashSet,同时又基于LinkedHashMap来进行实现,底层使用的是LinkedHashMp)。

区别

      List集合中对象按照索引位置排序,可以有重复对象,允许按照对象在集合中的索引位置检索对象,例如通过list.get(i)方法来获取集合中的元素;Map中的每一个元素包含一个键和一个值,成对出现,键对象不可以重复,值对象可以重复;Set集合中的对象不按照特定的方式排序,并且没有重复对象,但它的实现类能对集合中的对象按照特定的方式排序,例如TreeSet类,可以按照默认顺序,也可以通过实现Java.util.Comparator<Type>接口来自定义排序方式。

3  Map、Set、List、Queue、Stack的特点与用法

Map

  1. Map键值对,键Key是唯一不能重复的,一个键对应一个值,值可以重复。
  2. TreeMap可以保证顺序。
  3. HashMap不保证顺序,即为无序的。
  4. Map中可以将Key和Value单独抽取出来,其中KeySet()方法可以将所有的keys抽取正一个Set。而Values()方法可以将map中所有的values抽取成一个集合。

Set

  1. 不包含重复元素的集合,set中最多包含一个null元素。
  2. 只能用Lterator实现单项遍历,Set中没有同步方法。

List

  1. 有序的可重复集合。
  2. 可以在任意位置增加删除元素。
  3. 用Iterator实现单向遍历,也可用ListIterator实现双向遍历。

Queue

  1. Queue遵从先进先出原则。
  2. 使用时尽量避免add()和remove()方法,而是使用offer()来添加元素,使用poll()来移除元素,它的优点是可以通过返回值来判断是否成功。
  3. LinkedList实现了Queue接口。
  4. Queue通常不允许插入null元素。

Stack

  1. Stack遵从后进先出原则。
  2. Stack继承自Vector。
  1. 它通过五个操作对类Vector进行扩展,允许将向量视为堆栈,它提供了通常的push和pop操作,以及取堆栈顶点的peek()方法、测试堆栈是否为空的empty方法等。

4 Java中已经有数组类型,为什么还要提供集合?

数组的优点

  • 数组的效率高于集合类
  • 数组能存放基本数据类型和对象;集合中只能放对象

数组的缺点

  • 不是面向对象的,存在明显的缺陷
  • 数组长度固定且无法动态改变;集合类容量动态改变
  • 数组无法判断其中实际存了多少元素,只能通过length属性获取数组的申明的长度
  • 数组存储的特点是顺序的连续内存;集合的数据结构更丰富

JDK 提供集合的意义

  • 集合以类的形式存在,符合面向对象,通过简单的方法和属性调用可实现各种复杂操作
  • 集合有多种数据结构,不同类型的集合可适用于不同场合
  • 弥补了数组的一些缺点,比数组更灵活、实用,可提高开发效率

5 数组和List之间的转换

     数组转 List:使用 Arrays. asList(array) 进行转换。

      List 转数组:使用 List 自带的 toArray() 方法。

代码示例:

// 集合转数组
List<String> list = new ArrayList<String>();
list.add("多");
list.add("少");
Object[] objects = list.toArray();
// 数组转集合
String[] array = new String[] { "多", "少" };
List<String> stringList = Arrays.asList(array);

 

6 Java 容器都有哪些?

Java 容器分为 Collection 和 Map 两大类,其下又有很多子类,如下所示:

•       Collection

•       List

o      ArrayList

o      LinkedList

o      Vector

o      Stack

•       Set

o      HashSet

o      LinkedHashSet

o      TreeSet

Map

•       HashMap

o      LinkedHashMap

•       TreeMap

•       ConcurrentHashMap

•       Hashtable

7 集合的默认初始容量、加载因子、扩容增量

     常用集合的默认初始容量和扩容的原因:

     当底层实现涉及到扩容时,容器或重新分配一段更大的连续内存(如果是离散分配则不需要重新分配,离散分配都是插入新元素时动态分配内存),要将容器原来的数据全部复制到新的内存上,这无疑使效率大大降低。

      加载因子的系数小于等于1,意指  即当 元素个数 超过 容量长度*加载因子的系数 时,进行扩容。另外,扩容也是有默认的倍数的,不同的容器扩容情况不同。

List 元素是有序的、可重复

ArrayList、Vector默认初始容量为10

       LinkedList 是一个双向链表,没有初始化大小,没有扩容的机制,所以没有加载因子,就是一直在前面或者后面新增就好。

Vector:线程安全,但速度慢

    底层数据结构是数组结构

    加载因子为1:即当 元素个数 超过 容量长度 时,进行扩容

    扩容增量:原容量的 1倍

    如 Vector的容量为10,一次扩容后是容量为20

ArrayList:线程不安全,查询速度快

    底层数据结构是数组结构

    扩容增量:原容量的 0.5倍+1

    如 ArrayList的容量为10,一次扩容后是容量为16

Set(集) 元素无序的、不可重复。

HashSet:线程不安全,存取速度快

     底层实现是一个HashMap(保存数据),实现Set接口

     默认初始容量为16(为何是16,见下方对HashMap的描述)

     加载因子为0.75:即当 元素个数 超过 容量长度的0.75倍 时,进行扩容

     扩容增量:原容量的 1 倍

     如 HashSet的容量为16,一次扩容后是容量为32

LinkedHashSet继承自HashSet,同上

Map是一个双列集合

HashMap:默认初始容量为16

     (为何是16:16是2^4,可以提高查询效率,另外,32=16<<1,至于详细的原因可另行分析,或分析源代码)

     加载因子为0.75:即当 元素个数 超过 容量长度的0.75倍 时,进行扩容

     扩容增量:原容量的 1 倍

     如 HashSet的容量为16,一次扩容后是容量为32

HashTable:默认初始容量为11

     线程安全,但是速度慢,不允许key/value为null

     加载因子为0.75:即

当元素个数 超过 容量长度的0.75倍 时,进行扩容

     扩容增量:2*原数组长度+1

     如 HashTable的容量为11,一次扩容后是容量为23

8 Collection和Collections区别

•       Collection 是一个集合接口,它提供了对集合对象进行基本操作的通用接口方法,所有集合都是它的子类,比如 List、Set 等。

•       Collections 是一个包装类,包含了很多静态方法,不能被实例化,就像一个工具类,比如提供的排序方法:Collections. sort(list)。

9 HashMap和Hashtable 区别

  1. 线程安全性不同。HashMap线程不安全;Hashtable 中的方法是Synchronize的。
  2. key、value是否允许null。HashMap的key和value都是可以是null,key只允许一个null;Hashtable的key和value都不可为null。
  3. 迭代器不同。HashMap的Iterator是fail-fast迭代器;Hashtable还使用了enumerator迭代器。
  4. hash的计算方式不同。HashMap计算了hash值;Hashtable使用了key的hashCode方法。
  5. 默认初始大小和扩容方式不同。HashMap默认初始大小16,容量必须是2的整数次幂,扩容时将容量变为原来的2倍;Hashtable默认初始大小11,扩容时将容量变为原来的2倍加1。
  6. 是否有contains方法。HashMap没有contains方法;Hashtable包含contains方法,类似于containsValue。
  7. 父类不同。HashMap继承自AbstractMap;Hashtable继承自Dictionary。

         推荐使用:在 Hashtable 的类注释可以看到,Hashtable 是保留类不建议使用,推荐在单线程环境下使用 HashMap 替代,如果需要多线程使用则用 ConcurrentHashMap 替代。

10 Map的实现类中,哪些是有序的,哪些是无序的,如何保证其有序性?

  • Map 的实现类有 HashMap、LinkedHashMap、TreeMap
  • HashMap是有无序的
  • LinkedHashMap 和 TreeMap 是有序的。LinkedHashMap 记录了添加数据的顺序;TreeMap 默认是升序
  • LinkedHashMap 底层存储结构是哈希表+链表,链表记录了添加数据的顺序
  • TreeMap 底层存储结构是二叉树,二叉树的中序遍历保证了数据的有序性

11 Map的遍历方式

  • Map 的 keySet() 方法,单纯拿到所有 Key 的 Set
  • Map 的 values() 方法,单纯拿到所有值的 Collection
  • keySet() 获取到 key 的 Set,遍历 Set 根据 key 找值(不推荐使用,效率比下面的方式低,原因是多出了根据 key 找值的消耗)
  • 获取所有的键值对集合,迭代器遍历
  • 获取所有的键值对集合,for 循环遍历

12 如何决定使用HashMap还是TreeMap?

都是非线程安全。

  1. HashMap基于散列桶(数组和链表)实现;TreeMap基于红黑树实现。
  2. HashMap不支持排序;TreeMap默认是按照Key值升序排序的,可指定排序的比较器,主要用于存入元素时对元素进行自动排序。
  3. HashMap大多数情况下有更好的性能,尤其是读数据。在没有排序要求的情况下,使用HashMap。

对于在 Map 中插入、删除、定位一个元素这类操作,HashMap 是最好的选择,因为相对而言 HashMap 的插入会更快,但如果你要对一个 key 集合进行有序的遍历,那 TreeMap 是更好的选择

13 说一下 HashMap 的实现原理

  • HashMap 基于 Hash 算法实现,通过 put(key,value) 存储,get(key) 来获取 value
  • 当传入 key 时,HashMap 会根据 key,调用 hash(Object key) 方法,计算出 hash 值,根据 hash 值将 value 保存在 Node 对象里,Node 对象保存在数组里
  • 当计算出的 hash 值相同时,称之为 hash 冲突,HashMap 的做法是用链表和红黑树存储相同 hash 值的 value
  • 当 hash 冲突的个数:小于等于 8 使用链表;大于 8 时,使用红黑树解决链表查询慢的问题

ps:

  • 上述是 JDK 1.8 HashMap 的实现原理,并不是每个版本都相同,比如 JDK 1.7 的 HashMap 是基于数组 + 链表实现,所以 hash 冲突时链表的查询效率低
  • hash(Object key)  方法的具体算法是 (h = key.hashCode()) ^ (h >>> 16),经过这样的运算,让计算的 hash 值分布更均匀

14 hashmap 为什么初始化容量是2的幂次方

        个人理解 做下记录,不正确的地方望不吝赐教

        这是hashmap初始化容量时候 对容量大小做的处理,保证初始化容量为最近的2的幂次方(JDK1.8)

static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

思考,为啥非得是2的幂次方 ,2的倍数不行么,奇数不行么?

 结合源码加别人的资料,做如下解释:1.奇数不行的解释很能被接受,在计算hash的时候,确定落在数组的位置的时候,计算方法是(n-1)&hash,奇数n-1为偶数,偶数2进制的结尾都是0,经过&运算末尾都是0,会增加hash冲突。2.为啥要是2的幂,不能是2的倍数么,比如6,10?2.1 hashmap 结构是数组,每个数组里面的结构是node(链表或红黑树),正常情况下,如果你想放数据到不同的位置,肯定会想到取余数确定放在那个数据里,  计算公式:hash%n,这个是十进制计算。在计算机中,(n-1)&hash,当n为2次幂时,会满足一个公式:(n-1)&hash = hash%n,计算更加高效。
2.2 只有是2的幂数的数字经过n-1之后,二进制肯定是...11111111 这样的格式,这种格式计算的位置的时候(&),完全是由产生的hash值类决定,而不受n-1(组数长度的二进制)影响。你可能会想,受影响不是更好么,又计算了一下,类似于扰动函数,hash冲突可能更低了,这里要考虑到扩容了,2的幂次方*2,在二进制中比如4和8,代表2的2次方和3次方,他们的2进制结构相似,比如4和8  00000100  00001000只是高位向前移了一位,这样扩容的时候,只需要判断高位hash,移动到之前位置的倍数就可以了,免去了重新计算位置的运算。

15 HashMap扩容机制

      当HashMap决定扩容时,会调用HashMap类中的resize(int newCapacity)方法,参数是新的table长度。在JDK1.7和JDK1.8的扩容机制有很大不同。

JDK1.7下的扩容机制

JDK1.7下的resize()方法是这样的:

   void resize(int newCapacity) {
2.	        Entry[] oldTable = table;
3.	        int oldCapacity = oldTable.length;
4.	        if (oldCapacity == MAXIMUM_CAPACITY) {
5.	            threshold = Integer.MAX_VALUE;
6.	            return;
7.	        }
8.	        Entry[] newTable = new Entry[newCapacity];
9.	        transfer(newTable, initHashSeedAsNeeded(newCapacity));
10.	        table = newTable;
11.	        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
12.	    }

      代码中可以看到,如果原有table长度已经达到了上限,就不再扩容了。如果还未达到上限,则创建一个新的table,并调用transfer方法:

  /**
2.	     * Transfers all entries from current table to newTable.
3.	     */
4.	    void transfer(Entry[] newTable, boolean rehash) {
5.	        int newCapacity = newTable.length;
6.	        for (Entry<K,V> e : table) {
7.	            while(null != e) {
8.	                Entry<K,V> next = e.next;              //注释1
9.	                if (rehash) {
10.	                    e.hash = null == e.key ? 0 : hash(e.key);
11.	                }
12.	                int i = indexFor(e.hash, newCapacity); //注释2
13.	                e.next = newTable[i];                  //注释3
14.	                newTable[i] = e;                       //注释4
15.	                e = next;                              //注释5
16.	            }
17.	        }
18.	    }

     transfer方法的作用是把原table的Node放到新的table中,使用的是头插法,也就是说,新table中链表的顺序和旧列表中是相反的,在HashMap线程不安全的情况下,这种头插法可能会导致环状节点。

    其中的while循环描述了头插法的过程,这个逻辑有点绕,下面举个例子来解析一下这段代码。

    假设原有table记录的某个链表,比如table[1]=3,链表为3-->5-->7,那么处理流程为:

1,注释1:记录e.next的值。开始时e是table[1],所以e==3,e.next==5,那么此时next==5。

2,注释2,计算e在newTable中的节点。为了展示头插法的倒序结果,这里假设e再次散列到了newTable[1]的链表中。

3,注释3,把newTable [1]赋值给e.next。因为newTable是新建的,所以newTable[1]==null,所以此时3.next==null。

4,注释4,e赋值给newTable[1]。此时newTable[1]=3。

 

 5,注释5,next赋值给e。此时e==5。

此时newTable[1]中添加了第一个Node节点3,下面进入第二次循环,第二次循环开始时e==5。

1,注释1:记录e.next的值。5.next是7,所以next==7。

2,注释2,计算e在newTable中的节点。为了展示头插法的倒序结果,这里假设e再次散列到了newTable[1]的链表中。

3,注释3,把newTable [1]赋值给e.next。因为newTable[1]是3(参见上一次循环的注释4),e是5,所以5.next==3。

4,注释4,e赋值给newTable[1]。此时newTable[1]==5。 

 

 

 5,注释5,next赋值给e。此时e==7。

此时newTable[1]是5,链表顺序是5-->3。

下面进入第三次循环,第二次循环开始时e==7。

1,注释1:记录e.next的值。7.next是NULL,所以next==NULL。

 

2,注释2,计算e在newTable中的节点。为了展示头插法的倒序结果,这里假设e再次散列到了newTable[1]的链表中。

3,注释3,把newTable [1]赋值给e.next。因为newTable[1]是5(参见上一次循环的注释4),e是7,所以7.next==5。、

4,注释4,e赋值给newTable[1]。此时newTable[1]==7。

5,注释5,next赋值给e。此时e==NULL。 

     此时newTable[1]是7,循环结束,链表顺序是7-->5-->3,和原链表顺序相反。

     注意:这种逆序的扩容方式在多线程时有可能出现环形链表,出现环形链表的原因大概是这样的:线程1准备处理节点,线程二把HashMap扩容成功,链表已经逆向排序,那么线程1在处理节点时就可能出现环形链表。

    另外单独说一下indexFor(e.hash, newCapacity);这个方法,这个方法是计算节点在新table中的下标用的,这个方法的代码如下:

1.	    /**
2.	     * Returns index for hash code h.
3.	     */
4.	    static int indexFor(int h, int length) {
5.	        // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
6.	        return h & (length-1);
7.	    }

 

     计算下标的算法很简单,hash值 和 (length-1)按位与,使用length-1的意义在于,length是2的倍数,所以length-1在二进制来说每位都是1,这样可以保证最大的程度的散列hash值,否则,当有一位是0时,不管hash值对应位是1还是0,按位与后的结果都是0,会造成散列结果的重复。

JDK7的ConcurrentHashMap扩容

     HashMap是线程不安全的,我们来看下线程安全的ConcurrentHashMap,在JDK7的时候,这种安全策略采用的是分段锁的机制,ConcurrentHashMap维护了一个Segment数组,Segment这个类继承了重入锁ReentrantLock,并且该类里面维护了一个 HashEntry<K,V>[] table数组,在写操作put,remove,扩容的时候,会对Segment加锁,所以仅仅影响这个Segment,不同的Segment还是可以并发的,所以解决了线程的安全问题,同时又采用了分段锁也提升了并发的效率。 

JDK1.8下的扩容机制

    在JDK8里面,HashMap的底层数据结构已经变为数组+链表+红黑树的结构了,因为在hash冲突严重的情况下,链表的查询效率是O(n),所以JDK8做了优化对于单个链表的个数大于8的链表,会直接转为红黑树结构算是以空间换时间,这样以来查询的效率就变为O(logN),图示如下:

 

JDK1.8对resize()方法进行很大的调整,JDK1.8的resize()方法如下: 

final Node<K,V>[] resize() {
2.	        Node<K,V>[] oldTab = table;
3.	        int oldCap = (oldTab == null) ? 0 : oldTab.length;
4.	        int oldThr = threshold;
5.	        int newCap, newThr = 0;
6.	        if (oldCap > 0) {
7.	            if (oldCap >= MAXIMUM_CAPACITY) {
8.	                threshold = Integer.MAX_VALUE;
9.	                return oldTab;
10.	            }
11.	            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
12.	                     oldCap >= DEFAULT_INITIAL_CAPACITY)                      //注释1
13.	                newThr = oldThr << 1; // double threshold
14.	        }
15.	        else if (oldThr > 0) // initial capacity was placed in threshold
16.	            newCap = oldThr;
17.	        else {               // zero initial threshold signifies using defaults
18.	            newCap = DEFAULT_INITIAL_CAPACITY;
19.	            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
20.	        }
21.	        if (newThr == 0) {
22.	            float ft = (float)newCap * loadFactor;
23.	            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
24.	                      (int)ft : Integer.MAX_VALUE);
25.	        }
26.	        threshold = newThr;
27.	        @SuppressWarnings({"rawtypes","unchecked"})
28.	            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
29.	        table = newTab;
30.	        if (oldTab != null) {
31.	            for (int j = 0; j < oldCap; ++j) {                                 //注释2
32.	                Node<K,V> e;
33.	                if ((e = oldTab[j]) != null) {
34.	                    oldTab[j] = null;
35.	                    if (e.next == null)                                        //注释3
36.	                        newTab[e.hash & (newCap - 1)] = e;
37.	                    else if (e instanceof TreeNode)
38.	                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
39.	                    else { // preserve order
40.	                        Node<K,V> loHead = null, loTail = null;
41.	                        Node<K,V> hiHead = null, hiTail = null;
42.	                        Node<K,V> next;
43.	                        do {
44.	                            next = e.next;
45.	                            if ((e.hash & oldCap) == 0) {                      //注释4
46.	                                if (loTail == null)                            //注释5
47.	                                    loHead = e;
48.	                                else
49.	                                    loTail.next = e;                           //注释6
50.	                                loTail = e;                                    //注释7
51.	                            }
52.	                            else {
53.	                                if (hiTail == null)
54.	                                    hiHead = e;
55.	                                else
56.	                                    hiTail.next = e;
57.	                                hiTail = e;
58.	                            }
59.	                        } while ((e = next) != null);
60.	                        if (loTail != null) {                                  /注释8
61.	                            loTail.next = null;
62.	                            newTab[j] = loHead;
63.	                        }
64.	                        if (hiTail != null) {
65.	                            hiTail.next = null;
66.	                            newTab[j + oldCap] = hiHead;
67.	                        }
68.	                    }
69.	                }
70.	            }
71.	        }
72.	        return newTab;
73.	    }

代码解析:

     1,在resize()方法中,定义了oldCap参数,记录了原table的长度,定义了newCap参数,记录新table长度,newCap是oldCap长度的2倍(注释1),同时扩展点也乘2。

    2,注释2是循环原table,把原table中的每个链表中的每个元素放入新table。

    3,注释3,e.next==null,指的是链表中只有一个元素,所以直接把e放入新table,其中的e.hash & (newCap - 1)就是计算e在新table中的位置,和JDK1.7中的indexFor()方法是一回事。

    4,注释// preserve order,这个注释是源码自带的,这里定义了4个变量:loHead,loTail,hiHead,hiTail,看起来可能有点眼晕,其实这里体现了JDK1.8对于计算节点在table中下标的新思路:

       正常情况下,计算节点在table中的下标的方法是:hash&(oldTable.length-1),扩容之后,table长度翻倍,计算table下标的方法是hash&(newTable.length-1),也就是hash&(oldTable.length*2-1),于是我们有了这样的结论:这新旧两次计算下标的结果,要不然就相同,要不然就是新下标等于旧下标加上旧数组的长度

      举个例子,假设table原长度是16,扩容后长度32,那么一个hash值在扩容前后的table下标是这么计算的:

 

       hash值的每个二进制位用abcde来表示,那么,hash和新旧table按位与的结果,最后4位显然是相同的,唯一可能出现的区别就在第5位,也就是hash值的b所在的那一位,如果b所在的那一位是0,那么新table按位与的结果和旧table的结果就相同,反之如果b所在的那一位是1,则新table按位与的结果就比旧table的结果多了10000(二进制),而这个二进制10000就是旧table的长度16。

       换言之,hash值的新散列下标是不是需要加上旧table长度,只需要看看hash值第5位是不是1就行了,位运算的方法就是hash值和10000(也就是旧table长度)来按位与,其结果只可能是10000或者00000。

      所以,注释4处的e.hash & oldCap,就是用于计算位置b到底是0还是1用的,只要其结果是0,则新散列下标就等于原散列下标,否则新散列坐标要在原散列坐标的基础上加上原table长度。

理解了上面的原理,这里的代码就好理解了,代码中定义的四个变量:

loHead,下标不变情况下的链表头
loTail,下标不变情况下的链表尾
hiHead,下标改变情况下的链表头
hiTail,下标改变情况下的链表尾

 

     而注释4处的(e.hash & oldCap) == 0,就是代表散列下标不变的情况,这种情况下代码只使用了loHead和loTail两个参数,由他们组成了一个链表,否则将使用hiHead和hiTail参数。

其实e.hash & oldCap等于0和不等于0后的逻辑完全相同,只是用的变量不一样。

以等于0的情况为例,处理一个3-->5-->7的链表,过程如下:

首先处理节点3,e==3,e.next==5

         1,注释5,一开始loTail是null,所以把3赋值给loHead。

         2,注释7,把3赋值给loTail。

然后处理节点5,e==5,e.next==7

         1,注释6,loTail有值,把e赋值给loTail.next,也就是3.next==5。

          2,注释7,把5赋值给loTail。

      现在新链表是3-->5,然后处理节点7,处理完之后,链表的顺序是3-->5-->7,loHead是3,loTail是7。可以看到,链表中节点顺序和原链表相同,不再是JDK1.7的倒序了。

代码到注释8这里就好理解了,

       只要loTail不是null,说明链表中的元素在新table中的下标没变,所以新table的对应下标中放的是loHead,另外把loTail的next设为null

          反之,hiTail不是null,说明链表中的元素在新table中的下标,应该是原下标加原table长度,新table对应下标处放的是hiHead,另外把hiTail的next设为null。

JDK8的ConcurrentHashMap扩容

       在JDK8中彻底抛弃了JDK7的分段锁的机制,新的版本主要使用了Unsafe类的CAS自旋赋值+synchronized同步+LockSupport阻塞等手段实现的高效并发,代码可读性稍差。

       ConcurrentHashMap的JDK8与JDK7版本的并发实现相比,最大的区别在于JDK8的锁粒度更细,理想情况下talbe数组元素的大小就是其支持并发的最大个数,在JDK7里面最大并发个数就是Segment的个数,默认值是16,可以通过构造函数改变一经创建不可更改,这个值就是并发的粒度,每一个segment下面管理一个table数组,加锁的时候其实锁住的是整个segment,这样设计的好处在于数组的扩容是不会影响其他的segment的,简化了并发设计,不足之处在于并发的粒度稍粗,所以在JDK8里面,去掉了分段锁,将锁的级别控制在了更细粒度的table元素级别,也就是说只需要锁住这个链表的head节点,并不会影响其他的table元素的读写,好处在于并发的粒度更细,影响更小,从而并发效率更好,但不足之处在于并发扩容的时候,由于操作的table都是同一个,不像JDK7中分段控制,所以这里需要等扩容完之后,所有的读写操作才能进行,所以扩容的效率就成为了整个并发的一个瓶颈点,好在Doug lea大神对扩容做了优化,本来在一个线程扩容的时候,如果影响了其他线程的数据,那么其他的线程的读写操作都应该阻塞,但Doug lea说你们闲着也是闲着,不如来一起参与扩容任务,这样人多力量大,办完事你们该干啥干啥,别浪费时间,于是在JDK8的源码里面就引入了一个ForwardingNode类,在一个线程发起扩容的时候,就会改变sizeCtl这个值,其含义如下:

  1. sizeCtl :默认为0,用来控制table的初始化和扩容操作,具体应用在后续会体现出来。
  2. -1 代表table正在初始化
  3. -N 表示有N-1个线程正在进行扩容操作

其余情况:

      1、如果table未初始化,表示table需要初始化的大小。

      2、如果table初始化完成,表示table的容量,默认是table大小的0.75倍

      扩容时候会判断这个值,如果超过阈值就要扩容,首先根据运算得到需要遍历的次数i,然后利用tabAt方法获得i位置的元素f,初始化一个forwardNode实例fwd,如果f == null,则在table中的i位置放入fwd,否则采用头插法的方式把当前旧table数组的指定任务范围的数据给迁移到新的数组中,然后 给旧table原位置赋值fwd。直到遍历过所有的节点以后就完成了复制工作,把table指向nextTable,并更新sizeCtl为新数组大小的0.75倍 ,扩容完成。在此期间如果其他线程的有读写操作都会判断head节点是否为forwardNode节点,如果是就帮助扩容。

在扩容时读写操作如何进行

      (1)对于get读操作,如果当前节点有数据,还没迁移完成,此时不影响读,能够正常进行。

如果当前链表已经迁移完成,那么头节点会被设置成fwd节点,此时get线程会帮助扩容。

    (2)对于put/remove写操作,如果当前链表已经迁移完成,那么头节点会被设置成fwd节点,此时写线程会帮助扩容,如果扩容没有完成,当前链表的头节点会被锁住,所以写线程会被阻塞,直到扩容完成。

对于size和迭代器是弱一致性

      volatile修饰的数组引用是强可见的,但是其元素却不一定,所以,这导致size的根据sumCount的方法并不准确。

       同理Iteritor的迭代器也一样,并不能准确反映最新的实际情况

17 JDK7和JDK8中HashMap为什么是线程不安全的?

前言

      只要是对于集合有一定了解的一定都知道HashMap是线程不安全的,我们应该使用ConcurrentHashMap。但是为什么HashMap是线程不安全的呢,之前面试的时候也遇到到这样的问题,但是当时只停留在***知道是***的层面上,并没有深入理解***为什么是***。于是今天重温一个HashMap线程不安全的这个问题。

       首先需要强调一点,HashMap的线程不安全体现在会造成死循环、数据丢失、数据覆盖这些问题。其中死循环和数据丢失是在JDK1.7中出现的问题,在JDK1.8中已经得到解决,然而1.8中仍会有数据覆盖这样的问题。

      1.7因为hashmap底层数据结构为数组+链表,在扩容时使用的头插法,高并发情况可能会出现环形链表的结构,造成死循环。 1.8数据结构为使用了红黑树扩容时采用尾插法,不会改变原有的数据结构。1.8不安全主要是因为put/get方法都没有加同步锁,高并发情况下容易出现上一秒put的值,下一秒get的时候还是原值

扩容引发的线程不安全

  HashMap的线程不安全主要是发生在扩容函数中,即根源是在transfer函数中,JDK1.7中HashMaptransfer函数如下:

void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

      这段代码是HashMap的扩容操作,重新定位每个桶的下标,并采用头插法将元素迁移到新数组中。头插法会将链表的顺序翻转,这也是形成死循环的关键点。理解了头插法后再继续往下看是如何造成死循环以及数据丢失的。

扩容造成死循环和数据丢失的分析过程

假设现在有两个线程A、B同时对下面这个HashMap进行扩容操作:

正常扩容后的结果是下面这样的:

    但是当线程A执行到上面transfer函数的 newTable[i] = e 代码时,CPU时间片耗尽,线程A被挂起。即如下图中位置所示:

 

此时线程A中:e=3、next=7、e.next=null

 

当线程A的时间片耗尽后,CPU开始执行线程B,并在线程B中成功的完成了数据迁移 

      重点来了,根据Java内存模式可知,线程B执行完数据迁移后,此时主内存中newTabletable都是最新的,也就是说:7.next=3、3.next=null。

      随后线程A获得CPU时间片继续执行newTable[i] = e,将3放入新数组对应的位置,执行完此轮循环后线程A的情况如下:

     接着继续执行下一轮循环,此时e=7,从主内存中读取e.next时发现主内存中7.next=3,于是乎next=3,并将7采用头插法的方式放入新数组中,并继续执行完此轮循环,结果如下: 

     执行下一次循环可以发现,next=e.next=null,所以此轮循环将会是最后一轮循环。接下来当执行完e.next=newTable[i]即3.next=7后,3和7之间就相互连接了,当执行完newTable[i]=e后,3被头插法重新插入到链表中,执行结果如下图所示: 

       上面说了此时e.next=null即next=null,当执行完e=null后,将不会进行下一轮循环。到此线程A、B的扩容操作完成,很明显当线程A执行完后,HashMap中出现了环形结构,当在以后对该HashMap进行操作时会出现死循环。

     并且从上图可以发现,元素5在扩容期间被莫名的丢失了,这就发生了数据丢失的问题。

JDK1.8中的线程不安全

      根据上面JDK1.7出现的问题,在JDK1.8中已经得到了很好的解决,如果你去阅读1.8的源码会发现找不到transfer函数,因为JDK1.8直接在resize函数中完成了数据迁移。另外说一句,JDK1.8在进行元素插入时使用的是尾插法。

    为什么说JDK1.8会出现数据覆盖的情况喃,我们来看一下下面这段JDK1.8中的put操作代码:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null) // 如果没有hash碰撞则直接插入元素
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            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;
    }

  

      其中第六行代码是判断是否出现hash碰撞,假设两个线程A、B都在进行put操作,并且hash函数计算出的插入下标是相同的,当线程A执行完第六行代码后由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全。

      除此之前,还有就是代码的第38行处有个++size,我们这样想,还是线程A、B,这两个线程同时进行put操作时,假设当前HashMap的zise大小为10,当线程A执行到第38行代码时,从主内存中获得size的值为10后准备进行+1操作,但是由于时间片耗尽只好让出CPU,线程B快乐的拿到CPU还是从主内存中拿到size的值10进行+1操作,完成了put操作并将size=11写回主内存,然后线程A再次拿到CPU并继续执行(此时size的值仍为10),当执行完put操作后,还是将size=11写回内存,此时,线程A、B都执行了一次put操作,但是size的值只增加了1,所以说还是由于数据覆盖又导致了线程不安全。

总结

HashMap的线程不安全主要体现在下面两个方面:

   1.在JDK1.7中,当并发执行扩容操作时会造成环形链和数据丢失的情况。

   2.在JDK1.8中,在并发执行put操作时会发生数据覆盖的情况。

17 ConcurrentHashMap了解吗?说说实现原理。

  • HashMap 是线程不安全的,效率高;HashTable 是线程安全的,效率低。
  • ConcurrentHashMap 可以做到既是线程安全的,同时也可以有很高的效率,得益于使用了分段锁。

实现原理

JDK 1.7

  • ConcurrentHashMap 是通过数组 + 链表实现,由 Segment 数组和 Segment 元素里对应多个 HashEntry 组成
  • value 和链表都是 volatile 修饰,保证可见性
  • ConcurrentHashMap 采用了分段锁技术,分段指的就是 Segment 数组,其中 Segment 继承于 ReentrantLock
  • 理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发,每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment

put 方法的逻辑较复杂:

  • 尝试加锁,加锁失败 scanAndLockForPut 方法自旋,超过 MAX_SCAN_RETRIES 次数,改为阻塞锁获取
  • 将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry
  • 遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value
  • 不为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容
  • 最后释放所获取当前 Segment 的锁

get 方法较简单:

  • 将 key 通过 hash 之后定位到具体的 Segment,再通过一次 hash 定位到具体的元素上
  • 由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了其内存可见性

JDK 1.8

  • 抛弃了原有的 Segment 分段锁,采用了 CAS + synchronized 来保证并发安全性
  • HashEntry 改为 Node,作用相同
  • val next 都用了 volatile 修饰

put 方法逻辑:

  • 根据 key 计算出 hash 值
  • 判断是否需要进行初始化
  • 根据 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋
  • 如果当前位置的 hashcode == MOVED == -1,则需要扩容
  • 如果都不满足,则利用 synchronized 锁写入数据
  • 如果数量大于 TREEIFY_THRESHOLD 则转换为红黑树

get 方法逻辑:

  • 根据计算出来的 hash 值寻址,如果在桶上直接返回值
  • 如果是红黑树,按照树的方式获取值
  • 如果是链表,按链表的方式遍历获取值

JDK 1.7 到 JDK 1.8 中的 ConcurrentHashMap 最大的改动:

  • 链表上的 Node 超过 8 个改为红黑树,查询复杂度 O(logn)
  • ReentrantLock 显示锁改为 synchronized,说明 JDK 1.8 中 synchronized 锁性能赶上或超过 ReentrantLock

18 为什么Hashtable、ConcurrentHashmap不支持key或者value为null

      在很多java资料中,都有提到 ConcurrentHashmap HashMap和Hashtable都是key-value存储结构,但他们有一个不同点是 ConcurrentHashmap、Hashtable不支持key或者value为null,而HashMap是支持的。为什么会有这个区别?在设计上的目的是什么?

        在网上找到了这样的解答:The main reason that nulls aren’t allowed in ConcurrentMaps (ConcurrentHashMaps, ConcurrentSkipListMaps) is that ambiguities that may be just barely tolerable in non-concurrent maps can’t be accommodated. The main one is that if map.get(key) returns null, you can’t detect whether the key explicitly maps to null vs the key isn’t mapped. In a non-concurrent map, you can check this via map.contains(key), but in a concurrent one, the map might have changed between calls.

        理解如下:ConcurrentHashmap和Hashtable都是支持并发的,这样会有一个问题,当你通过get(k)获取对应的value时,如果获取到的是null时,你无法判断,它是put(k,v)的时候value为null,还是这个key从来没有做过映射。HashMap是非并发的,可以通过contains(key)来做这个判断。而支持并发的Map在调用m.contains(key)和m.get(key),m可能已经不同了。

总结

      ConcurrentHashMap与HashTable因为在多线程并发的情况下,put操作时无法分辨key是没找到为null,还是key值对应的value为null,所以基于这类情况不允许键值为null。

HashMap是线程不安全的,也就无所谓了。

19 HashSet实现原理是什么?有什么特点?

  • HashSet 是基于 HashMap 实现的,查询速度特别快
  • HashMap 是支持 key 为 null 值的,所以 HashSet 支持添加 null 值
  • HashSet 存放自定义类时,自定义类需要重写 hashCode() 和 equals() 方法,确保集合对自定义类的对象的唯一性判断(具体判断逻辑,见 HashMap put() 方法,简单概括就是 key 进行 哈希。判断元素 hash 值是否相等、key 是否为同个对象、key 是否 equals。第 1 个条件为 true,2、3 有一个为 true,HashMap 即认为 key 相同)
  • 无序、不可重复

20 TreeSet的原理是什么?使用需要注意什么?

TreeSet 基于 TreeMap 实现,TreeMap 基于红黑树实现

特点

  • 有序
  • 无重复
  • 添加、删除元素、判断元素是否存在,效率比较高,时间复杂度为 O(log(N))

使用方式

  • TreeSet 默认构造方法,调用 add() 方法时会调用对象类实现的 Comparable 接口的 compareTo() 方法和集合中的对象比较,根据方法返回的结果有序存储
  • TreeSet 默认构造方法,存入对象的类未实现 Comparable 接口,抛出 ClassCastException
  • TreeSet 支持构造方法指定 Comparator 接口,按照 Comparator 实现类的比较逻辑进行有序存储

21 LinkedList的pop()方法和push()方法

 

LinkedList<Integer> linkedList = new LinkedList<>();
for (int i = 0; i < 10; i++) {
    linkedList.add(i);
}
//pop()方法 出栈操作,获取栈顶元素,获取后该元素就从栈中被删除了
System.out.println(linkedList.pop());//0
System.out.println(linkedList);//[1, 2, 3, 4, 5, 6, 7, 8, 9]
//push()方法 添加一个元素,新入栈的元素会在栈顶(栈中第一个元素)
linkedList.push(10);
System.out.println(linkedList);//[10, 1, 2, 3, 4, 5, 6, 7, 8, 9]

22 ArrayList和LinkedList区别

      数据结构实现:ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。

     随机访问效率:ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。

    增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其他数据的下标。

     综合来说,在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多时,更推荐使用 LinkedList。

linkedLIst是链表结构,何为链表?

       就是元素之间的所有关系是通过引用关联的,比如说一节车厢,你只能知道当前车厢的上一节车厢是什么,当前车厢的下一节车厢是什么,这样,它在查询的时候,只能一个一个的遍历查询,所以他的查询效率很低,如果我们想删除一节车厢怎么办呢?就相当于自行车的链子,有一节坏了,我们是不是直接把坏的那节仍掉,然后让目标节的上一节指向目标节的下一节,增加同样,假如有abc三节车厢,你想在b后面增加车厢,那么只需要让目标车厢的上一节指向b,让目标车厢的下一节指向c,别的元素不动,所以它的增删在理论上比较快!

ArrayList是数组结构,何为数组?

    就是有相同特性的一组数据的箱子,比如说我有一个能容下10个苹果的箱子,我现在只放了5个苹果,那么放第6个是不是直接放进去就行了?呢我要放11个呢?这个箱子是不是放不下了?所以我是不是需要换个大点的箱子?这就是数组的扩容!同样,我们一般放箱子里面的东西是不是按照顺序放的?假如说是按abcd的顺序放的,我突然想添加一个e,这个e要放到c的后面,你是不是需要把d先拿出来,再把e放进去,再把d放进去?假如说c后面有10000个呢?你是不是要把这10000个都拿出来,把e放进去,再放这10000个?效率是不是很低了?所以,理论上它的增删比较慢!但是前面也说了,我们箱子里面放东西,都是按照顺序放的,所以我知道其中一个"地址",是不是就知道所有元素的地址?所以它的查询在理论上比较快!

23 ArrayList和Vector 区别

      线程安全:Vector 使用了 synchronized 来实现线程同步,是线程安全的,而 ArrayList 是非线程安全的。

     性能:ArrayList 在性能方面要优于 Vector。

     扩容:ArrayList 和 Vector 都会根据实际的需要动态的调整容量,只不过在 Vector 扩容每次会增加 1 倍,而 ArrayList 只会增加 50%。

24 Array(数组)和ArrayList区别

     Array 可以存储基本数据类型和对象,ArrayList 只能存储对象。

     Array 是指定固定大小的,而 ArrayList 大小是自动扩展的。

      Array 内置方法没有 ArrayList 多,比如 addAll、removeAll、iteration 等方法只有 ArrayList 有。

25 LinkedHashMap、LinkedHashSet、LinkedList哪个最适合当作Stack使用?

     LinkedList

分析

  • Stack 是线性结构,具有先进后出的特点
  • LinkedList 天然支持 Stack 的特性,调用 push(E e) 方法放入元素,调用 pop() 方法取出栈顶元素,内部实现只需要移动指针即可
  • LinkedHashSet 是基于 LinkedHashMap 实现的,记录添加顺序的 Set 集合
  • LinkedHashMap 是基于 HashMap 和 链表实现的,记录添加顺序的键值对集合
  • 如果要删除后进的元素,需要使用迭代器遍历、取出最后一个元素,移除,性能较差

26 哪些集合类是线程安全的

       VectorHashtableStack 都是线程安全的,而像 HashMap 则是非线程安全的,不过在 JDK 1.5 之后随着 Java. util. concurrent 并发包的出现,它们也有了自己对应的线程安全类,比如 HashMap 对应的线程安全类就是 ConcurrentHashMap

27 迭代器 Iterator 是什么

      Iterator 接口提供遍历任何 Collection 的接口。我们可以从一个 Collection 中使用迭代器方法来获取迭代器实例。迭代器取代了 Java 集合框架中的 Enumeration,迭代器允许调用者在迭代过程中移除元素。

28 Iterator 怎么使用?有什么特点?

Iterator 接口源码中的方法

  • java.lang.Iterable 接口被 java.util.Collection 接口继承,java.util.Collection 接口的 iterator() 方法返回一个 Iterator 对象
  • next() 方法获得集合中的下一个元素
  • hasNext() 检查集合中是否还有元素
  • remove() 方法将迭代器新返回的元素删除
  • forEachRemaining(Consumer<? super E> action) 方法,遍历所有元素

Iterator 使用代码如下:

Iterator<User> iterator = userList.iterator();
while (iterator.hasNext()) {
    System.out.println(iterator.next());
}

      Iterator 的特点是更加安全,因为它可以确保,在当前遍历的集合元素被更改的时候,就会抛出 ConcurrentModificationException 异常。

29 Iterator 和 ListIterator 区别

     ListIterator 继承 Iterator, ListIterator 比 Iterator多方法

1) add(E e)  将指定的元素插入列表,插入位置为迭代器当前位置之前

2) set(E e)  迭代器返回的最后一个元素替换参数e

3) hasPrevious()  迭代器当前位置,反向遍历集合是否含有元素

4) previous()  迭代器当前位置,反向遍历集合,下一个元素

5) previousIndex()  迭代器当前位置,反向遍历集合,返回下一个元素的下标

6) nextIndex()  迭代器当前位置,返回下一个元素的下标

  • 使用范围不同,Iterator可以迭代所有集合;ListIterator 只能用于List及其子类
  • ListIterator 有 add 方法,可以向 List 中添加对象;Iterator 不能
  • ListIterator 有 hasPrevious() 和 previous() 方法,可以实现逆向遍历;Iterator不可以
  • ListIterator 有 nextIndex() 和previousIndex() 方法,可定位当前索引的位置;Iterator不可以
  • ListIterator 有 set()方法,可以实现对 List 的修改;Iterator 仅能遍历,不能修改

30 怎么确保一个集合不能被修改

      可以使用 Collections. unmodifiableCollection(Collection c) 方法来创建一个只读集合,这样改变集合的任何操作都会抛出 Java. lang. UnsupportedOperationException 异常。

Unmodifiable:不可修改的

示例代码如下:

List<String> list = new ArrayList<>();
list.add("x");
Collection<String> collection = Collections.unmodifiableCollection(list);
collection.add("y"); // 运行时此行报UnsupportedOperationException异常
System.out.println(list.size());

31 为什么基本类型不能做为HashMap的键值?

  • Java中是使用泛型来约束 HashMap 中的key和value的类型的,HashMap<K, V>
  • 泛型在Java的规定中必须是对象Object类型的,基本数据类型不是Object类型,不能作为键值
  • map.put(0, "Lee")中编译器已将 key 值 0 进行了自动装箱,变为了 Integer 类型

32 HashMap 排序题,上机题

     已知一个 HashMap<Integer,User>集合, User 有 name(String)和 age(int)属性。请写一个方法实现对HashMap 的排序功能,该方法接收 HashMap<Integer,User>为形参,返回类型为 HashMap<Integer,User>,要求对 HashMap 中的 User 的 age 倒序进行排序。排序时 key=value  键值对不得拆散。

    注意:要做出这道题必须对集合的体系结构非常的熟悉。HashMap本身就是不可排序的,但是该道题偏偏让给

      HashMap 排序,那我们就得想在 API 中有没有这样的 Map 结构是有序的,LinkedHashMap,对的,就是他,他是

     Map 结构,也是链表结构,有序的,更可喜的是他是 HashMap 的子类,我们返回  LinkedHashMap<Integer,User>

     即可,还符合面向接口(父类编程的思想)。

        但凡是对集合的操作,我们应该保持一个原则就是能用 JDK 中的 API 就有 JDK 中的 API,比如排序算法我们不应

     该去用冒泡或者选择,而是首先想到用 Collections  集合工具类。

代码如下:

HashMap<Integer, User> hashMap = new HashMap();
hashMap.put(1, User.builder().name("Lee").age(25).build());
hashMap.put(2, User.builder().name("Tom").age(23).build());
hashMap.put(3, User.builder().name("Jack").age(27).build());
System.out.println(hashMap);
System.out.println("---------------");
Set<Map.Entry<Integer, User>> set = hashMap.entrySet();
set.forEach(System.out::println);
System.out.println("---------------");
//转换成List
List<Map.Entry<Integer, User>> list = new ArrayList(set);
list.sort((o1, o2) -> {
    //当返回值为-1时,也就是说o1的值小于o2的值时 ,compareTo是按照降序(由大到小)排序的!
    return o1.getValue().getAge() > o2.getValue().getAge() ? -1 : 1;
});
list.forEach(System.out::println);
//新建LinkedHashMap
LinkedHashMap<Object, Object> linkedHashMap = new LinkedHashMap<>();
for (Map.Entry<Integer, User> userEntry : list) {
    linkedHashMap.put(userEntry.getKey(), userEntry.getValue());
}
System.out.println("---------------");
linkedHashMap.forEach((k, v) -> System.out.println(k + ":" + v));

33 请问 ArrayList、HashSet、HashMap  是线程安全的吗?如果不是我想要线程安全的集合怎么办?

       我们都看过上面那些集合的源码(如果没有那就看看吧),每个方法都没有加锁,显然都是线程不安全的。话又说

      过来如果他们安全了也就没第二问了。

      在集合中   Vector  和  HashTable  倒是线程安全的。你打开源码会发现其实就是把各自核心方法添加上了

synchronized  关键字。

     Collections 工具类提供了相关的 API,可以让上面那 3  个不安全的集合变为安全的

Collections.synchronizedCollection(c);
Collections.synchronizedList(list);
Collections.synchronizedMap(m);
Collections.synchronizedSet(s);

上面几个函数都有对应的返回值类型,传入什么类型返回什么类型。打开源码其实实现原理非常简单,就是将集合的核心方法添加上了 synchronized  关键字。

34 并发集合和普通集合如何区别?

      并发集合常见的有 ConcurrentHashMapConcurrentLinkedQueueConcurrentLinkedDeque  等。并发集合位于java.util.concurrent包下,是jdk1.5之后才有的,主要作者是Doug Lea (http://baike.baidu.com/view/3141057.htm)完成的。

       在 java  中有普通集合同步(线程安全)的集合并发集合。普通集合通常性能最高,但是不保证多线程的安全性和并发的可靠性。线程安全集合仅仅是给集合添加了 synchronized  同步锁,严重牺牲了性能,而且对并发的效率就更低了,并发集合则通过复杂的策略不仅保证了多线程的安全又提高的并发时的效率。

       ConcurrentHashMap 线程安全的 HashMap 的实现,默认构造同样有 initialCapacity 和 loadFactor  属性,不过还多了一个 concurrencyLevel 属性,三属性默认值分别为 16、0.75  及 16。其内部使用锁分段技术,维持这锁Segment 的数组,在 Segment 数组中又存放着 Entity[]数组,内部 hash  算法将数据较均匀分布在不同锁中。put 操作:并没有在此方法上加上 synchronized,首先对 key.hashcode 进行 hash 操作,得到 key 的 hash  值。hash操作的算法和map也不同,根据此hash值计算并获取其对应的数组中的Segment对象(继承自ReentrantLock),接着调用此 Segment 对象的 put  方法来完成当前操作。

       ConcurrentHashMap 基于 concurrencyLevel 划分出了多个 Segment 来对 key-value进行存储,从而避免每次 put 操作都得锁住整个数组。在默认的情况下,最佳情况下可允许 16  个线程并发无阻塞的操作集合对象,尽可能地减少并发时的阻塞现象。

         get(key)首先对 key.hashCode 进行 hash 操作,基于其值找到对应的 Segment  对象,调用其 get  方法完成当前操作。而 Segment  的  get 操作首先通过  hash 值和对象数组大小减  1  的值进行按位与操作来获取数组上对应位置的HashEntry。在这个步骤中,可能会因为对象数组大小的改变,以及数组上对应位置的 HashEntry  产生不一致性,那么 ConcurrentHashMap  是如何保证的?

      对象数组大小的改变只有在 put  操作时有可能发生,由于 HashEntry 对象数组对应的变量是  volatile  类型的,因此可以保证如 HashEntry  对象数组大小发生改变,读操作可看到最新的对象数组大小。

       在获取到了   HashEntry  对象后,怎么能保证它及其    next  属性构成的链表上的对象不会改变呢?这点ConcurrentHashMap 采用了一个简单的方式,即 HashEntry 对象中的 hash、key、next 属性都是 final  的,这也就意味着没办法插入一个 HashEntry 对象到基于 next属性构成的链表中间或末尾。这样就可以保证当获取到  HashEntry对象后,其基于 next  属性构建的链表是不会发生变化的。

       ConcurrentHashMap 默认情况下采用将数据分为  16 个段进行存储,并且16个段分别持有各自不同的锁Segment,锁仅用于 put 和 remove 等改变集合对象的操作,基于 volatile 及 HashEntry  链表的不变性实现了读取的不加锁。这些方式使得 ConcurrentHashMap 能够保持极好的并发支持,尤其是对于读远比插入和删除频繁的  Map而言,而它采用的这些方法也可谓是对于 Java内存模型、并发机制深刻掌握的体现。

35 为什么HashMap是线程不安全的

HashMap 并发执行 put 操作时会引起死循环,导致 CPU 利用率接近100%。因为多线程会导致 HashMap 的 Node 链表形成环形数据结构,一旦形成环形数据结构,Node 的 next 节点永远不为空,就会在获取 Node 时产生死循环。

36 ArrayList list = new ArrayList(20);中的list扩充几次?

0次。

      解析:ArrayList list=new ArrayList();   这种是默认创建大小为10的数组,每次扩容大小为1.5倍。

   ArrayList list=new ArrayList(20);  这种是指定数组大小的创建,没有扩充。

所以最好指定大小来创建数组

37 foreach与正常for循环效率对比

       用for循环arrayList 10万次花费时间:5毫秒。 用foreach循环arrayList 10万次花费时间:7毫秒。 用for循环LinkedList 10万次花费时间:4481毫秒。 用foreach循环LinkedList10万次花费时间:5毫秒。

      循环ArrayList时,普通for循环比foreach循环花费的时间要少一点。 循环LinkList时,普通for循环比foreach循环花费的时间要多很多。

      当我将循环次数提升到一百万次的时候,循环ArrayList,普通for循环还是比foreach要快一点;但是普通for循环在循环LinkedList时,程序直接卡死。

       ArrayList:ArrayList是采用数组的形式保存对象的,这种方式将对象放在连续的内存块中,所以插入和删除时比较麻烦,查询比较方便。

        LinkedList:LinkedList是将对象放在独立的空间中,而且每个空间中还保存下一个空间的索引,也就是数据结构中的链表结构,插入和删除比较方便,但是查找很麻烦,要从第一个开始遍历。

结论

      需要循环数组结构的数据时,建议使用普通for循环,因为for循环采用下标访问,对于数组结构的数据来说,采用下标访问比较好。

     需要循环链表结构的数据时,一定不要使用普通for循环,这种做法很糟糕,数据量大的时候有可能会导致系统崩溃。

38 为什么基类不能做为 HashMap 的键值,而只能是引用类型,把引用类型作为HashMap 的键值, 需要注意哪些地方?

      引用类型和原始类型的行为完全不同,并且它们具有不同的语义。引用类型和原始类型具有不同的特征和用法,它们包括:大小和速度问题,这种类型以哪种类型的数据结构存储,当引用类型和原始类型用作某个类的实例数据时所指定的缺省值。对象引用实例变量的缺省值为 null,而原始类型实例变量的缺省值与它们的类型有关

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

一:快速失败(fail—fast)

       在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出Concurrent Modification Exception。

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

      注意:这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的bug。

      场景:java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改)。

二:安全失败(fail—safe)

      采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。

原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception。

缺点:基于拷贝内容的优点是避免了Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。

场景:java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。

40 Java中的队列都有哪些,有什么区别

        Queue: 基本上,一个队列就是一个先入先出(FIFO)的数据结构

Queue接口与List、Set同一级别,都是继承了Collection接口。LinkedList实现了Deque接 口。

Queue的实现

1、没有实现的阻塞接口的LinkedList: 实现了java.util.Queue接口和java.util.AbstractQueue接口

  内置的不阻塞队列: PriorityQueue 和 ConcurrentLinkedQueue

  PriorityQueue 和 ConcurrentLinkedQueue 类在 Collection Framework 中加入两个具体集合实现。 

  PriorityQueue 类实质上维护了一个有序列表。加入到 Queue 中的元素根据它们的天然排序(通过其 java.util.Comparable 实现)或者根据传递给构造函数的 java.util.Comparator 实现来定位。

  ConcurrentLinkedQueue 是基于链接节点的、线程安全的队列。并发访问不需要同步。因为它在队列的尾部添加元素并从头部删除它们,所以只要不需要知道队列的大 小, ConcurrentLinkedQueue 对公共集合的共享访问就可以工作得很好。收集关于队列大小的信息会很慢,需要遍历队列。

2)实现阻塞接口的:

  java.util.concurrent 中加入了 BlockingQueue 接口和五个阻塞队列类。它实质上就是一种带有一点扭曲的 FIFO 数据结构。不是立即从队列中添加或者删除元素,线程执行操作阻塞,直到有空间或者元素可用。

五个队列所提供的各有不同:

  * ArrayBlockingQueue :一个由数组支持的有界队列。

  * LinkedBlockingQueue :一个由链接节点支持的可选有界队列。

  * PriorityBlockingQueue :一个由优先级堆支持的无界优先级队列。

  * DelayQueue :一个由优先级堆支持的、基于时间的调度队列。

  * SynchronousQueue :一个利用 BlockingQueue 接口的简单聚集(rendezvous)机制。

下表显示了jdk1.5中的阻塞队列的操作:

  add        增加一个元索                     如果队列已满,则抛出一个IIIegaISlabEepeplian异常

  remove   移除并返回队列头部的元素    如果队列为空,则抛出一个NoSuchElementException异常

  element  返回队列头部的元素             如果队列为空,则抛出一个NoSuchElementException异常

  offer       添加一个元素并返回true       如果队列已满,则返回false

  poll         移除并返问队列头部的元素    如果队列为空,则返回null

  peek       返回队列头部的元素             如果队列为空,则返回null

  put         添加一个元素                      如果队列满,则阻塞

  take        移除并返回队列头部的元素     如果队列为空,则阻塞

remove、element、offer 、poll、peek 其实是属于Queue接口。 

      阻塞队列的操作可以根据它们的响应方式分为以下三类:add、remove和element操作在你试图为一个已满的队列增加元素或从空队列取得元素时 抛出异常。当然,在多线程程序中,队列在任何时间都可能变成满的或空的,所以你可能想使用offer、poll、peek方法。这些方法在无法完成任务时 只是给出一个出错示而不会抛出异常。

      注意:poll和peek方法出错进返回null。因此,向队列中插入null值是不合法的

     最后,我们有阻塞操作put和take。put方法在队列满时阻塞,take方法在队列空时阻塞。

     LinkedBlockingQueue的容量是没有上限的(说的不准确,在不指定时容量为Integer.MAX_VALUE,不要然的话在put时怎么会受阻呢),但是也可以选择指定其最大容量,它是基于链表的队列,此队列按 FIFO(先进先出)排序元素。

      ArrayBlockingQueue在构造时需要指定容量, 并可以选择是否需要公平性,如果公平参数被设置true,等待时间最长的线程会优先得到处理(其实就是通过将ReentrantLock设置为true来 达到这种公平性的:即等待时间最长的线程会先操作)。通常,公平性会使你在性能上付出代价,只有在的确非常需要的时候再使用它。它是基于数组的阻塞循环队 列,此队列按 FIFO(先进先出)原则对元素进行排序。

        PriorityBlockingQueue是一个带优先级的 队列,而不是先进先出队列。元素按优先级顺序被移除,该队列也没有上限(看了一下源码,PriorityBlockingQueue是对 PriorityQueue的再次包装,是基于堆数据结构的,而PriorityQueue是没有容量限制的,与ArrayList一样,所以在优先阻塞 队列上put时是不会受阻的。虽然此队列逻辑上是无界的,但是由于资源被耗尽,所以试图执行添加操作可能会导致 OutOfMemoryError),但是如果队列为空,那么取元素的操作take就会阻塞,所以它的检索操作take是受阻的。另外,往入该队列中的元 素要具有比较能力。

        DelayQueue(基于PriorityQueue来实现的)是一个存放Delayed 元素的无界阻塞队列,只有在延迟期满时才能从中提取元素。该队列的头部是延迟期满后保存时间最长的 Delayed 元素。如果延迟都还没有期满,则队列没有头部,并且poll将返回null。当一个元素的 getDelay(TimeUnit.NANOSECONDS) 方法返回一个小于或等于零的值时,则出现期满,poll就以移除这个元素了。此队列不允许使用 null 元素。

 

 一个例子:
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class BlockingQueueTest {
 /**
 定义装苹果的篮子
  */
 public static class Basket{
  // 篮子,能够容纳3个苹果
  BlockingQueue<String> basket = new ArrayBlockingQueue<String>(3);
  // 生产苹果,放入篮子
  public void produce() throws InterruptedException{
   // put方法放入一个苹果,若basket满了,等到basket有位置
   basket.put("An apple");
  }
  // 消费苹果,从篮子中取走
  public String consume() throws InterruptedException{
   // get方法取出一个苹果,若basket为空,等到basket有苹果为止
   String apple = basket.take();
   return apple;
  }
  public int getAppleNumber(){
   return basket.size();
  }
 }
 // 测试方法
 public static void testBasket() {
  // 建立一个装苹果的篮子
  final Basket basket = new Basket();
  // 定义苹果生产者
  class Producer implements Runnable {
   public void run() {
    try {
     while (true) {
      // 生产苹果
      System.out.println("生产者准备生产苹果:" 
        + System.currentTimeMillis());
      basket.produce();
      System.out.println("生产者生产苹果完毕:" 
        + System.currentTimeMillis());
      System.out.println("生产完后有苹果:"+basket.getAppleNumber()+"个");
      // 休眠300ms
      Thread.sleep(300);
     }
    } catch (InterruptedException ex) {
    }
   }
  }
  // 定义苹果消费者
  class Consumer implements Runnable {
   public void run() {
    try {
     while (true) {
      // 消费苹果
      System.out.println("消费者准备消费苹果:" 
        + System.currentTimeMillis());
      basket.consume();
      System.out.println("消费者消费苹果完毕:" 
        + System.currentTimeMillis());
      System.out.println("消费完后有苹果:"+basket.getAppleNumber()+"个");
      // 休眠1000ms
      Thread.sleep(1000);
     }
    } catch (InterruptedException ex) {
    }
   }
  }
  ExecutorService service = Executors.newCachedThreadPool();
  Producer producer = new Producer();
  Consumer consumer = new Consumer();
  service.submit(producer);
  service.submit(consumer);
  // 程序运行10s后,所有任务停止
  try {
   Thread.sleep(10000);
  } catch (InterruptedException e) {
  }
  service.shutdownNow();
 }
 public static void main(String[] args) {
  BlockingQueueTest.testBasket();
 }
}

     在java5中新增加了java.util.Queue接口,用以支持队列的常见操作。Queue接口与List、Set同一级别,都是继承了Collection接口。

     Queue使用时要尽量避免Collection的add()和remove()方法,而是要使用offer()来加入元素,使用poll()来获取并移出元素。它们的优点是通过返回值可以判断成功与否,add()和remove()方法在失败的时候会抛出异常。 如果要使用前端而不移出该元素,使用element()或者peek()方法。值得注意的是LinkedList类实现了Queue接口,因此我们可以把LinkedList当成Queue来用。LinkedList实现了Queue接口。Queue接口窄化了对LinkedList的方法的访问权限(即在方法中的参数类型如果是Queue时,就完全只能访问Queue接口所定义的方法 了,而不能直接访问 LinkedList的非Queue的方法),以使得只有恰当的方法才可以使用。BlockingQueue 继承了Queue接口

import java.util.Queue;
import java.util.LinkedList;
public class TestQueue {
     public static void main(String[] args) {
         Queue<String> queue = new LinkedList<String>();
         queue.offer("Hello");
         queue.offer("World!");
         queue.offer("你好!");
         System.out.println(queue.size());
         String str;
         while((str=queue.poll())!=null){
             System.out.print(str);
         }
         System.out.println();
         System.out.println(queue.size());
     }
}

41 Queue的add()和offer()方法有什么区别?

  1. Queue 中 add() 和 offer() 都是用来向队列添加一个元素。
  2. 在容量已满的情况下,add() 方法会抛出IllegalStateException异常,offer() 方法只会返回 false 。

42 Queue中poll()和remove()区别

        相同点:都是返回第一个元素,并在队列中删除返回的对象。

        不同点:如果没有元素 poll()会返回 null,而 remove()会直接抛出 NoSuchElementException 异常。

代码示例:

Queue<String> queue = new LinkedList<>();
queue.offer("string");
//poll() 移除并返问队列头部的元素,如果队列为空,则返回null
System.out.println(queue.poll());
//remove() 移除并返回队列头部的元素,如果队列为空,则抛出一个NoSuchElementException异常
System.out.println(queue.remove());

43 Queue的element()和peek()方法有什么区别?

  1. Queue 中 element() 和 peek() 都是用来返回队列的头元素,不删除。
  2. 在队列元素为空的情况下,element() 方法会抛出NoSuchElementException异常,peek() 方法只会返回 null。
Queue<String> queue = new LinkedList<>();
queue.offer("string");
//peek() 返回队列头部的元素,如果队列为空,则返回null
System.out.println(queue.peek());
//element()  返回队列头部的元素,如果队列为空,则抛出一个NoSuchElementException异常
System.out.println(queue.element());

44 阻塞队列

阻塞队列实现了 BlockingQueue 接口,并且有多组处理方法。

抛出异常:add(e) 、remove()、element()

返回特殊值:offer(e) 、pool()、peek()

阻塞:put(e) 、take()

JDK 8 中提供了七个阻塞队列可供使用:

  1. ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
  2. LinkedBlockingQueue :一个由链表结构组成的无界阻塞队列。
  3. PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
  4. DelayQueue:一个使用优先级队列实现的无界阻塞队列。
  5. SynchronousQueue:一个不存储元素的阻塞队列。
  6. LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
  7. LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

ArrayBlockingQueue,一个由数组实现的有界阻塞队列。该队列采用 FIFO 的原则对元素进行排序添加的。内部使用可重入锁 ReentrantLock + Condition 来完成多线程环境的并发操作。

相关文章参考

阻塞队列 BlockingQueue

Java并发编程:阻塞队列

45 BlockingQueue相比普通的Queue最大的区别是什么?

     阻塞队列是与普通队列的区别有两点

    1.阻塞队列获取元素时,如果队列为空,则会等待队列有元素,否则就阻塞队列(普通队列返回结果,无元素)

   2.阻塞队列放入元素时,如果队列满,则等待队列,直到有空位置,然后插入。(普通队列,要么直接扩容,要么直接无法插入,不阻塞)

     阻塞队列的最佳场景就是生产者和消费者,使用代码时无需判断直接获取并处理(普通队列得判断有没有元素,阻塞队列不用判断,无元素自身就会阻塞,直到有东西)

46 BlockingQueue的实现类

       BlockingQueue只是java.util.concurrent包中的一个接口,而在具体使用时,我们用到的是它的实现类,当然这些实现类也位于java.util.concurrent包中。在Java6中,BlockingQueue的实现类主要有以下几种:

1. ArrayBlockingQueue

2. DelayQueue

3. LinkedBlockingQueue

4. PriorityBlockingQueue

5. SynchronousQueue

下面我们就分别介绍这几个实现类。

   4.1 ArrayBlockingQueue

       ArrayBlockingQueue是一个有边界的阻塞队列,它的内部实现是一个数组。有边界的意思是它的容量是有限的,我们必须在其初始化的时候指定它的容量大小,容量大小一旦指定就不可改变。

ArrayBlockingQueue是以先进先出的方式存储数据,最新插入的对象是尾部,最新移出的对象是头部。下面是一个初始化和使用ArrayBlockingQueue的例子:

BlockingQueue queue = new ArrayBlockingQueue(1024);
queue.put("1");
Object object = queue.take();

4.2 DelayQueue

       DelayQueue阻塞的是其内部元素,DelayQueue中的元素必须现 java.util.concurrent.Delayed接口,这个接口的定义非常简单:

public interface Delayed extends Comparable<Delayed> {
long getDelay(TimeUnit unit);
}

       getDelay()方法的返回值就是队列元素被释放前的保持时间,如果返回0或者一个负值,就意味着该元素已经到期需要被释放,此时DelayedQueue会通过其take()方法释放此对象。

从上面Delayed 接口定义可以看到,它还继承了Comparable接口,这是因为DelayedQueue中的元素需要进行排序,一般情况,我们都是按元素过期时间的优先级进行排序。

例1:为一个对象指定过期时间

首先,我们先定义一个元素,这个元素要实现Delayed接口

public class DelayedElement implements Delayed {
 private long expired;
 private long delay;
 private String name;
 
 DelayedElement(String elementName, long delay) {
   this. name = elementName;
   this. delay= delay;
   expired = ( delay + System. currentTimeMillis());
 }
 
 @Override
 public int compareTo(Delayed o) {
  DelayedElement cached=(DelayedElement) o;
   return cached.getExpired()> expired?1:-1;
 }
 
 @Override
 public long getDelay(TimeUnit unit) {
 
   return ( expired - System. currentTimeMillis());
 }
 
 @Override
 public String toString() {
   return "DelayedElement [delay=" + delay + ", name=" + name + "]";
 }
 
 public long getExpired() {
   return expired;
 }
 
}

设置这个元素的过期时间为3s

public class DelayQueueExample {
 public static void main(String[] args) throws InterruptedException {
  DelayQueue<DelayedElement> queue= new DelayQueue<>();
  DelayedElement ele= new DelayedElement( "cache 3 seconds",3000);
   queue.put( ele);
  System. out.println( queue.take());
 
 }
}

运行这个main函数,我们可以发现,我们需要等待3s之后才会打印这个对象。

其实DelayQueue应用场景很多,比如定时关闭连接、缓存对象,超时处理等各种场景,下面我们就拿学生考试为例让大家更深入的理解DelayQueue的使用。

例2:把所有考试的学生看做是一个DelayQueue,谁先做完题目释放谁

首先,我们构造一个学生对象

public class Student implements Runnable,Delayed{
 private String name; //姓名
 private long costTime;//做试题的时间
 private long finishedTime;//完成时间
 
 public Student(String name, long costTime) {
   this. name = name;
   this. costTime= costTime;
   finishedTime = costTime + System. currentTimeMillis();
 }
 
 @Override
 public void run() {
  System. out.println( name + " 交卷,用时" + costTime /1000);
 }
 
 @Override
 public long getDelay(TimeUnit unit) {
   return ( finishedTime - System. currentTimeMillis());
 }
 
 @Override
 public int compareTo(Delayed o) {
  Student other = (Student) o;
   return costTime >= other. costTime?1:-1;
 }
 
}

然后在构造一个教师对象对学生进行考试

public class Teacher {
 static final int STUDENT_SIZE = 30;
 public static void main(String[] args) throws InterruptedException {
  Random r = new Random();
  //把所有学生看做一个延迟队列
  DelayQueue<Student> students = new DelayQueue<Student>();
  //构造一个线程池用来让学生们“做作业”
  ExecutorService exec = Executors.newFixedThreadPool(STUDENT_SIZE);
   for ( int i = 0; i < STUDENT_SIZE; i++) {
    //初始化学生的姓名和做题时间
    students.put( new Student( "学生" + (i + 1), 3000 + r.nextInt(10000)));
  }
  //开始做题
  while(! students.isEmpty()){
    exec.execute( students.take());
  }
   exec.shutdown();
 }
}

我们看一下运行结果:

学生2 交卷,用时3

学生1 交卷,用时5

学生5 交卷,用时7

学生4 交卷,用时8

学生3 交卷,用时11

通过运行结果我们可以发现,每个学生在指定开始时间到达之后就会“交卷”(取决于getDelay()方法),并且是先做完的先交卷(取决于compareTo()方法)。

通过查看其源码可以看到,DelayQueue内部实现用的是PriorityQueue和一个Lock:

4.3 LinkedBlockingQueue

      LinkedBlockingQueue阻塞队列大小的配置是可选的,如果我们初始化时指定一个大小,它就是有边界的,如果不指定,它就是无边界的。说是无边界,其实是采用了默认大小为Integer.MAX_VALUE的容量 。它的内部实现是一个链表。

     和ArrayBlockingQueue一样,LinkedBlockingQueue 也是以先进先出的方式存储数据,最新插入的对象是尾部,最新移出的对象是头部。下面是一个初始化和使LinkedBlockingQueue的例子:

BlockingQueue<String> unbounded = new LinkedBlockingQueue<String>();
BlockingQueue<String> bounded = new LinkedBlockingQueue<String>(1024);
bounded.put("Value");
String value = bounded.take();

 

4.4 PriorityBlockingQueue

PriorityBlockingQueue是一个没有边界的队列,它的排序规则和 java.util.PriorityQueue一样。需要注意,PriorityBlockingQueue中允许插入null对象。

所有插入PriorityBlockingQueue的对象必须实现 java.lang.Comparable接口,队列优先级的排序规则就是按照我们对这个接口的实现来定义的。

另外,我们可以从PriorityBlockingQueue获得一个迭代器Iterator,但这个迭代器并不保证按照优先级顺序进行迭代。

下面我们举个例子来说明一下,首先我们定义一个对象类型,这个对象需要实现Comparable接口:

public class PriorityElement implements Comparable<PriorityElement> {
private int priority;//定义优先级
PriorityElement(int priority) {
 //初始化优先级
 this.priority = priority;
}
@Override
public int compareTo(PriorityElement o) {
 //按照优先级大小进行排序
 return priority >= o.getPriority() ? 1 : -1;
}
public int getPriority() {
 return priority;
}
public void setPriority(int priority) {
 this.priority = priority;
}
@Override
public String toString() {
 return "PriorityElement [priority=" + priority + "]";
}
}

然后我们把这些元素随机设置优先级放入队列中

public class PriorityBlockingQueueExample {

public static void main(String[] args) throws InterruptedException {

 PriorityBlockingQueue<PriorityElement> queue = new PriorityBlockingQueue<>();

 for (int i = 0; i < 5; i++) {

  Random random=new Random();

  PriorityElement ele = new PriorityElement(random.nextInt(10));

  queue.put(ele);

 }

 while(!queue.isEmpty()){

  System.out.println(queue.take());

 }

}

}

看一下运行结果:

PriorityElement [priority=3]

PriorityElement [priority=4]

PriorityElement [priority=5]

PriorityElement [priority=8]

PriorityElement [priority=9]

4.5 SynchronousQueue

      SynchronousQueue队列内部仅允许容纳一个元素。当一个线程插入一个元素后会被阻塞,除非这个元素被另一个线程消费。

47 请用两个队列模拟堆栈结构

     两个队列模拟一个堆栈,队列是先进先出,而堆栈是先进后出。模拟如下

队列 a 和  b

(1)入栈:a 队列为空,b 为空。例:则将”a,b,c,d,e”需要入栈的元素先放 a 中,a 进栈为”a,b,c,d,e”

(2)出栈:a 队列目前的元素为”a,b,c,,d,e”。将 a 队列依次加入 Arraylist 集合 a 中。以倒序的方法,将 a  中的集

合取出,放入 b 队列中,再将 b  队列出列。代码如下:

Queue<String> queue = new LinkedList<>();  //a队列
Queue<String> queue2 = new LinkedList<>();  //b队列
ArrayList<String> a = new ArrayList<>();
//往a队列添加元素
//arrylist集合是中间参数
queue.offer("a");
queue.offer("b");
queue.offer("c");
queue.offer("d");
queue.offer("e");
System.out.print("进栈:");
//a队列依次加入list集合之中
for (String q : queue) {
    a.add(q);
    System.out.print(q);
}
//以倒序的方法取出(a队列依次加入list集合)之中的值,加入b对列
for (int i = a.size() - 1; i >= 0; i--) {
    queue2.offer(a.get(i));
}
//打印出栈队列
System.out.print("出栈:");
for (String q : queue2) {
    System.out.print(q);
}

打印结果为(遵循栈模式先进后出):

进栈:a b c d e

出栈:e d c b a

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值