Java 核心集合容器全解((上))
从面试角度作为切入点提升大家的 Java 内功,所谓根基不牢,地动山摇。
码哥在 《Redis 系列》的开篇 Redis 为什么这么快中说过:学习一个技术,通常只接触了零散的技术点,没有在脑海里建立一个完整的知识框架和架构体系,没有系统观。这样会很吃力,而且会出现一看好像自己会,过后就忘记,一脸懵逼。
我们需要一个系统观,清晰完整的去学习技术,在「面霸篇:Java 核心基础大满贯(卷一)」中,码哥梳理了 Java 高频核心知识点。
本篇将一举攻破 Java 集合容器知识点,跟着「码哥」一起来提纲挈领,梳理一个完整的 Java 容器开发技术能力图谱,将基础夯实。
◆ 什么是集合?
◆ 集合的特点
◆ 集合与数组的区别
◆ 集合框架有哪些优势
◆ 有哪些常用的集合类
◆ 集合的底层数据结构
◊ Collection
◊ Map
◆ 集合的 fail-fast 快速失败机制
◆ List 接口
◊ Itertator 是什么
◊ 如何边遍历边移除 Collection 中的元素?
◊ 如何实现数组和 List 之间的转换?
◊ ArrayList 和 LinkedList 的区别是什么?
◊ 为什么 ArrayList 的 elementData 加上 transient 修饰?
◊ 介绍下 CopyOnWriteArrayList?
◊ List、Set、Map 三者的区别?
◆ Set 接口
◊ 说一下 HashSet 的实现原理?
◆ Queue
◊ BlockingQueue 是什么?
◊ 在 Queue 中 poll()和 remove()有什么区别?
◆ Map 接口
◊ HashMap 的实现原理?
◊ JDK1.7 VS JDK1.8 比较
◊ 如何有效避免哈希碰撞
◊ HashMap 的 put 方法的具体流程?
◊ HashMap 的扩容操作是怎么实现的?
◊ 任何类都可以作为 Key 么?
◊ 为什么 HashMap 中 String、Integer 这样的包装类适合作为 K?
◊ HashMap 为什么不直接使用 hashCode()处理后的哈希值直接作为 table 的下标?
◊ HashMap 的长度为什么是 2 的幂次方
◊ HashMap 和 ConcurrentHashMap 的区别
◊ ConcurrentHashMap 实现原理
集合容器概述
什么是集合?
顾名思义,集合就是用于存储数据的容器。
集合框架是为表示和操作集合而规定的一种统一的标准的体系结构。任何集合框架都包含三大块内容:对外的接口、接口的实现和对集合运算的算法。
❝码老湿,可以说下集合框架的三大块内容具体指的是什么吗?
接口
面向接口编程,抽象出集合类型,使得我们可以在操作集合的时候不必关心具体实现,达到「多态」。
就好比密码箱,我们只关心能打开箱子,存放东西,并且关闭箱子,至于怎么加密咱们不关心。
接口实现
每种集合的具体实现,是重用性很高的数据结构。
算法
集合提供了数据存放以及查找、排序等功能,集合有很多种,也就是算法通常也是多态的,因为相同的方法可以在同一个接口被多个类实现时有不同的表现。
事实上,算法是可复用的函数。它减少了程序设计的辛劳。
集合框架通过提供有用的数据结构和算法使你能集中注意力于你的程序的重要部分上,而不是为了让程序能正常运转而将注意力于低层设计上。
集合的特点
● 对象封装数据,多个对象需要用集合存储;
● 对象的个数可以确定使用数组更高效,不确定个数的情况下可以使用集合,因为集合是可变长度。
集合与数组的区别
● 数组是固定长度的;集合可变长度的。
● 数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用数据类型。
● 数组存储的元素必须是同一个数据类型;集合存储的对象可以是不同数据类型。
由于有多种集合容器,因为每一个容器的自身特点不同,其实原理在于每个容器的内部数据结构不同。
集合容器在不断向上抽取过程中,出现了集合体系。在使用一个体系的原则:参阅顶层内容。建立底层对象。
集合框架有哪些优势
● 容量自动增长扩容;
● 提供高性能的数据结构和算法;
● 可以方便地扩展或改写集合,提高代码复用性和可操作性。
● 通过使用 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
集合的底层数据结构
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 值,是的话就返回遍历;否则抛出异常,终止遍历。
解决办法:
- 在遍历过程中,所有涉及到改变 modCount 值得地方全部加上 synchronized。
- 使用 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()的相关规定:
- 如果两个对象相等,则 hashcode 一定也是相同的
- 两个对象相等,对两个 equals 方法返回 true
- 两个对象有相同的 hashcode 值,它们也不一定是相等的
- 综上,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖
- hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。
==与 equals 的区别
- ==是判断两个变量或实例是不是指向同一个内存空间 equals 是判断两个变量或实例所指向的内存空间的值是不是相同
- == 是指对内存地址进行比较 equals() 是对字符串的内容进行比较
- ==指引用是否相同, equals() 指的是值是否相同。
文章转载自公众号:码哥字节