面霸篇:Java 集合容器大满贯(卷二)(一)

本文涉及的产品
容器服务 Serverless 版 ACK Serverless,317元额度 多规格
容器服务 Serverless 版 ACK Serverless,952元额度 多规格
容器镜像服务 ACR,镜像仓库100个 不限时长
简介: 面霸篇,从面试角度作为切入点提升大家的 Java 内功,所谓根基不牢,地动山摇。码哥在 《Redis 系列》的开篇 Redis 为什么这么快中说过:学习一个技术,通常只接触了零散的技术点,没有在脑海里建立一个完整的知识框架和架构体系,没有系统观。这样会很吃力,而且会出现一看好像自己会,过后就忘记,一脸懵逼。我们需要一个系统观,清晰完整的去学习技术,在「面霸篇:Java 核心基础大满贯(卷一)」中,码哥梳理了 Java 高频核心知识点。本篇将一举攻破 Java 集合容器知识点,跟着「码哥」一起来提纲挈领,梳理一个完整的 Java 容器开发技术能力图谱,将基础夯实。

集合容器概述


什么是集合?


顾名思义,集合就是用于存储数据的容器


集合框架是为表示和操作集合而规定的一种统一的标准的体系结构。 任何集合框架都包含三大块内容:对外的接口、接口的实现和对集合运算的算法


码老湿,可以说下集合框架的三大块内容具体指的是什么吗?


接口


面向接口编程,抽象出集合类型,使得我们可以在操作集合的时候不必关心具体实现,达到「多态」。


就好比密码箱,我们只关心能打开箱子,存放东西,并且关闭箱子,至于怎么加密咱们不关心。


接口实现


每种集合的具体实现,是重用性很高的数据结构。


算法


集合提供了数据存放以及查找、排序等功能,集合有很多种,也就是算法通常也是多态的,因为相同的方法可以在同一个接口被多个类实现时有不同的表现


事实上,算法是可复用的函数。 它减少了程序设计的辛劳。


集合框架通过提供有用的数据结构和算法使你能集中注意力于你的程序的重要部分上,而不是为了让程序能正常运转而将注意力于低层设计上。


集合的特点


  • 对象封装数据,多个对象需要用集合存储;


  • 对象的个数可以确定使用数组更高效,不确定个数的情况下可以使用集合,因为集合是可变长度。


集合与数组的区别


  • 数组是固定长度的;集合可变长度的。


  • 数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用数据类型。


  • 数组存储的元素必须是同一个数据类型;集合存储的对象可以是不同数据类型。


由于有多种集合容器,因为每一个容器的自身特点不同,其实原理在于每个容器的内部数据结构不同。


集合容器在不断向上抽取过程中,出现了集合体系。在使用一个体系的原则:参阅顶层内容。建立底层对象。


集合框架有哪些优势


  • 容量自动增长扩容;


  • 提供高性能的数据结构和算法;


  • 可以方便地扩展或改写集合,提高代码复用性和可操作性。


  • 通过使用JDK自带的集合类,可以降低代码维护和学习新API成本。


有哪些常用的集合类


Java 容器分为 Collection 和 Map 两大类,Collection集合的子接口有Set、List、Queue三种子接口。


我们比较常用的是Set、List,Map接口不是 collection的子接口。


Collection集合主要有List和Set两大接口


  • List:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。


  • Set:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。


  • Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。


Map是一个键值对集合,存储键、值和之间的映射。 Key无序,唯一;value 不要求有序,允许重复。


Map没有继承于 Collection 接口,从 Map 集合中检索元素时,只要给出键对象,就会返回对应的值对象。


Map 的常用实现类:HashMap、TreeMap、HashTable、LinkedHashMap、ConcurrentHashMap


image.png


集合的底层数据结构


Collection


  1. List


  • ArrayList:Object 数组;


  • Vector:Object 数组;


  • LinkedList:双向循环链表;


2.Set


  • HashSet:唯一,无序。基于 HashMap 实现,底层采用 HashMap 保存数据。
    它不允许集合中有重复的值,当我们提到HashSet时,第一件事情就是在将对象存储在HashSet之前,要先确保对象重写equals()和hashCode()方法,这样才能比较对象的值是否相等,以确保set中没有储存相等的对象。
    如果我们没有重写这两个方法,将会使用这个方法的默认实现。


  • LinkedHashSet: LinkedHashSet 继承与 HashSet,底层使用 LinkedHashMap 来保存所有元素。


  • TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树。)


Map


  • HashMap:JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。
    JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。


  • LinkedHashMap:LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。内部还有一个双向链表维护键值对的顺序,每个键值对既位于哈希表中,也位于双向链表中。LinkedHashMap支持两种顺序插入顺序 、 访问顺序。


  • 插入顺序:先添加的在前面,后添加的在后面。修改操作不影响顺序


  • 访问顺序:所谓访问指的是get/put操作,对一个键执行get/put操作后,其对应的键值对会移动到链表末尾,所以最末尾的是最近访问的,最开始的是最久没有被访问的,这就是访问顺序。


  • HashTable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的


  • TreeMap: 红黑树(自平衡的排序二叉树)


集合的 fail-fast 快速失败机制


Java 集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fast 机制。


原因:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。


集合在被遍历期间如果内容发生变化,就会改变modCount的值。


每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。


解决办法:


  1. 在遍历过程中,所有涉及到改变modCount值得地方全部加上synchronized。


  1. 使用CopyOnWriteArrayList来替换ArrayList


Collection 接口


List 接口


Itertator 是什么


Iterator 接口提供遍历任何 Collection 的接口。我们可以从一个 Collection 中使用迭代器方法来获取迭代器实例。


迭代器取代了 Java 集合框架中的 Enumeration,迭代器允许调用者在迭代过程中移除元素。


List<String> list = new ArrayList<>();
Iterator<String> it = list. iterator();
while(it. hasNext()){
  String obj = it. next();
  System. out. println(obj);
}


如何边遍历边移除 Collection 中的元素?


Iterator<Integer> it = list.iterator();
while(it.hasNext()){
   *// do something*
   it.remove();
}


一种最常见的错误代码如下:


for(Integer i : list){
   list.remove(i)
}


运行以上错误代码会报 ConcurrentModificationException 异常


如何实现数组和 List 之间的转换?


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


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


ArrayList 和 LinkedList 的区别是什么?


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


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


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


  • 内存空间占用:LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。


  • 线程安全:ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;


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


为什么 ArrayList 的 elementData 加上 transient 修饰?


ArrayList 中的数组定义如下:


private transient Object[] elementData;


ArrayList 的定义:


public class ArrayList<E> extends AbstractList<E>
     implements List<E>, RandomAccess, Cloneable, java.io.Serializable


ArrayList 实现了 Serializable 接口,这意味着 ArrayList 支持序列化。


transient 的作用是说不希望 elementData 数组被序列化。


每次序列化时,先调用 defaultWriteObject() 方法序列化 ArrayList 中的非 transient 元素,然后遍历 elementData,只序列化已存入的元素,这样既加快了序列化的速度,又减小了序列化之后的文件大小。


介绍下CopyOnWriteArrayList?


CopyOnWriteArrayList是ArrayList的线程安全版本,也是大名鼎鼎的copy-on-write(COW,写时复制)的一种实现。


在读操作时不加锁,跟ArrayList类似;在写操作时,复制出一个新的数组,在新数组上进行操作,操作完了,将底层数组指针指向新数组。


适合使用在读多写少的场景。例如add(Ee)方法的操作流程如下:使用ReentrantLock加锁,拿到原数组的length,使用Arrays.copyOf方法从原数组复制一个新的数组(length+1),将要添加的元素放到新数组的下标length位置,最后将底层数组指针指向新数组。


List、Set、Map三者的区别?


  • List(对付顺序的好帮手):存储的对象是可重复的、有序的。


  • Set(注重独一无二的性质):存储的对象是不可重复的、无序的。


  • Map(用Key来搜索的专业户):存储键值对(key-value),不能包含重复的键(key),每个键只能映射到一个值。


Set 接口


说一下 HashSet 的实现原理?


  • HashSet底层原理完全就是包装了一下HashMap


  • HashSet的唯一性保证是依赖与hashCode()equals()两个方法,所以存入对象的时候一定要自己重写这两个方法来设置去重的规则。


  • HashSet 中的元素都存放在 HashMap key 上面,而value 中的值都是统一的一个 private static final Object PRESENT = new Object();


hashCode()与equals()的相关规定


  1. 如果两个对象相等,则 hashcode 一定也是相同的


  1. 两个对象相等,对两个 equals 方法返回 true


  1. 两个对象有相同的 hashcode 值,它们也不一定是相等的


  1. 综上,equals方法被覆盖过,则hashCode方法也必须被覆盖


  1. hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。


==与equals的区别


  1. == 是判断两个变量或实例是不是指向同一个内存空间 equals 是判断两个变量或实例所指向的内存空间的值是不是相同


  1. == 是指对内存地址进行比较 equals() 是对字符串的内容进行比较


  1. ==指引用是否相同, equals() 指的是值是否相同。


Queue


BlockingQueue是什么?


Java.util.concurrent.BlockingQueue 是一个队列,在进行检索或移除一个元素的时候,线程会等待队列变为非空;


当在添加一个元素时,线程会等待队列中的可用空间。


BlockingQueue接口是Java集合框架的一部分,主要用于实现生产者-消费者模式。

Java提供了几种 BlockingQueue 的实现,比如ArrayBlockingQueueLinkedBlockingQueuePriorityBlockingQueueSynchronousQueue等。


在 Queue 中 poll()和 remove()有什么区别?


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


  • 不同点:如果没有元素 poll()会返回 null,而 remove()会直接抛出 NoSuchElementException 异常。
相关文章
|
29天前
|
算法 Java 数据处理
从HashSet到TreeSet,Java集合框架中的Set接口及其实现类以其“不重复性”要求,彻底改变了处理唯一性数据的方式。
从HashSet到TreeSet,Java集合框架中的Set接口及其实现类以其“不重复性”要求,彻底改变了处理唯一性数据的方式。HashSet基于哈希表实现,提供高效的元素操作;TreeSet则通过红黑树实现元素的自然排序,适合需要有序访问的场景。本文通过示例代码详细介绍了两者的特性和应用场景。
38 6
|
29天前
|
存储 Java
深入探讨了Java集合框架中的HashSet和TreeSet,解析了两者在元素存储上的无序与有序特性。
【10月更文挑战第16天】本文深入探讨了Java集合框架中的HashSet和TreeSet,解析了两者在元素存储上的无序与有序特性。HashSet基于哈希表实现,添加元素时根据哈希值分布,遍历时顺序不可预测;而TreeSet利用红黑树结构,按自然顺序或自定义顺序存储元素,确保遍历时有序输出。文章还提供了示例代码,帮助读者更好地理解这两种集合类型的使用场景和内部机制。
38 3
|
29天前
|
存储 Java 数据处理
Java Set接口凭借其独特的“不重复”特性,在集合框架中占据重要地位
【10月更文挑战第16天】Java Set接口凭借其独特的“不重复”特性,在集合框架中占据重要地位。本文通过快速去重和高效查找两个案例,展示了Set如何简化数据处理流程,提升代码效率。使用HashSet可轻松实现数据去重,而contains方法则提供了快速查找的功能,彰显了Set在处理大量数据时的优势。
32 2
|
1月前
|
存储 算法 Java
Java Set因其“无重复”特性在集合框架中独树一帜
【10月更文挑战第14天】Java Set因其“无重复”特性在集合框架中独树一帜。本文深入解析Set接口及其主要实现类(如HashSet、TreeSet)如何通过特定的数据结构(哈希表、红黑树)确保元素唯一性,并提供最佳实践建议,包括选择合适的Set实现类和正确实现自定义对象的`hashCode()`与`equals()`方法。
29 3
|
9天前
|
Java
Java 8 引入的 Streams 功能强大,提供了一种简洁高效的处理数据集合的方式
Java 8 引入的 Streams 功能强大,提供了一种简洁高效的处理数据集合的方式。本文介绍了 Streams 的基本概念和使用方法,包括创建 Streams、中间操作和终端操作,并通过多个案例详细解析了过滤、映射、归并、排序、分组和并行处理等操作,帮助读者更好地理解和掌握这一重要特性。
18 2
|
8天前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
13天前
|
存储 Java
判断一个元素是否在 Java 中的 Set 集合中
【10月更文挑战第30天】使用`contains()`方法可以方便快捷地判断一个元素是否在Java中的`Set`集合中,但对于自定义对象,需要注意重写`equals()`方法以确保正确的判断结果,同时根据具体的性能需求选择合适的`Set`实现类。
|
13天前
|
存储 Java 开发者
在 Java 中,如何遍历一个 Set 集合?
【10月更文挑战第30天】开发者可以根据具体的需求和代码风格选择合适的遍历方式。增强for循环简洁直观,适用于大多数简单的遍历场景;迭代器则更加灵活,可在遍历过程中进行更多复杂的操作;而Lambda表达式和`forEach`方法则提供了一种更简洁的函数式编程风格的遍历方式。
|
13天前
|
Java 开发者
|
25天前
|
安全 Java 程序员
深入Java集合框架:解密List的Fail-Fast与Fail-Safe机制
本文介绍了 Java 中 List 的遍历和删除操作,重点讨论了快速失败(fail-fast)和安全失败(fail-safe)机制。通过普通 for 循环、迭代器和 foreach 循环的对比,详细解释了各种方法的优缺点及适用场景,特别是在多线程环境下的表现。最后推荐了适合高并发场景的 fail-safe 容器,如 CopyOnWriteArrayList 和 ConcurrentHashMap。
52 5