Java面试八股文-集合篇

目录

1、三大集合的区别是什么?/介绍一下集合

2、ArrayList和LinkedList的区别是什么?

3、ArrayList和LinkedList使用场景

4、ArrayList如何去重?

5、HashMap的底层原理实现

6、HashMap和LinkedHashMap的区别

7、HashMap和Hashtable的区别

8、ConcurrentHashMap和Hashtable的区别

9、ConcurrentHashMap为什么是线程安全的?/ConcurrentHashMap线程安全的具体实现方式/底层具体实现/有没有了解过线程安全的HashMap?

10、Hashtable也是线程安全的,为什么不推荐使用Hashtable呢?

11、HashMap底层为什么要用红黑树呢?为什么不用平衡二叉树?

12、HashMap是线程安全的吗?为什么呢?

13、HashMap线程不安全会出现什么问题?

14、HashMap在遇到key冲突的时候是怎么处理的呢?

15、HashMap的遍历方式

16、HashMap什么时候退回回链表?

17、集合框架中的线程安全类你知道哪些?

18、Collection和Collections的区别是什么?


1、三大集合的区别是什么?/介绍一下集合

  • Collection

    • List:存储的元素是有序的、可重复的。

      • ArrayList

      • LinkedList

      • Vector

    • Set:存储的元素是无序的、不可重复的。

      • HashSet

        • LinkedHashSet

      • TreeSet

  • Map:使用键值对(key-value)存储,key是无序的、不可重复的,value是无序的、可重复的,每个键最多映射到一个值。

    • HashMap

    • TreeMap

    • Hashtable

2、ArrayList和LinkedList的区别是什么?

  • ArrayList和LinkedList都是线程不安全的

  • Arraylist底层使用的是Object数组,支持随机访问;LinkedList底层使用的是双向链表数据结构,不支持随机访问

  • 使用下标访问一个元素,ArrayList的时间复杂度是O(1),而LinkedList是O(n)。

3、ArrayList和LinkedList使用场景

  • 如果应用程序对数据有较多的随机访问,ArrayList对象要优于LinkedList对象。

  • 如果应用程序有更多的插入或者删除操作,较少的随机访问,LinkedList对象要优于ArrayList对象。

  • 不过如果在List靠近末尾的地方插入,那么ArrayList只需要移动较少的数据,而LinkedList则需要一直查找到列表尾部,反而耗费较多时间,这时ArrayList就比LinkedList要快。

4、ArrayList如何去重?

  • 利用HashSet唯一性的特点对ArrayList进行去重

  • 使用HashSet去重后会影响元素原有的位置,可以替换为LinkedHashSet保持元素原来的顺序

HashSet objects = new HashSet<>(arr1);
//或
LinkedHashSet hashSet = new LinkedHashSet<>(arr1);
  • 使用java8新特性stream进行List去重

  • 使用steam的distinct()方法返回一个由不同数据组成的流,通过对象的equals()方法进行比较。

List listWithoutDuplicates = numbersList.stream().distinct().collect(Collectors.toList());
  • 利用List的contains方法循环遍历,重新排序,只添加一次数据,避免重复

for (String str : list) {
    if (!result.contains(str)) {
        result.add(str);
    }
}
  • 双重for循环去重

for (int i = 0; i < list.size(); i++) { 
    for (int j = 0; j < list.size(); j++) { 
        if(i!=j&&list.get(i)==list.get(j)) { 
            list.remove(list.get(j)); 
        } 
    } 
}

5、HashMap的底层原理实现

HashMap在jdk7中实现原理:

  • 在实例化以后,底层创建了长度是16的一维数组Entry[]table。

  • 首先,调用key所在类的hashCode()计算key的哈希值,此哈希值经过某种算法计算以后,得到在Entry数组中的存放位置。

  • 如果这个位置上的数据为空,此时的key-value添加成功。----情况1

  • 如果这个位置上的数据不为空,意味着这个位置上以链表的形式存在一个或多个数据,比较key和已经存在的一个或多个数据的哈希值:

    • 如果key的哈希值与已经存在的数据的哈希值都不相同,这时key-value添加成功。----情况2

    • 如果key的哈希值和已经存在的某一个数据(key2-value2)的哈希值相同,调用key所在类的equals(key2)方法比较:

      • 如果equals()返回false:此时key-value添加成功。----情况3

      • 如果equals()返回true:使用value替换value2。

  • 补充:关于情况2和情况3:此时key-value和原来的数据以链表的方式存储。

  • 在不断的添加过程中,会涉及到扩容问题,当超出临界值(且要存放的位置非空)时,扩容。默认的扩容方式:扩容为原来容量的2倍,并将原来的数据复制过来。

HashMap在jdk8中相较于jdk7在底层实现方面的不同:

  • newHashMap():底层没有创建一个长度为16的数组,而在首次调用put()方法时,底层创建长度为16的数组

  • jdk8底层的数组是:Node[],而不是Entry[]

  • jdk7底层结构是:数组+链表。jdk8中底层结构:数组+链表+红黑树。

  • 形成链表时,jdk7是新的元素指向旧的元素。jdk8是旧的元素指向新的元素(七上八下)

  • 当数组的某一个索引位置上的元素以链表形式存在的数据个数>8且当前数组的长度>64时,此时此索引位置上的所数据改为使用红黑树存储。

6、HashMap和LinkedHashMap的区别

  • HashMap

    • HashMap是线程不安全的,效率较高,可以存储null的key和value,但null作为键只能有一个,null作为值可以有多个。

  • LinkedHashMap

    • 因为在原有的HashMap底层结构基础上,添加了一对指针,指向前一个和后一个元素,所以可以保证在遍历map元素时,可以照添加的顺序实现遍历。

    • 对于频繁的遍历操作,此类执行效率高于HashMap。

7、HashMap和Hashtable的区别

  • 线程是否安全:

    • HashMap是非线程安全的,Hashtable是线程安全的,因为Hashtable内部的方法基本都经过synchronized修饰。

  • 效率:

    • 因为线程安全的问题,HashMap要比Hashtable效率高一点。

  • 对Nullkey和Nullvalue的支持:

    • HashMap可以存储null的key和value,但null作为键只能有一个,null作为值可以有多个;Hashtable不允许有null键和null值,否则会抛出NullPointerException。

  • 初始容量大小和每次扩充容量大小的不同:

    • ①创建时如果不指定容量初始值,Hashtable默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。

    • ②创建时如果给定了容量初始值,那么Hashtable会直接使用给定的大小,而HashMap会将其扩充为2的幂次方大小。

  • 底层数据结构:JDK1.8以后的HashMap,当链表长度大于阈值(默认为8)时,会将链表转化为红黑树,以减少搜索时间,将链表转换成红黑树前会判断,如果当前数组的长度小于64,那么会选择先进行数组扩容,而不是转换为红黑树。Hashtable没有这样的机制。

8、ConcurrentHashMap和Hashtable的区别

ConcurrentHashMap和Hashtable的区别主要体现在实现线程安全的方式上不同。

  • 底层数据结构:JDK1.7的ConcurrentHashMap底层采用分段的数组+链表实现,JDK1.8采用的数据结构是数组+链表/红黑二叉树。Hashtable底层数据结构是采用数组+链表的形式。

  • 实现线程安全的方式(重要):

    • ①在JDK1.7的时候,ConcurrentHashMap(分段锁)对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。JDK1.8的时候摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用synchronized和CAS来操作。

    • ②Hashtable(同一把锁):使用synchronized来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用put添加元素,另一个线程不能使用put添加元素,也不能使用get,竞争会越来越激烈效率越低。

9、ConcurrentHashMap为什么是线程安全的?/ConcurrentHashMap线程安全的具体实现方式/底层具体实现/有没有了解过线程安全的HashMap?

  • JDK1.7中,ConcurrentHashMap将数据分为一段一段的存储,然后给每段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。

  • ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。

  • Segment继承了ReentrantLock,所以Segment是一种可重入锁,扮演锁的角色。HashEntry用于存储键值对数据。

  • ConcurrentHashMap里包含Segment数组,Segment包含HashEntry数组,当对HashEntry数组的数据进行修改时,必须首先获得对应的Segment的锁。

  • JDK1.8中,ConcurrentHashMap取消了Segment分段锁,采用CAS和synchronized来保证并发安全。数据结构为数组+链表/红黑二叉树。Java8在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为O(N))转换为红黑树(寻址时间复杂度为O(log(N)))

  • synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。

10、Hashtable也是线程安全的,为什么不推荐使用Hashtable呢?

  • Hashtable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下Hashtable的效率非常低下。因为多个线程访问Hashtable的同步方法时,可能会进入阻塞或轮询状态。如线程1使用put进行添加元素,线程2不但不能使用put方法添加元素,并且也不能使用get方法来获取元素,所以竞争越激烈效率越低。

11、HashMap底层为什么要用红黑树呢?为什么不用平衡二叉树?

  • 平衡二叉树是更加严格的平衡,因此可以提供更快的查找速度,一般读取查找密集型任务,适用平衡二叉树。

  • 红黑树更适合于插入修改密集型任务。

  • 通常,平衡二叉树的旋转比红黑树的旋转更加难以平衡和调试。

12、HashMap是线程安全的吗?为什么呢?

  • 不是线程安全的。

  • 如果有两个线程A和B,都进行插入操作,刚好这两条不同的数据经过哈希计算后得到的hashCode是一样的,且该位置还没有其他的数据,所以这两个线程都会进入代码中。假设一种情况,线程A通过if判断,该位置没有哈希冲突,进入了if语句,还没有进行数据插入,这时候CPU就把资源让给了线程B,线程A停在了if语句里面,线程B判断该位置没有哈希冲突,也进入了if语句,线程B执行完后,轮到线程A执行,线程A直接在该位置插入而不用再判断。这时候,线程A就会把线程B插入的数据给覆盖,发生了线程不安全情况。

13、HashMap线程不安全会出现什么问题?

  • 运行结果出错

  • 死锁

14、HashMap在遇到key冲突的时候是怎么处理的呢?

  • HashMap发生哈希碰撞时,采用的是拉链法(链表法)来解决问题。

  • 当哈希碰撞发生时,HashMap会在出现冲突的位置上拉出一条链表,用链表来存储出现冲突的数据。

  • 当有新元素准备插入到链表的时候,JDK8中采用的是尾插法,在JDK7中采用的是头插法。但是头插法有个问题,就是在两个线程执行resize()扩容的时候,很可能会造成环形链表,导致get()时出现死循环。

  • 在HashMap中,链表法并不是处理Hash碰撞的唯一方法,另一种策略是结合红黑树。当链表长度达到8之后,如果再有新元素添加进来,就会将链表转换为红黑树。

15、HashMap的遍历方式

  • 迭代器(Iterator)方式遍历

  • ForEach方式遍历

  • Lambda表达式遍历(JDK1.8+)

  • StreamsAPI遍历(JDK1.8+)

具体的遍历方式又可以分为以下7种:

  1. 使用迭代器(Iterator)EntrySet的方式进行遍历

    Iterator>iterator=map.entrySet().iterator();
    while(iterator.hasNext()){
    Map.Entryentry=iterator.next();
    System.out.println(entry.getKey());
    System.out.println(entry.getValue());
    }

  2. 使用迭代器(Iterator)KeySet的方式进行遍历

    Iteratoriterator=map.keySet().iterator();
    while(iterator.hasNext()){
    Integerkey=iterator.next();
    System.out.println(key);
    System.out.println(map.get(key));
    }

  3. 使用ForEachEntrySet的方式进行遍历

    for(Map.Entryentry:map.entrySet()){
    System.out.println(entry.getKey());
    System.out.println(entry.getValue());
    }

  4. 使用ForEachKeySet的方式进行遍历

    for(Integerkey:map.keySet()){
    System.out.println(key);
    System.out.println(map.get(key));
    }

  5. 使用Lambda表达式的方式进行遍历

    map.forEach((key,value)->{
    System.out.println(key);
    System.out.println(value);
    });

  6. 使用StreamsAPI单线程的方式进行遍历

    map.entrySet().stream().forEach((entry)->{
    System.out.println(entry.getKey());
    System.out.println(entry.getValue());
    });

  7. 使用StreamsAPI多线程的方式进行遍历。

    map.entrySet().parallelStream().forEach((entry)->{
    System.out.println(entry.getKey());
    System.out.println(entry.getValue());
    });

16、HashMap什么时候退回回链表?

  • 扩容 resize( ) 时,红黑树拆分成的树的结点数小于等于临界值6个,则退化成链表。

  • 移除元素 remove( ) 时,removeTreeNode( ) 方法会检查红黑树是否满足退化条件,与结点数无关。如果红黑树根 root 为空,或者 root 的左子树/右子树为空,root.left.left 根的左子树的左子树为空,都会发生红黑树退化成链表。

17、集合框架中的线程安全类你知道哪些?

  • vector:就比arraylist多了个同步化机制(线程安全),因为效率较低,现在已经不太建议使用。在web应用中,特别是前台页面,往往效率(页面响应速度)是优先考虑的。

  • statck:堆栈类,先进后出

  • hashtable:就比hashmap多了个线程安全

  • enumeration:枚举,相当于迭代器

18、Collection和Collections的区别是什么?

  • Collection是一个集合接口。它提供了对集合对象进行基本操作的通用接口方法。实现该接口的类主要有List和Set,该接口的设计目标是为了各种具体的集合提供最大化的统一的操作方式。

  • Collections是针对集合类的一个包裹类,它提供了一系列静态方法实现对各种集合的搜索、排序以及线程安全化等操作,其中的大多数方法都是用于处理线性表。Collections类不能实例化,如同一个工具类,服务于Collection框架。如果在使用Collections类的方法时,对应的Collection对象null,则这些方法都会抛出NullPointerException。

你可能感兴趣的:(java,面试,java-ee)