面试——Java容器篇

面试🤺🤺🤺<持续更新>

  • 🗡️JAVA
    • ⚔️ Java基础篇
    • ⚔️ I/O、泛型、反射、异常篇
    • ⚔️ Java容器篇
    • ⚔️ JUC
    • ⚔️ JVM
    • ⚔️ 新特性
    • ⚔️ 补充点


前言

通过本文,你将了解如下内容:

  • 🕐 Collection
  • 🕑 List、Set、Queue
  • 🕒 Map
  • 穿插在文章内的补充知识点(希望也能看看,说不定有不一样的收获❤️❤️❤️)`

提示:以下是本篇文章正文内容

一、🕐 Collection

List

📦 ArrayList与LinkedList区别?

  • 线程安全:都不是同步的,都是线程不安全的
  • 数据结构:ArrayList底层是Object数组;LinkedList底层是双向链表(JDK 1.7以后,JDK 1.6以前1是双向循环链表)
  • 查询、插入、删除、增加性能
    – 查询:数组结构的查询速度快于链表,在随机访问上ArrayList支持,LinkedList不支持
    – 插入、删除、增加:ArrayList在末尾操作的时间复杂度为O(1),在中间操作是O(n - i),i为操作位置,数组向前(后)移动n-i个元素;LinkedList在两端操作的时间复杂度为O(1),在中间操作是O(n)
  • 内存占用:ArrayList 的空 间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)

📦 数组为什么比链表查询效率高?

图片来源【https://blog.csdn.net/jizhu4873/article/details/84341884】

通过上图可以看到CPU缓存的执行速度是远快于内存和磁盘的。
在CPU需要数据的时候,遵循一级缓存〉二级缓存〉内存的顺序,从而尽量提高读取速度。

  • CPU缓存会把一片连续的内存空间读入, 因为数组结构是连续的内存地址,所以数组全部或者部分元素被连续存在CPU缓存里面;链表的节点是分散在堆空间里面的,地址跨度可能很大,这时候CPU只能是去读取内存
  • 根据上图访问缓存和内存的时间比,可以大概知道,数组的查询是快于链表的

📦 双端链表和双向链表与双向循环链表

  • 双端链表与单链表十分相似,不同的是它新增一个对尾结点的引用。双端链表不是双向链表
  • 每个结点除了保存了对下一个结点的引用,同时还保存着对前一个结点的引用
  • 在双向链表基础上, 最后一个节点的 next 指向 head,而 head 的 prev 指向最后一个节点,构成一个环

📦 RandomAccess接口作用?

RandomAccess接口中什么都没有定义。 RandomAccess 接口用于标识实现这个接口的类具有随机访问功能。
Collections类里面二分查找方法中根据对象是否实现了RandomAccess而采用不同的方法

public static <T> int binarySearch(List<? extends Comparable<? super T>> list, T key) {
    if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
        return Collections.indexedBinarySearch(list, key);
    else
        return Collections.iteratorBinarySearch(list, key);
}

📦 ArrayList扩容机制


Set

📦 无序性和不可重复性的含义是什么?

1、什么是无序性?无序性不等于随机性 ,无序性是指存储的数据在底层数组中并非按照数组索引的顺序添加 ,而是根据数据的哈希值决定的
2、什么是不可重复性?不可重复性是指添加的元素按照 equals()判断时 ,返回 false,需要同时重写 equals()方法和 HashCode()方法

📦 比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同?

相同点:

  • 顶层接口:都实现了Set接口
  • 唯一性:都保证数据唯一
  • 线程安全:都不是线程安全的

不同点:

  • 数据结构:HashSet底层结构是hash表(基于HashMap);LinkedHashSet底层结构是链表+hash表;TreeSet底层结构是红黑树
  • 应用场景:HashSet 用于不需要保证元素插入和取出顺序的场景;LinkedHashSet 用于保证元素的插入和取出顺序满足 先进先出(FIFO) 的场景;TreeSet 用于支持对元素自定义排序规则的场景

📦 HashSet 如何检查重复?

HashSet先计算对象的HashCode,如果相同,会调用equals比较对象内容,如果相同,则不会


Queue

📦 Queue 与 Deque 的区别?

Queue :

  • Queue 是单端队列,只能从一端插入元素,另一端删除元素,实现上一般遵循 先进先出(FIFO) 规则。
  • Queue 扩展了 Collection 的接口,根据 因为容量问题而导致操作失败后处理方式的不同 可以分为两类方法: 一种在操作失败后会抛出异常,另一种则会返回特殊值

Deque :

  • Deque 是双端队列,在队列的两端均可以插入或删除元素。
  • Deque 扩展了 Queue 的接口, 增加了在队首和队尾进行插入和删除的方法

📦 ArrayDeque 与 LinkedList 的区别?

ArrayDeque 和 LinkedList 都实现了 Deque 接口,两者都具有队列的功能,两者主要区别:

  • 数据结构:ArrayDeque 是基于可变长的数组和双指针来实现,而 LinkedList 则通过链表来实现
  • 插入数据性能:ArrayDeque 插入时可能存在扩容过程, 不过均摊后的插入操作依然为 O(1)。虽然 LinkedList 不需要扩容,但是每次插入数据时均需要申请新的堆空间,均摊性能相比更慢
  • NULL值:ArrayDeque 不能存入NULL,而LinkedList 可以存入NULL
  • JDK引入时间:ArrayDeque 是在 JDK1.6 才被引入的,而LinkedList 早在 JDK1.2 时就已经存在

📦 说一说 PriorityQueue

  • 优先级队列,默认是小根堆,底层数据结构是二叉堆,底层采用数组
  • 通过堆元素的上浮和下沉,实现了在 O(logn) 的时间复杂度内插入元素和删除堆顶元素
  • 不支持存储 NULL 和 non-comparable 的对象
  • 通过Comparator 作为构造参数,自定义元素优先级的先后

📦 说一说 二叉堆

二叉堆定义:堆是完全二叉树,底层数据结构是数组。父结点的键值总是大于等于(小于等于)任何一个子节点的键值。
大根堆:父结点的键值总是大于等于任何一个子节点的键值
小根堆:父结点的键值总是小于等于任何一个子节点的键值

每次增加数据都会保证父节点为最大值(最小值),下面以大根堆为例:

保证堆顶是最大值:

// 父节点:index >> 1
public void heapInsert(int[] arr, int index) {
    int indexNext;
    // (x + (1 << k) -1) >> k  ===>   [(index - 1) + (1 << 1) - 1] >> 1 = index >> 1 
    // 值大于父节点值,进入循环,交换值
    while (arr[index] > arr[(indexNext = ((index - 1) < 0 ? index : (index - 1))) >> 1]) {
    	// 交换位置
        sortUtil.arrayToSwap1(arr, index, indexNext >> 1);
        index = indexNext >> 1;
    }
}

二、🕐 Map

HashMap、HashTable、TreeMap、ConcurrentHashMap

📦 HashMap put、get方法详解【https://blog.csdn.net/qq_44833552/article/details/124006289】

📦 HashMap与ConcurrentHashMap 区别?

  • ConcurrentHashMap对整个桶数组进行了分割分段(Segment),然后在每一个分段上都用lock锁进行保护,相对于HashTable的synchronized锁的粒度更精细了一些,并发性能更好,而HashMap没有锁机制,不是线程安全的。(JDK1.8之后ConcurrentHashMap启用了一种全新的方式实现,利用CAS算法。)
  • HashMap的键值对允许有null,但是ConCurrentHashMap都不允许

📦 HashMap与HashTable区别?

  • 线程安全:HashMap线程不安全,HashTable方法基本都经过synchronized 修饰,线程安全
  • 性能:因为synchronized的原因,HashMap性能比HashTable高一点
  • 数据结构:HashMap数据结构是数组+链表+红黑树(JDK 8后,JDK 7是数组+链表);HashTable数据结构是数组+链表
  • 对NULL key和NULL value支持:HashMap支持存在一个NULL key,多个NULL value;HashTable不支持,会抛出 NullPointerException
  • 初始容量大小和每次扩充容量大小:

📦 HashMap与TreeMap区别?

HashMapHashSet
实现了 Map 接口实现 Set 接口
存储键值对仅存储对象
调用 put()向 map 中添加元素调用 add()方法向 Set 中添加元素
HashMap 使用键(Key)计算 hashcodeHashSet 使用成员对象来计算 hashcode 值,对于两个对象来说 hashcode 可能相同,所以equals()方法用来判断对象的相等性

📦 HashMap与HashSet区别?

TreeMap 和HashMap 都继承自AbstractMap
TreeMap实现了NavigableMap接口,获得了对集合内元素的搜索的能力
TreeMap实现了SortedMap 接口,获得了根据对象属性自定义排序

📦 JAVA HASHMAP的死循环【https://coolshell.cn/articles/9606.html】

📦 HashMap 有哪几种常见的遍历方式?

  • 迭代器(Iterator)方式遍历:
    – 使用迭代器(Iterator)EntrySet 的方式进行遍历:
public static void main(String[] args) {
       Map<Integer, String> map = new HashMap<>(8);
       map.put(1, "d1");
       map.put(2, "d2");
       map.put(3, "d3");
       map.put(4, "d4");
       map.put(5, "d5");

       Iterator<Map.Entry<Integer, String>> iterator = map.entrySet().iterator();
       while (iterator.hasNext()) {
           Map.Entry<Integer, String> entry = iterator.next();
           System.out.println("key:" + entry.getKey() + ", value:" + entry.getValue());
       }

   }

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

public static void main(String[] args) {
    Map<Integer, String> map = new HashMap<>(8);
    map.put(1, "d1");
    map.put(2, "d2");
    map.put(3, "d3");
    map.put(4, "d4");
    map.put(5, "d5");

    Iterator<Integer> iterator = map.keySet().iterator();
    while (iterator.hasNext()) {
        Integer key = iterator.next();
        System.out.println("key:" + key + ", value:" + map.get(key));
    }

}
  • For Each 方式遍历:
    – 使用 For Each EntrySet 的方式进行遍历:
public static void main(String[] args) {
        Map<Integer, String> map = new HashMap<>(8);
        map.put(1, "d1");
        map.put(2, "d2");
        map.put(3, "d3");
        map.put(4, "d4");
        map.put(5, "d5");

        for (Map.Entry<Integer, String> entry : map.entrySet()) {
            System.out.println("key:" + entry.getKey() + ", value:" + entry.getValue());
        }

    }

– 使用 For Each KeySet 的方式进行遍历:

public static void main(String[] args) {
    Map<Integer, String> map = new HashMap<>(8);
    map.put(1, "d1");
    map.put(2, "d2");
    map.put(3, "d3");
    map.put(4, "d4");
    map.put(5, "d5");

    for (Integer key : map.keySet()) {
        System.out.println("key:" + key + ", value:" + map.get(key));
    }

}
  • Lambda 表达式遍历(JDK 1.8+):
public static void main(String[] args) {
    Map<Integer, String> map = new HashMap<>(8);
    map.put(1, "d1");
    map.put(2, "d2");
    map.put(3, "d3");
    map.put(4, "d4");
    map.put(5, "d5");

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

}
  • Streams API 遍历(JDK 1.8+):
    – 使用 Streams API 单线程的方式进行遍历:
public static void main(String[] args) {
   Map<Integer, String> map = new HashMap<>(8);
    map.put(1, "d1");
    map.put(2, "d2");
    map.put(3, "d3");
    map.put(4, "d4");
    map.put(5, "d5");

    map.entrySet().stream().forEach((entry) -> {
        System.out.println("key:" + entry.getKey() + ", value:" + entry.getValue());
    });

}

– 使用 Streams API 多线程的方式进行遍历:

 public static void main(String[] args) {
    Map<Integer, String> map = new HashMap<>(8);
    map.put(1, "d1");
    map.put(2, "d2");
    map.put(3, "d3");
    map.put(4, "d4");
    map.put(5, "d5");

    map.entrySet().parallelStream().forEach((entry) -> {
        System.out.println("key:" + entry.getKey() + ", value:" + entry.getValue());
    });

}

📦 ConcurrentHashMap与HashTable区别?

  • 底层数据结构: JDK1.7的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的HashMap 的底层数据结构类似都是采用 数组+链表 的形式
  • 实现线程安全的方式(重要): ① 在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),Segment 数组 + HashEntry 数组 + 链表,每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。(默认分配16个Segment,比Hashtable效率提高16倍。) 到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对synchronized锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;②Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低

    HashTable全表锁

    concurrenthashmap JDK 7

    concurrenthashmap JDK 8(TreeBin: 红黑二叉树节点 Node: 链表节点)

📦 JDK 1.7与JDK 1.8 HashMap区别?

📦 说一说红黑树【https://zhuanlan.zhihu.com/p/273829162】

📦 说一说“fail-fast”

而fail-fast 机制是Java集合(Collection)中的一种错误机制。当多个线程(单线程也可能)对同一个集合的内容进行操作时,就可能会产生fail-fast(快速失败)事件

  • 原因:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。
  • 解决办法:
  1. 在遍历过程中,所有涉及到改变modCount值得地方全部加上synchronized。
  2. 使用CopyOnWriteArrayList来替换ArrayList

总结

以上就是今天要记录的内容,本文仅仅对Java集合知识点进行初步总结。

资料来源:

- guide哥.【JavaGuide】. 面试指北. JAVA,.
- 陈皓.【酷 壳 – COOLSHELL】. 疫苗:JAVA HASHMAP的死循环. JAVA,.
- 敖丙.【知乎】. 图解:什么是红黑树?. JAVA,.

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

蓝桉未与

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值