0
点赞
收藏
分享

微信扫一扫

大厂必问的Java集合面试题

大家好,我是大彬~

今天给大家分享Java集合常考的面试题,准备找工作的小伙伴赶紧收藏起来~

常见的集合有哪些?

Java集合类主要由两个接口CollectionMap派生出来的,Collection有三个子接口:List、Set、Queue。
Java集合框架图如下:

大厂必问的Java集合面试题_链表大厂必问的Java集合面试题_Java_02

List代表了有序可重复集合,可直接根据元素的索引来访问;Set代表无序不可重复集合,只能根据元素本身来访问;Queue是队列集合。Map代表的是存储key-value对的集合,可根据元素的key来访问value。
集合体系中常用的实现类有ArrayList、LinkedList、HashSet、TreeSet、HashMap、TreeMap等实现类。

List 、Set和Map 的区别

  • List 以索引来存取元素,有序的,元素是允许重复的,可以插入多个null;
  • Set 不能存放重复元素,无序的,只允许一个null;
  • Map 保存键值对映射;
  • List 底层实现有数组、链表两种方式;Set、Map 容器有基于哈希存储和红黑树两种方式实现;
  • Set 基于 Map 实现,Set 里的元素值就是 Map的键值。

ArrayList 了解吗?

​ArrayList​​ 的底层是动态数组,它的容量能动态增长。在添加大量元素前,应用可以使用​​ensureCapacity​​操作增加 ​​ArrayList​​ 实例的容量。ArrayList 继承了 AbstractList ,并实现了 List 接口。

ArrayList 的扩容机制?

ArrayList扩容的本质就是计算出新的扩容数组的size后实例化,并将原有数组内容复制到新数组中去。默认情况下,新的容量会是原容量的1.5倍。以JDK1.8为例说明:

public boolean add(E e) {
//判断是否可以容纳e,若能,则直接添加在末尾;若不能,则进行扩容,然后再把e添加在末尾
ensureCapacityInternal(size + 1);
//将e添加到数组末尾
elementData[size++] = e;
return true;
}

// 每次在新增一个元素时,需要判断这个list的容量
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}

private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 容量不足则扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}


private void grow(int minCapacity) {
int oldCapacity = elementData.length;
// 扩容至原来的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
//检查容量是否充足
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;

if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);

elementData = Arrays.copyOf(elementData, newCapacity);
}

怎么在遍历 ArrayList 时移除一个元素?

foreach删除会导致快速失败问题,可以使用迭代器的 remove() 方法。

Iterator itr = list.iterator();
while(itr.hasNext()) {
if(itr.next().equals("jay") {
itr.remove();
}
}

Arraylist 和 Vector 的区别

  1. ArrayList在内存不够时默认是扩展50%
  2. Arraylist 与 LinkedList 区别
  3. HashMap

  4. HashMap 使用数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的, 链表长度大于8(TREEIFY_THRESHOLD)时,会把链表转换为红黑树,红黑树节点个数小于6(UNTREEIFY_THRESHOLD)时才转化为链表,防止频繁的转化。
  5. 扩容过程?

  6. 1.8扩容机制:当元素个数大于threshold时,会进行扩容,使用2倍容量的数组代替原有数组。采用尾插入的方式将原数组元素拷贝到新数组。1.8扩容之后链表元素相对位置没有变化,而1.7扩容之后链表元素会倒置。 原数组的元素在重新计算hash之后,因为数组容量n变为2倍,那么n-1的mask范围在高位多1bit。在元素拷贝过程不需要重新计算元素在数组中的位置,只需要看看原来的hash值新增的那个bit是1还是0,是0的话索引没变,是1的话索引变成“原索引+oldCap”(根据e.hash & (oldCap - 1) == 0判断) 。这样可以省去重新计算hash值的时间,而且由于新增的1bit是0还是1可以认为是随机的,因此resize的过程会均匀的把之前的冲突的节点分散到新的bucket
  7. 红黑树的特点?

  8. 为什么使用红黑树而不使用AVL树?

  9. ConcurrentHashMap 在put的时候会加锁,使用红黑树插入速度更快,可以减少等待锁释放的时间。红黑树是对AVL树的优化,只要求部分平衡,用非严格的平衡来换取增删节点时候旋转次数的降低,提高了插入和删除的性能。
  10. 在解决 hash 冲突的时候,为什么选择先用链表,再转红黑树?

  11. 因为红黑树需要进行左旋,右旋,变色这些操作来保持平衡,而单链表不需要。当元素小于 8 个的时候,链表结构可以保证查询性能。当元素大于 8 个的时候, 红黑树搜索时间复杂度是 O(logn),而链表是 O(n),此时需要红黑树来加快查询速度,但是插入和删除节点的效率变慢了。如果一开始就用红黑树结构,元素太少,插入和删除节点的效率又比较慢,浪费性能。
  12. HashMap 的长度为什么是 2 的幂次方?

  13. Hash 值的范围值比较大,使用之前需要先对数组的长度取模运算,得到的余数才是元素存放的位置也就是对应的数组下标。这个数组下标的计算方法是(n - 1) & hash。将HashMap的长度定为2 的幂次方,这样就可以使用(n - 1)&hash位运算代替%取余的操作,提高性能。
  14. HashMap为什么线程不安全?

  15. HashMap和HashTable的区别?

  16. HashMap和Hashtable都实现了Map接口。
  17. 讲一下TreeMap?

  18. TreeMap是一个能比较元素大小的Map集合,会对传入的key进行了大小排序。可以使用元素的自然顺序,也可以使用集合中自定义的比较器来进行排序。
  19. public class TreeMap<K,V>
    extends AbstractMap<K,V>
    implements NavigableMap<K,V>, Cloneable, java.io.Serializable {
    }
  20. TreeMap 的继承结构:
  21. 大厂必问的Java集合面试题_数组_03TreeMap的特点:
  22. HashSet底层原理?

  23. HashSet 基于 HashMap 实现。放入HashSet中的元素实际上由HashMap的key来保存,而HashMap的value则存储了一个静态的Object对象。
  24. public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable {
    static final long serialVersionUID = -5024744406713321676L;

    private transient HashMap<E,Object> map; //基于HashMap实现
    //...
    }

    HashSet、LinkedHashSet 和 TreeSet 的区别?

  25. HashSetSet 接口的主要实现类 ,HashSet 的底层是 HashMap,线程不安全的,可以存储 null 值;
  26. LinkedHashSetHashSet 的子类,能够按照添加的顺序遍历;
  27. TreeSet 底层使用红黑树,能够按照添加元素的顺序进行遍历,排序的方式可以自定义。
  28. 什么是fail fast?

  29. fast-fail是Java集合的一种错误机制。当多个线程对同一个集合进行操作时,就有可能会产生fast-fail事件。例如:当线程a正通过iterator遍历集合时,另一个线程b修改了集合的内容,此时modCount(记录集合操作过程的修改次数)会加1,不等于expectedModCount,那么线程a访问集合的时候,就会抛出ConcurrentModificationException,产生fast-fail事件。边遍历边修改集合也会产生fast-fail事件。 解决方法:
  30. 什么是fail safe?

  31. 采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。
  32. 原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception
  33. 缺点:基于拷贝内容的优点是避免了Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。
  34. 讲一下ArrayDeque?

  35. ArrayDeque实现了双端队列,内部使用循环数组实现,默认大小为16。它的特点有:
  36. ArrayDequeLinkedList都实现了Deque接口,如果只需要从两端进行操作,ArrayDeque效率更高一些。如果同时需要根据索引位置进行操作,或者经常需要在中间进行插入和删除(LinkedList有相应的 api,如add(int index, E e)),则应该选LinkedList
  37. ArrayDequeLinkedList都是线程不安全的,可以使用Collections工具类中synchronizedXxx()转换成线程同步。
  38. 哪些集合类是线程安全的?哪些不安全?

  39. 线性安全的集合类
  40. 线性不安全的集合类
  41. 迭代器 Iterator 是什么?

  42. Iterator 模式使用同样的逻辑来遍历集合。它可以把访问逻辑从不同类型的集合类中抽象出来,不需要了解集合内部实现便可以遍历集合元素,统一使用 Iterator 提供的接口去遍历。它的特点是更加安全,因为它可以保证,在当前遍历的集合元素被更改的时候,就会抛出 ConcurrentModificationException 异常。
  43. public interface Collection<E> extends Iterable<E> {
    Iterator<E> iterator();
    }
  44. 主要有三个方法:hasNext()、next()和remove()。
  45. Iterator 和 ListIterator 有什么区别?

  46. ListIterator 是 Iterator的增强版。
  47. ConcurrentHashMap

  48. 多线程环境下,使用Hashmap进行put操作会引起死循环,应该使用支持多线程的 ConcurrentHashMap。 JDK1.8 ConcurrentHashMap取消了segment分段锁,而采用CASsynchronized来保证并发安全。数据结构采用数组+链表/红黑二叉树。synchronized只锁定当前链表或红黑二叉树的首节点,相比1.7锁定HashEntry数组,锁粒度更小,支持更高的并发量。当链表长度过长时,Node会转换成TreeNode,提高查找速度。
  49. ConcurrentHashMap 和 Hashtable 的区别?

  50. CopyOnWrite

  51. 写时复制。当我们往容器添加元素时,不直接往容器添加,而是先将当前容器进行复制,复制出一个新的容器,然后往新的容器添加元素,添加完元素之后,再将原容器的引用指向新容器。这样做的好处就是可以对CopyOnWrite容器进行并发的读而不需要加锁,因为当前容器不会被修改。
  52. public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    //需要加锁
    lock.lock();
    try {
    Object[] elements = getArray();
    int len = elements.length;
    Object[] newElements = Arrays.copyOf(elements, len + 1);
    newElements[len] = e;
    setArray(newElements);
    return true;
    } finally {
    lock.unlock();
    }
    }
  53. 从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayList和CopyOnWriteArraySet。 CopyOnWriteArrayList中add方法添加的时候是需要加锁的,保证同步,避免了多线程写的时候复制出多个副本。读的时候不需要加锁,如果读的时候有其他线程正在向CopyOnWriteArrayList添加数据,还是可以读到旧的数据。
  54. 缺点:
  • 内存占用问题。由于CopyOnWrite的写时复制机制,在进行写操作的时候,内存里会同时驻扎两个对象的内存。
  • CopyOnWrite容器不能保证数据的实时一致性,可能读取到旧数据。

举报

相关推荐

0 条评论