java集合基础及相关面试题整理

目录

1.添加一组元素

Collection 和 Collections的区别

2 迭代器

2.1 Iterator

2.2 ListIterator

2.3 Foreach与迭代器

2.4 快速失败(fail-fast)和安全失败(fail-safe)的区别

3 List

3.1 ArrayList

3.2 LinkedList

4 Stack

5 Queue

5.1 PriorityQueue

6 Set

7 Map

7.1 HashMap

7.2 HashMap和Hashtable的区别

7.3 ConcurrentHashMap

7.3 TreeMap

7.4 LinkedHashMap

8 补充

8.1 List、Map、Set三个接口存取元素时各自的特点

8.2  集合类没有实现Cloneable和Serializable接口


1.添加一组元素

Collections.addAll():接受一个Collection对象,以及一个数组或是一个用逗号分割的列表,将元素添加到Collection中。

Collection.addAll():只能接受另一个Collection对象作为参数。

Arrays.asList():接受一个数组或是一个用逗号分隔的元素列表(使用可变参数),并将其转换为一个List对象。注意,该list对象底层是数组,因此不能调整尺寸。

Collection 和 Collections的区别

  • Collection是集合类的上级接口,继承于它的接口主要有Set 和List.
  • Collections是针对集合类的一个工具类,提供一系列静态方法实现对各种集合的搜索、排序、线程安全化等操作。

2 迭代器

2.1 Iterator

Iterator提供了统一遍历集合元素操作的统一接口,  Java的Iterator只能单向移动,可用来遍历Set和List集合:使用方法iterator()要求容器返回一个Iterator,Iterator将准备好返回序列的第一个元素;使用next()获得序列中的下一个元素;使用hasNext()检查序列中是否还有元素;使用remove()将迭代器新返回的元素删除(调用remove()方法之前必须先调用next()方法)。

注:在迭代元素的时候不能通过集合的方法删除元素, 否则会抛出ConcurrentModificationException异常. 但是可以通过Iterator接口中的remove()方法进行删除.

2.2 ListIterator

ListIterator可以双向移动,只用来遍历List集合:通过调用listIterator()方法产生一个指向List开始处的ListIterator,并且还可以通过调用ListIterator(n)方法创建一个一开始就指向列表索引为n的元素处的ListIterator;可以使用set()方法替换它访问过的最后一个元素。

2.3 Foreach与迭代器

foreach语法主要用于数组,也可以用于任何Collection对象。任何实现了Iterable的类,都可以用于foreach语句中。但是数组本身并不是一个Iterable。

2.4 快速失败(fail-fast)和安全失败(fail-safe)的区别

  • 快速失败:在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出ConcurrentModificationException。
  • 安全失败:在遍历时先复制原有集合内容,在拷贝的集合上进行遍历。由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception。
  • java.util包下面的所有的集合类都是快速失败的,而java.util.concurrent包下面的所有的类都是安全失败的。

3 List

  • 当确定一个元素是否属于某个List,发现某个元素的索引,以及从某个List中移除一个元素等,都会用到equals()方法。
  • containsAll():与里面元素的顺序无关。

3.1 ArrayList

用于随机访问元素,但是在List的中间插入和移除元素时较慢。

ArrayList自动扩容机制:ArrayList的默认初始容量为10,也可以通过ArrayList(int initialCapacity)构造一个具有指定初始容量的空列表,随着动态的向其中添加元素,其容量可能会动态的增加,每次扩充至原有基础的1.5倍。例如初始化容量为20,总共有50个元素,扩容次数依次为:20->30->45->67。另外,ArrayList并发add()可能出现数组下标越界异常。这是因为ArrayList在扩容的过程中,内部的一致性被破坏,但由于没有锁的保护,另外一个线程访问到了这个不一致的内部状态,导致出现越界问题。

ArrayList和Vector的区别:都使用数组方式存储数据,索引数据快但增删慢,Vector中的方法由于添加了synchronized修饰,因此Vector是线程安全的容器,但性能上较ArrayList差,因此已经是Java中的遗留容器。

3.2 LinkedList

使用双向链表实现存储,通过代价较低的在List中间进行的插入和删除操作,提供了优化的顺序访问。LinkedList还添加了可以使其用作栈、队列或双端队列的方法。

  • 返回列表的头:getFirst()、element()、peek().如果列表为空,peek()返回null,其它抛出异常;
  • 移除并返回列表的头:removeFirst()、remove()、poll().如果列表为空,poll()返回null,其它抛出异常;
  • 插入列表尾部:add()、offer()、addLast().

注:1.ArrayList和LinkedList都是非线程安全的,如果遇到多个线程操作同一个容器的场景,则可通过工具类Collections中的synchronizedList方法将其转换成线程安全的容器后再使用

2. 如果一直在list的尾部添加元素,当数据量小的时候,ArrayList需要扩容,所以LinkedList的效率就会比较高;当数据量很大的时候,new对象的时间大于扩容的时间,那么ArrayList的效率比LinkedList高。

4 Stack

可以直接将LinkedList作为栈使用,程序中应避免使用java.util.Stack(已过时)。

   public class Stack<T>{
       private LinkedList<T> list = new LinkedList<>();
       public void push(T v){
           list.addFirst(v);
       }
       public T peek(){
           return list.getFirst();
       }
       public T pop(){
           return list.removeFirst();
       }
       public boolean empty(){
           return list.isEmpty();
       }
       public String toString(){
           return list.toString();
       }
   }

5 Queue

LinkedList提供了方法以支持队列的行为,并且它实现了Queue接口。peek()和element()都将在不移除的情况下返回队头,但是peek()方法在队列为空时返回null,而element()会抛出NoSuchElementException异常。poll()和remove()方法将移除并返回队头,但是poll()在队列为空时返回null,而remove()会抛出NoSuchElementException异常。

5.1 PriorityQueue

PriorityQueue调用offer()方法来插入一个对象时,这个对象会在队列中被排序,默认为自然排序,我们可以通过提供自己的Comparator来修改这个顺序。PriorityQueue可确保调用peek()、poll()和remove()方法时获取的元素是队列中优先级最高的元素。

6 Set

  • HashSet:底层使用散列函数
  • TreeSet:底层使用红-黑树数据结构
  • LinkedHashSet:使用散列以及链表来维护数据的插入顺序。

7 Map

7.1 HashMap

HashMap用来快速访问,是基于数组+链表+红黑树实现的,它的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就会进行扩容。

transient Node<K,V>[] table;

 static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
}

HashMap初始容量为2^n及每次扩容为原来的2倍

  • 通过(n - 1) & hash来计算索引位置,位运算&速度高于取模运算%
  • 散列更均匀,减少碰撞:HashMap的容量固定为2的n次幂,(n-1)的2进制也就是1111111***111这样形式的,这样与添加元素的hash值进行位运算时,能够充分的散列,使得添加的元素均匀分布在HashMap的每个位置上,减少hash碰撞

hash方法原理

  • 先求key的哈希值,取key的hashcode值h与h的高8位做与运算(混合原始哈希码的高位和低位,以此来加大低位的随机性)
  • 取模:(n - 1) & hash
static final int hash(Object key) {   //jdk1.8
     int h;
     // h = key.hashCode() 第一步 取hashCode值
     // h ^ (h >>> 16)  第二步 高位参与运算
     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

put方法:首次put元素需要进行扩容为默认容量16,每次put,先根据key的hash值得到插入的数组索引i,如果索引i中有值,看key是否存在(equals()方法),如果存在就直接覆盖,否则插入链表或者红黑树中。当链表元素个数大于等于8时,链表转换成树结构;若桶中链表元素个数小于等于6时,树结构还原成链表。因为红黑树的平均查找长度是log(n),长度为8的时候,平均查找长度为3,如果继续使用链表,平均查找长度为8/2=4,这才有转换为树的必要。链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。还有选择6和8,中间有个差值7可以有效防止链表和树频繁转换

注:使用HashMap,如果key是自定义的类,就必须重写hashcode()和equals()。

扩容机制:首次put元素需要进行扩容为默认容量16,之后达到阈值就扩容为原来的两倍,接下来就是进行扩容后table的调整:假设扩容前的table大小为2的N次方,而元素的table索引为key的hash值的后N位确定,那么扩容后元素的table索引为其hash值的后N+1位确定,比原来多了一位,因此,若元素hash值第N+1位为0,则不需要进行位置调整,反之如果为1则调整至原索引的两倍位置。扩容或初始化完成后,resize方法返回新的table。注意:JDK1.8之后是先插入后扩容

7.2 HashMap和Hashtable的区别

  • 父类不同: HashMap是继承自AbstractMap类,而HashTable是继承自Dictionary(已被废弃)。 
  • null值问题:Hashtable不允许键或者值是null;HashMap允许键和值是null,只能有一个键为null。当get()方法返回null值时,可能是 HashMap中没有该键,也可能使该键所对应的值为null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个键, 而应该用containsKey()方法来判断。
  • 线程安全性:Hashtable是线程安全的,而HashMap不是线程安全的。多线程操作时可使用线程安全的ConcurrentHashMap。因ConcurrentHashMap使用了分段锁,并不对整个数据进行锁定,ConcurrentHashMap效率比Hashtable要高好多倍。
  • 初始容量:Hashtable的初始长度是11,之后每次扩充容量变为之前的2n+1(n为上一次的长度);而HashMap的初始长度为16,之后每次扩充变为原来的两倍。
  • 计算哈希值的方法:Hashtable直接使用对象的hashCode,使用除留余数法来获得最终的位置,效率很低;HashMap将哈希表的大小固定为了2的幂,这样在取模预算时,不需要做除法,只需要做位运算。

7.3 ConcurrentHashMap

ConcurrentHashMap 类中包含两个静态内部类 HashEntry 和 Segment,一个 ConcurrentHashMap 实例中包含由若干个 Segment 对象组成的数组

  • HashEntry 用来封装映射表的键 / 值对;在 HashEntry 类中,key,hash 和 next 域都被声明为final型,value 域被声明为volatile 型。
  • Segment用来充当锁的角色,每个Segment对象守护整个散列映射表的若干个桶。每个桶是由若干个HashEntry对象链接起来的链表。
static final class HashEntry<K,V> {
       final K key;                       // 声明 key 为 final 型
       final int hash;                   // 声明 hash 值为 final 型
       volatile V value;                 // 声明 value 为 volatile 型
       final HashEntry<K,V> next;      // 声明 next 为 final 型
  
       HashEntry(K key, int hash, HashEntry<K,V> next, V value) {
           this.key = key;
           this.hash = hash;
           this.next = next;
           this.value = value;
       }
}

在ConcurrentHashMap 中,在散列时如果产生“碰撞”,将采用“分离链接法”来处理“碰撞”:把“碰撞”的 HashEntry 对象链接成一个链表。由于 HashEntry 的 next 域为 final 型,所以新节点只能在链表的表头处插入。 下图是在一个空桶中依次插入 A,B,C 三个 HashEntry 对象后的结构图:

图1. 插入三个节点后桶的结构示意图:

注意:由于只能在表头插入,所以链表中节点的顺序和插入的顺序相反。

Segment 类继承于 ReentrantLock 类,从而使得 Segment 对象能充当锁的角色。每个 Segment 对象用来守护其(成员对象 table 中)包含的若干个桶。

7.3 TreeMap

TreeMap是一个有序的key-value集合,基于红黑树实现。红黑树的插入、删除、遍历时间复杂度都为O(lgN)。该映射根据其键的自然顺序进行排序,或根据创建映射时提供的 Comparator进行排序,具体取决于使用的构造方法。TreeMap的特性(黑根黑叶路同黑,红黑二色红生黑

  • 根节点是黑色 
  • 每个节点都只能是红色或者黑色
  • 每个叶节点(NULL节点)是黑色的。 
  • 如果一个节点是红色的,则它两个子节点都是黑色的,也就是说在一条路径上不能出现两个红色的节点。
  • 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

7.4 LinkedHashMap

保持元素出入的顺序,同时通过散列提供了快速访问能力。

8 补充

8.1 List、Map、Set三个接口存取元素时各自的特点

  • List以特定索引来存取元素,可以有重复元素。
  • Set不能存放重复元素(用对象的equals()方法来区分元素是否重复)。
  • Map保存键值对(key-value pair)映射,映射关系可以是一对一或多对一。
  • Set和Map容器都有基于哈希存储和排序树的两种实现版本,基于哈希存储的版本理论存取时间复杂度为O(1),而基于排序树版本的实现在插入或删除元素时会按照元素或元素的键(key)构成排序树从而达到排序和去重的效果。

8.2  集合类没有实现Cloneable和Serializable接口

克隆(cloning)或者是序列化(serialization)的语义和含义是跟具体的实现相关的。因此,应该由集合类的具体实现来决定如何被克隆或者是序列化。实现Serializable序列化的作用:将对象的状态保存在存储媒体中以便可以在以后重写创建出完全相同的副本;按值将对象从一个从一个应用程序域发向另一个应用程序域。 实现 Serializable接口的作用就是可以把对象存到字节流,然后可以恢复。所以你想如果你的对象没有序列化,怎么才能进行网络传输呢?要网络传输就得转为字节流,所以在分布式应用中,你就得实现序列化。如果你不需要分布式应用,那就没必要实现实现序列化。

猜你喜欢

转载自blog.csdn.net/Nibaby9/article/details/104275121