1、 文章背景
工作已有五年之久,回望过去,没有在一线城市快节奏下学习成长,只能自己不断在工作中学习进步,最近一直想写写属于自己的文章,记录学习的内容和知识点,当做一次成长。
2、 集合的概述
摘要:Java 集合(Java Collections)是一组用于存储和操作对象的类库。
Java 的集合框架主要在 java.util 包中,它提供了一套设计良好的接口和实现类,这些接口和实现类用于存储和处理对象组。集合框架的设计提高了操作集合的性能,增加了操作的灵活性,并简化了编程工作。
根据上面框架图,抓住主干,即Collection和Map。
-
Collection是一个接口,是高度抽象出来的集合,它包含了集合的基本操作和属性。Collection分为两大分支List和Set。List是一个有序的队列,每一个元素都有它的索引。第一个元素的索引值是0。List的实现类有LinkedList、ArrayList、Vector、Stack。Set是一个不允许有重复元素的集合。Set的实现类有HastSet和TreeSet。HashSet依赖于HashMap,它实际上是通过HashMap实现的;TreeSet依赖于TreeMap,它实际上是通过TreeMap实现的。
-
Map是一个映射接口,即key-value键值对。Map中的每一个元素包含一个key和key对应的value。AbstractMap是个抽象类,它实现了Map接口中的大部分API。而HashMap、treeMap、WeakHashMap都是继承于AbstractMap。Hashtable虽然继承于Dictionary,但它实现了Map接口。
接下来,再看Iterator。它是遍历集合的工具,即我们通常通过Iterator迭代器来遍历集合。我们说Collection依赖于Iterator,是因为Collection的实现类都要实现iterator()函数,返回一个Iterator对象。ListIterator是专门为遍历List而存在的。
Java 集合框架主要分为几个核心部分:
-
接口(Interfaces) :
-
Collection:是最基础的集合表示方式,包含基本的添加、删除、遍历等操作。Set、List和Queue等接口都继承自Collection接口。Set:不允许有重复元素的集合。List:有序集合,允许有重复的元素。Queue:一种用于在处理之前保持元素的集合,提供了额外的插入、提取和检查操作。
-
Map:不属于 Collection 接口的一部分,它表示键值对的集合。每个键最多只能映射到一个值。
-
-
实现类(Implementations) :
ArrayList、LinkedList:List 接口的实现。HashSet、LinkedHashSet、TreeSet:Set 接口的实现。PriorityQueue:Queue 接口的实现。HashMap、LinkedHashMap、TreeMap:Map 接口的实现。
-
算法(Algorithms) :
- 集合框架提供了一些实现算法的静态方法,如排序、搜索、乱序、比较等,这些方法主要在
Collections类中。
- 集合框架提供了一些实现算法的静态方法,如排序、搜索、乱序、比较等,这些方法主要在
-
辅助类(Utilities) :
- 如
Collections和Arrays,提供了一系列的静态方法用于操作集合或数组。
- 如
3、 集合和数组的区别
Java集合和数组的区别主要体现在以下几个方面:
-
长度与可变性:
- 数组:长度固定,一旦创建无法改变。
- 集合:长度可变,可根据需要动态添加或删除元素。
-
类型限制:
- 数组:必须声明元素类型,且只能存储一种类型的数据。
- 集合:可存储任意类型的对象,增加灵活性。
-
访问方式:
- 数组:通过索引访问元素。
- 集合:提供多种访问方式,如迭代器、
foreach循环等。
-
效率与内存:
- 数组:在连续内存块中存储,访问速度快。
- 集合:基于链表或树等非连续结构,存储方式灵活但访问速度相对较慢。
-
初始化:
- 数组:创建后需手动初始化。
- 集合:创建时即可添加元素,无需单独初始化。
综上所述,Java集合和数组各有优势,选择哪种取决于具体需求和设计目标
4、 Java集合框架的主要特点
Java集合框架的主要特点可以概括为以下几点:
-
动态大小:集合可以根据需要动态调整大小,无需预先指定容量,使用更加灵活。
-
泛型支持:集合框架利用泛型机制,提供类型安全的操作,避免类型转换错误和运行时错误。
-
高性能:提供高效的数据结构和算法实现,满足大部分场景需求,如
ArrayList实现快速随机访问,LinkedList实现快速插入和删除。 -
统一接口:集合框架中的类和接口遵循统一的接口规范,方便切换不同的集合实现,便于代码编写和理解。 接口与实现分离:设计模式使得不同数据结构的实现可以相互替换,增加了框架的灵活性和可扩展性。
-
迭代支持:集合提供了迭代器来遍历元素,方便进行操作和遍历。
这些特点使得Java集合框架成为Java开发中处理数据集合的强大工具。
5、 常用集合的分类
-
Collection接口的对象的集合(单列集合):
-
List接口:元素按进入先后有序保存,可重复LinkedList接口实现类, 链表, 插入删除, 没有同步, 线程不安全。ArrayList接口实现类, 数组(动态数组), 随机访问, 没有同步, 线程不安全。Vector接口实现类, 数组, 同步, 线程安全。Stack 是Vector类的实现类。
-
Set接口: 仅接收一次,不可重复,并做内部排序HashSet使用hash表(数组)存储元素LinkedHashSet链表维护元素的插入次序
TreeSet底层实现为二叉树,元素排好序
-
-
Map 接口 键值对的集合 (双列集合):
Hashtable接口实现类, 同步, 线程安全HashMap接口实现类 ,没有同步, 线程不安全LinkedHashMap双向链表和哈希表实现WeakHashMap集合框架中的一个特殊成员,其键为弱引用,可能会在垃圾回收(GC)时被自动删除
TreeMap红黑树对所有的key进行排序ConcurrentHashMap接口实现类,同步,线程安全
6、 List和Set的区别
List和Set集合的区别主要体现在以下几个方面:
- 元素重复性:
List允许存储重复的元素,而Set不允许存储重复元素。 - 元素顺序:
List是有序的集合,维护了元素的插入顺序,可以通过索引访问元素;Set是无序的集合(除了特定实现类),元素没有特定的索引,不能通过索引访问。 - 实现类:
List接口的主要实现类有ArrayList、LinkedList和Vector;Set接口的主要实现类有HashSet、LinkedHashSet和TreeSet。 - 元素唯一性判断:Set集合通过元素的哈希码(hashCode())和equals()方法来保证元素的唯一性;而List集合则完全依赖于元素的顺序,不涉及唯一性判断。
综上所述,List和Set集合在元素重复性、元素顺序、实现类以及元素唯一性判断等方面存在明显的区别,适用于不同的使用场景。
7、 常用集合的底层实现
-
ArrayList实现
-
定义与位置:
ArrayList是Java集合框架中的一部分,位于java.util包下,是一个动态数组。 -
底层结构:
ArrayList底层采用数组数据结构存储元素,默认初始容量为10,扩容因子为1.5。扩容公式:原始长度*0.5+1。 -
特性:
- 动态性:数组长度可以随着容量的增长而增长,但不会自动缩小,除非调用
trimToSize方法。 - 非线程安全:
ArrayList不是线程安全的,多线程环境下需要手动同步。 - 泛型支持:
ArrayList支持泛型,可以在编译阶段约束集合对象只能操作某种数据类型。
- 动态性:数组长度可以随着容量的增长而增长,但不会自动缩小,除非调用
-
使用场景:适用于元素个数不确定且需要进行增删操作的场景,以及需要动态扩容的情况。
-
-
LinkList实现
-
定义与位置:
LinkList是Java集合框架中的一部分,位于java.util包下,是一个双向链表。这意味着每个节点(Node)都包含两部分:一部分是存储的实际数据(称为item或element),另一部分是两个指针(称为next和prev),分别指向列表中的下一个节点和上一个节点。 -
底层结构:
LinkedList没有初始默认长度。它是一个基于双向链表的数据结构,可以动态地增删节点,不需要预先定义大小。LinkedList不需要扩容。由于其底层是链表结构,每次添加或删除元素时,只需调整链表中的节点指针即可,无需像数组那样重新分配内存或复制数据。 -
特性:
- 动态性:
LinkedList是动态分配内存的,它没有固定的容量限制。 - 非线程安全:
LinkedList不是线程安全的,如果需要在多线程环境中使用,必须采用适当的同步机制,或者使用Collections.synchronizedList方法来包装LinkedList对象。 - 泛型支持:
LinkedList支持泛型,可以在编译阶段约束集合对象只能操作某种数据类型。
- 动态性:
-
使用场景:
- 在需要频繁在列表中间插入或删除元素时,
LinkedList性能优于ArrayList,如任务调度系统。 LinkedList可用作队列(FIFO)实现,支持高效的入队和出队操作,如打印任务队列系统。LinkedList实现了Deque接口,可以在两端进行插入和删除操作,如浏览器历史记录。
- 在需要频繁在列表中间插入或删除元素时,
-
-
Vector实现
-
定义与位置:
Vector在Java中是一个类,它实现了List接口。与ArrayList类似,Vector也是一个动态数组,但它与ArrayList有几个关键的区别。 -
底层结构:
Vector的默认容量为10。这意味着在没有指定初始容量的情况下,创建一个新的Vector对象时,它将能够容纳10个元素。扩容公式:原始长度*2 -
特性:
- 动态性:当
Vector达到其当前容量时,它会自动扩容。Vector的扩容策略是将当前容量增加一倍。例如,如果Vector的容量为10,一次扩容后容量将变为20。 - 线程安全:
Vector是线程安全的,因为它的方法被同步,以防止并发访问导致的数据不一致。然而,这种同步机制会导致性能上的开销,特别是在高并发环境中。 - 泛型支持:
Vector支持泛型,可以在编译阶段约束集合对象只能操作某种数据类型。
- 动态性:当
-
使用场景:
Vector的所有方法都是同步的,因此它适用于多线程环境下需要安全访问和修改集合的场景。- 当需要一个能够自动调整大小的数组时,可以选择
Vector。Vector会根据元素的增加自动扩容,无需手动管理数组大小。 Vector支持通过索引顺序存储和访问元素,适用于需要频繁按索引访问元素的场景。Vector提供了在末尾添加和删除元素的方法,可以方便地实现栈(后进先出)或队列(先进先出)的数据结构
-
-
HashSet实现
-
定义与位置:
HashSet(哈希集合)是Java编程语言中的一个集合类,它实现了Set接口并继承自AbstractSet类。HashSet通过使用哈希表来存储元素,提供了快速的插入、删除和查询操作。 -
底层结构:
- HashSet底层采用哈希表实现,具体是基于HashMap存储元素。在HashMap中,元素存储在key中,而value则使用统一的PRESENT对象。默认初始容量为16。加载因子为0.75:即当 元素个数 超过 容量长度的0.75倍 时,进行扩容。扩容增量:原容量的 1 倍。
- 在JDK 7中,哈希表采用数组+链表实现;JDK 8则引入红黑树,当链表长度超过8且数组长度大于64时,链表转化为红黑树,以提高性能。
-
特性:
- 动态性:默认初始容量为16。加载因子为0.75:即当 元素个数 超过 容量长度的0.75倍 时,进行扩容。扩容增量:原容量的 1 倍。如 HashSet的容量为16,一次扩容后是容量为32
- 非线程安全:线程不安全,存取速度快。
- 泛型支持:
HashSet支持泛型,可以在编译阶段约束集合对象只能操作某种数据类型。 - 无序性:
HashSet中的元素没有特定的顺序,存储和检索顺序是不确定的,与插入顺序无关。 - 不允许重复元素:
HashSet不允许存储相同的元素,如果尝试向HashSet中添加相同的元素,它将忽略该操作。
-
使用场景:
- 去重操作:由于
HashSet不允许存储重复元素,因此常用于去除集合中的重复项。 - 快速查找:
HashSet基于哈希表实现,提供了快速的查找性能,适用于需要频繁判断元素是否存在的场景。 - 集合运算:
HashSet支持集合的并集、交集、差集等运算,方便进行集合间的操作。 - 缓存:
HashSet可以作为缓存的一种数据结构,存储需要快速访问的数据。 - 词频统计:在处理文本数据时,HashSet可用于统计单词的出现次数,通过存储不重复的单词并遍历文本进行计数。
- 去重操作:由于
-
-
LinkHashSet实现
-
定义与位置:
LinkedHashSet是 Java 集合框架中的一个具体类,它继承自HashSet并且实现了Set接口。与HashSet不同的是,LinkedHashSet维护了一个双向链表来记录元素的插入顺序,因此它能够保证迭代时按照元素插入的顺序进行。 -
底层结构:
LinkedHashSet底层基于HashMap实现,但它额外维护了一个双向链表(LinkedHashMap中的Entry实际上是一个双向链表的节点)。- 这个双向链表记录了元素插入的顺序,使得
LinkedHashSet能够按照插入顺序进行迭代。
-
特性:
- 动态性:默认初始容量为16。加载因子为0.75:即当 元素个数 超过 容量长度的0.75倍 时,进行扩容。扩容增量:原容量的 1 倍。如 LinkedHashSet的容量为16,一次扩容后是容量为32
- 非线程安全:线程不安全,存取速度快。
- 泛型支持:
LinkedHashSet支持泛型,可以在编译阶段约束集合对象只能操作某种数据类型。 - 有序性:哈希表保证了元素的唯一性,而链表则维护了元素的插入顺序。
- 不允许重复元素:
LinkedHashSet不允许存储相同的元素,如果尝试向LinkedHashSet中添加相同的元素,它将忽略该操作。
-
使用场景:
LinkedHashSet适用于需要既保持集合特性(不重复元素)又需要按照插入顺序迭代元素的场景。
-
-
TreeSet实现
TreeSet是Java集合框架中的一个有序且不可重复的集合类。以下是关于TreeSet的详细总结:
- 底层实现:
TreeSet是基于红黑树(Red-Black Tree)的数据结构实现的,这使得TreeSet能够保持元素的排序顺序,并且具有较好的增删改查性能。 - 排序方式:
TreeSet中的元素会按照自然顺序进行排序,或者根据创建TreeSet时提供的Comparator进行自定义排序。 - 元素唯一性:
TreeSet存储的元素是唯一的,即不允许存储重复的元素。 - 应用场景:
TreeSet适用于需要保持元素排序顺序且不允许重复元素的场景,如按照生日对用户信息进行排序展示等。
综上所述,TreeSet是一个功能强大的有序集合类,它结合了红黑树的优点,实现了元素的自然排序和快速查找,同时保证了元素的唯一性
- Hashtable实现
- 线程安全性:
Hashtable是线程安全的,其方法通过synchronized关键字实现同步,适合在多线程环境中使用。但同步机制也带来了性能开销。 - 空键值:
Hashtable不允许键和值为null。如果尝试插入空键或值,会抛出NullPointerException。 - 继承关系:
Hashtable继承自Dictionary类,并实现了Map接口。 - 初始容量与扩容:
Hashtable的初始容量为11,扩容时是容量翻倍+1,即capacity(2^n+1),且填充因子默认为0.75。 - 底层实现:
Hashtable的底层实现是数组+链表结构,与HashMap类似。但在计算hash时,Hashtable直接使用key的hashcode对table数组的长度进行取模
综上所述,Hashtable是一个线程安全的、不允许空键值的集合类,适用于需要同步机制的多线程环境,但需注意其性能开销。
-
HashMap
HashMap 是一种快速的查找并且插入、删除性能都良好的一种 K/V键值对的数据结构,它基于哈希表的 Map 接口实现,是常用的 Java 集合之一,是非线程安全的。
HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个。
-
HashMap的底层数据结构
-
JDK1.8 之前,底层采用数组+链表,用(n - 1) & hash找到数组索引位置,若冲突则用拉链法解决冲突。
-
拉链法 简单来说就是将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可
-
JDK1.8 之后 底层初始数据结构仍采用数组+链表,当某个桶链表的长度大于8时,会先调用treeifyBin()方法,这个方法会判断数组长度是否小于64,如果大于或等于则执行转换红黑树操作,以减少搜索时间;反之则调用resize()进行扩容。
-
JDK1.8的数据结构示意图如下:
-
其中,桶数组是用来存储数据元素,链表是用来解决冲突,红黑树是为了提高查询的效率。
- 数据元素通过映射关系,也就是散列函数,映射到桶数组对应索引的位置。
- 如果发生冲突,从冲突的位置拉一个链表,插入冲突的元素
- 如果链表长度>8&数组大小>=64,链表转为红黑树
- 如果红黑树节点个数<6 ,转为链表
-
解决哈希冲突有哪些方法呢
- 链式地址法:把存在Hash冲突的key,以单向链表来进行存储。
- 开放定址法:开放定址法也称线性探测法,就是从冲突的位置再接着往下找,给冲突元素找个空位。
- 再哈希法:换种哈希函数对key进行Hash,一直运算,直到不再产生冲突为止。
- 建立公共溢出区:再建一个数组,把冲突的元素放进去。
-
HashMap是如何解决哈希冲突的
- 在JDK8之 前HashMap采用的是链式寻址法解决哈希冲突的,而JDK8之后则是通过链式寻址法以及红黑树来解决Hash冲突。
- LinkedHashMap
- 定义与继承:LinkedHashMap是HashMap的子类,结合了HashMap和双向链表的特点,实现了Map接口。
- 特性:
- 有序性:通过维护一个额外的双向链表,保证了迭代顺序。可以是插入顺序,也可以是访问顺序(需设置accessOrder为true)。
- 允许空值:与HashMap一样,LinkedHashMap允许键和值为null,但键最多只能有一个为null。
- 非同步:LinkedHashMap是非线程安全的,适用于单线程环境。
- 应用场景:由于LinkedHashMap支持按照访问顺序排序,因此常用于实现LRU(最近最少使用)缓存策略。
- 内部实现:LinkedHashMap的Entry继承了HashMap的Node,并增加了before和after变量,用于维护双向链表的前后节点关系。在put和get操作时,会维护这个双向链表,从而确保遍历时的有序性 键值对的能力。
- TreeMap
TreeMap的底层数据结构是红黑树。以下是关于TreeMap底层数据结构的详细解释:
- 红黑树特性:红黑树是一种自平衡的二叉搜索树,具有严格的规则来保持树的平衡。每个节点都有颜色(红色或黑色),且遵循特定的排列规则,如根节点是黑色的,所有叶子节点都是黑色的,红色节点的两个子节点都是黑色的等。
- 排序原理:TreeMap利用红黑树的性质,根据key进行排序。在TreeMap中,每个元素都能插入到红黑树的适当位置,从而维护了key的大小关系。这使得TreeMap特别适用于需要排序的场景。 自定义排序:如果需要实现自定义的排序方式(如降序),可以通过实现Comparator接口来自定义比较器。
综上所述,TreeMap的底层数据结构是红黑树,这种结构使得TreeMap能够高效地维护有序的键值对集合。
- ConcurrentHashMap ConcurrentHashMap是Java集合框架中一个非常重要的线程安全哈希表实现,专为多线程环境设计。以下是对ConcurrentHashMap的详细总结:
-
特性
- 线程安全:ConcurrentHashMap通过一系列复杂的机制,如分段锁(在早期版本中)和CAS(Compare and Swap)操作,以及改进的锁策略(在Java 8及更高版本中),确保在多线程环境下进行安全的读写操作。
- 高效并发:通过减小锁的粒度(例如,在Java 8中,锁只锁定哈希表的一个部分,而不是整个表),ConcurrentHashMap允许更高的并发度,从而提高了性能。
- 存储结构:底层采用数组+链表+红黑树的组合。当链表长度超过一定阈值时,链表会转换为红黑树,以提高查询效率。这种结构使得ConcurrentHashMap在处理大量数据时仍然能够保持高效。
-
原理
- 分段锁(早期版本):在Java 7及之前的版本中,ConcurrentHashMap使用分段锁来减少锁的竞争。哈希表被分成多个段(Segment),每个段都是一个独立的哈希表,有自己的锁。这样,当一个线程在操作某个段时,不会影响其他段的操作。
- CAS操作与锁策略(Java 8及更高版本):在Java 8中,ConcurrentHashMap的锁策略进行了改进,不再使用分段锁,而是采用了更细粒度的锁和CAS操作。这进一步减少了锁的竞争,提高了并发性能。
- 节点类型:在Java 8中,引入了节点(Node)的概念,用于存储键值对。节点可以是普通的链表节点,也可以是树节点(当链表长度超过阈值时转换为红黑树)。
-
注意事项
- 复合操作非原子性:尽管ConcurrentHashMap提供了线程安全的读写操作,但它不保证复合操作(如先检查键是否存在然后再插入)的原子性。对于此类操作,应使用ConcurrentHashMap提供的原子性复合操作方法,如
putIfAbsent。 - 迭代弱一致性:ConcurrentHashMap的迭代器是弱一致性的,即在迭代过程中,允许其他线程对哈希表进行并发修改。这可能导致迭代器在遍历过程中遇到并发修改的数据。
- 复合操作非原子性:尽管ConcurrentHashMap提供了线程安全的读写操作,但它不保证复合操作(如先检查键是否存在然后再插入)的原子性。对于此类操作,应使用ConcurrentHashMap提供的原子性复合操作方法,如
-
使用场景
- ConcurrentHashMap适用于需要高效并发访问的场景,如缓存、计数器、多线程环境下的数据共享等。它提供了比传统哈希表更高的并发性能,同时保证了线程安全。
综上所述,ConcurrentHashMap是Java中一个非常强大且高效的线程安全哈希表实现,适用于多线程环境下的数据存取操作。通过不断改进的锁策略和存储结构,它能够在高并发环境下提供出色的性能。