一、概念
1. 集合概述
集合类是Java数据结构的实现。Java的集合类是java.util包中的重要内容,它允许以各种方式将元素分组,并定义了各种使这些元素更容易操作的方法。Java集合类是Java将一些基本的和使用频率极高的基础类进行封装和增强后再以一个类的形式提供。集合类是可以往里面保存多个对象的类,存放的是对象,不同的集合类有不同的功能和特点,适合不同的场合,用以解决一些实际问题。
java集合可分为Set、List、Queue和Map四种体系。
set代表无序、不可重复的集合
List代表有序、重复的集合
而Map则代表具有映射关系的集合
Queue代表一种队列集合实现
2. 集合与数组的区别
数组的长度是在创建的时候是固定的,集合的长度是可以动态变化的。
数组中存放基本数据类型和引用类型,集合存放的是引用类型不能存放基本数据类型(若是简单的int,它会自动装箱成Integer)
3. 集合的特点
集合用于存储对象的容器,对象是用来封装数据,对象多了也需要存储集中式管理。
对象的个数确定可以使用数组,对象的个数不确定的可以用集合。因为集合是可变长度的。
4. 集合框架图
java 集合:
在 Java 中,集合框架主要由 Collection
接口体系和 Map
接口体系构成 :
- Collection 接口体系:是一组存储单个元素的集合接口,是所有单列集合的根接口,定义了如添加、删除、遍历等通用操作方法。它有
List
(元素有序且可重复,如ArrayList
、LinkedList
)、Set
(元素无序且不可重复,如HashSet
、TreeSet
)、Queue
(元素遵循特定队列规则,如先进先出,像PriorityQueue
)等子接口 。 - Map 接口体系:用于存储键值对(key - value),一个键最多映射到一个值,可快速根据键查找对应值,像
HashMap
、TreeMap
、ConcurrentHashMap
等 。
Collection集合:
Collection 接口的接口 对象的集合(单列集合)
├——-List 接口:元素按进入先后有序保存,可重复
│—————-├ LinkedList 接口实现类, 链表, 插入删除, 没有同步, 线程不安全
│—————-├ ArrayList 接口实现类, 数组, 随机访问, 没有同步, 线程不安全
│—————-└ Vector 接口实现类 数组, 同步, 线程安全
│ ———————-└ Stack 是Vector类的实现类
└——-Set 接口: 仅接收一次,无序不可重复,并做内部排序
├—————-└HashSet 使用hash表(数组)存储元素
│————————└ LinkedHashSet 链表维护元素的插入次序
└ —————-TreeSet 底层实现为二叉树,元素排好序
常用方法:
Map集合:
Map 接口 键值对的集合 (双列集合)
├———Hashtable 接口实现类, 同步, 线程安全
├———HashMap 接口实现类 ,没有同步, 线程不安全-
│—————–├ LinkedHashMap 双向链表和哈希表实现
│—————–└ WeakHashMap ├ ——–TreeMap 红黑树对所有的key进行排序
└———IdentifyHashMap
Collection 和 Map 是 Java 集合框架的根接口,这两个接口又包含了一些子接口或实现类
Collection接口:单列数据,定义了存取一组允许重复对象的方法的集合
- List:集合中的元素是有序集合,集合中的元素可以重复(动态数组),访问集合中的元素可以根据元素的索引来访问
- Set:集合是无序集合,集合中的元素不可以重复,访问集合中的元素只能根据元素本身来访问(也是集合里元素不允许重复的原因)
Map接口:双列数据,集合中保存具有映射关系“key-value对”的元素,访问时只能根据每项元素的 key 来访问其 value
二、Collection集合
1、Collections
Collections 是一个工具类型,一个抓们操作集合的工具类。
代码示例
public class TestCollections {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(2);
list.add(1);
list.add(3);
//将集合中的元素排序
Collections.sort(list);
for (Integer integer : list) {
System.out.println(integer);
}
//将多个元素添加到集合中
Collections.addAll(list, 4, 6, 5);
//获取集合中最大值
Integer max = Collections.max(list);
//获取集合中最小值
Integer min = Collections.min(list);
}
}
2、List(存储有序(插入顺序),可重复数据)
- ArrayList:底层数据结构是数组,查询快,增删慢,线程不安全,效率高,可以存储重复元素
- LinkedList: 底层数据结构是链表,查询慢,增删快,线程不安全,效率高,可以存储重复元素
- Vector:底层数据结构是数组,查询快,增删慢,线程安全,效率低,可以存储重复元素
ArrayList
- 有序,非线程安全,可为null,可重复 ,查询快,插入、删除慢、效率高。
- 底层实现Object数组,它实现了Serializable接口,因此它支持序列化。
- 默认容量为10,当数据超过当前容量会进行自动扩容,每次扩容原数组的1/2 。
使用:随机访问比较多就使用ArrayList
import java.util.ArrayList;
import java.util.List;
public class TestArrayList {
public static void main(String[] args) {
// 在创建一个ArrayList对象时一般将其向上转型为List,
List<Integer> list = new ArrayList<>();
// 在定义时,建议加上泛型
list.add(1);
list.add(3);
list.add(5);
// List 独有方法 ,将下标为2的元素修改为9
Integer set = list.set(2, 9);
System.out.println(list.get(2));
}
}
//结果
9
常用API
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
public class TestArrayList {
/*
* ArrayList 的基本使用:
* ArrayList 自身可以动态调整大小
* ArrayList 的内部使用数组存储元素,当数组将被存满,就会创建一个新数组,其容量是当前数组的 1.5 倍
* 同时,所有元素都将移至新数组,假设内部数组已满,而我们现在又添加了一个元素,ArrayList 容量就会以相同
* 的比例扩展(在这种情况下,内部数组中将有一些未分配的空间)。此时,trimToSize()方法可以删除未分配的空间
* 并更改 ArrayList 的容量,使其等于 ArrayList 中的元素个数
* */
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
//1.增加元素
list.add("Tom");
list.add("Marry");
list.add("Andy");
list.add("Jhon");
System.out.println(list);
//2.访问元素
/*System.out.println(list.get(1));*/ // 通过元素下标访问第二个元素
//3.修改元素
/* list.set(1, "ZS");
System.out.println(list);*/
//4.删除元素
/*list.remove(1);
System.out.println(list);*/
//5.计算大小
/*int length = list.size();
System.out.println(length);*/
//6、迭代数组列表
/*for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}*/
//7.使用for-each 来迭代元素
/*for (String s : list) {
System.out.println(s);
}*/
//8.ArrayList 排序
//8.1 使用 Arrays 自带方法 sort
//正序
/*list.sort(Comparator.naturalOrder());
System.out.println(list);
//倒序
list.sort(Comparator.reverseOrder());
System.out.println(list);*/
//9.插入元素到指定为止
/*list.add(2, "Pm");
System.out.println(list.toString());*/
//10.添加集合中所有元素到 arrayList 中
/*ArrayList<String> list1 = new ArrayList<String>();
list1.add("GOD");
list1.add("SD");
//把 list1 所有元素添加到 list 中
list.addAll(list1);
// 在指定位置插入 list1
list.addAll(2, list1);
System.out.println(list);*/
//11.删除 arrayList 中所有的元素
//list.clear();
/*list.removeAll(list);
System.out.println("所有 clear() 方法后: " + list);*/
//12.赋值一份 arrayList
//clone 属于浅拷贝,浅拷贝只是赋值指向某个对象的指针,而不复制对象本身,新旧对象还是共享一块内存,
// 所以如果期中一个对象改变了这个地址,就会影响到另一个对象
//浅拷贝对应的就是深拷贝,深拷贝是将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,
//且修改新对象不会影响原对象
/*ArrayList<String> cloneList = (ArrayList<String>)list.clone();
System.out.println("拷贝 ArrayList:" + cloneList);*/
//13.判断元素是否存在于 arrayList 中
/*System.out.println("Marry 是否存在于 arrayList中");
System.out.println(list.contains("Marry"));*/
//14.获取元素的索引值
/*int index = list.indexOf("Marry");
System.out.println("Marry 的索引位置: " + index);*/
//15.判断 arrayList 是否为空
/*System.out.println("arrayList 是否为空: " + list.isEmpty());*/
//16.截取 ArrayList 部分元素
/*System.out.println("SubList: " + list.subList(0,1));*/
//17.toArray() 方法将 ArrayList 对象转化为数组
//创建一个新的 String 类型数组
/*String[] arr = new String[list.size()];
list.toArray(arr);
for (String s : arr) {
System.out.println(s);
}*/
//18.设定指定容量大小的 arrayList
/*ArrayList<Integer> list2 = new ArrayList<Integer>();
list2.ensureCapacity(3);
list2.add(1);
list2.add(2);
System.out.println(list2.size());
System.out.println(list2);*/
//19.返回指定元素在 arrayList 中最后一次出现的位置
/*int index = list.lastIndexOf("Marry");
System.out.println("Marry最后一次出现为止: " + index);*/
//20.保留 arrayList 中 在指定集合中也存在的那些元素
//创建另一个动态数组
/*ArrayList<String> site = new ArrayList<String>();
site.add("Marry");
site.add("Jhon");
//保留元素
list.retainAll(site);
System.out.println("保留的元素:" + list);*/
//21.查看 arrayList 是否包含指定集合中的那些元素
//创建一个动态数组
/*ArrayList<String> site = new ArrayList<String>();
site.add("Marry");
site.add("Jhon");
// 检查数组 list 是否包含 site
boolean b = list.containsAll(site);
System.out.println(b);*/
//22.将 arrayList 中的容量调整为数组中的元素个数
//调整容量
/*list.trimToSize();
System.out.println("ArrayList 大小: " + list.size());*/
//23.删除 arrayList 中指定索引之间存在的元素
//删除所有满足特定条件的 arraylist 元素
list.removeIf(e -> e.contains("Jh"));
System.out.println("删除后的 ArrayList: " + list);
}
}
LinkedList
- 有序、非线程安全,插入、删除快、查询效率不高
- 底层为双向链表
使用:插入删除比较多的时候就选用LinkedList
import java.util.LinkedList;
import java.util.List;
public class TestLinkedList {
public static void main(String[] args) {
// 在创建一个LinkedList对象时一般将其向上转型为List
List<Integer> list = new LinkedList<>();
// 在定义时,建议加上泛型
list.add(1);
list.add(3);
list.add(5);
// List 独有方法 ,将下标为2的元素修改为9
Integer set = list.set(2, 9);
System.out.println(list.get(2));
}
}
//结果
9
常用API
public class TestLinkedList {
public static void main(String[] args) {
List<String> linkedList = new LinkedList<>();
linkedList.addFirst("aa");
linkedList.addLast("bb");
//获取头结点 没有头结点 java.util.NoSuchElementException
String first = linkedList.getFirst();
System.out.println(first);
//获取尾结点 没有尾结点 java.util.NoSuchElementException
String last = linkedList.getLast();
System.out.println(last);
//获取头结点 没有头结点 返回 null
String s = linkedList.peekFirst();
System.out.println(s);
//获取尾结点 没有尾结点 放回 null
String s1 = linkedList.peekLast();
System.out.println(s1);
//删除头结点 没有头结点 java.util.NoSuchElementException
String s2 = linkedList.removeFirst();
System.out.println(s2); //被删除结点中的值
//删除尾结点 没有尾结点 java.util.NoSuchElementException
String s3 = linkedList.removeLast();
System.out.println(s3);//被删除结点中的值
//删除头结点 没有头结点 返回 null
String s4 = linkedList.pollFirst();
System.out.println(s4);
//删除尾结点 没有尾结点 放回 null
String s5 = linkedList.pollLast();
System.out.println(s5);
}
}
Vector
- 有序,可重复,查询快,插入、删除慢
- 底层实现Object数组,跟ArrayList结构相似,线程安全的,加了synchronized,
- 效率低,一般不常用,在考虑到线程并发访问的情况下才会去使用Vector子类
Stack栈:后进先出(LIFO),继承自Vector,也是数组,线程安全的栈。
但作为栈数据类型,不建议使用Vector中与栈无关的方法,尽量只用Stack中的定义的栈相关方法,这样不会破坏栈数据类型。
使用:要求线程安全就选用Vector
import java.util.List;
import java.util.Vector;
public class TestVector {
public static void main(String[] args) {
// 在创建一个Vector对象时一般将其向上转型为List,
List<Integer> list = new Vector<>();
// 在定义时,建议加上泛型
list.add(1);
list.add(3);
list.add(5);
// List 独有方法 ,将下标为2的元素修改为9
Integer set = list.set(2, 9);
System.out.println(list.get(2));
}
}
//结果
9
ArrayList 和 Vector比较
构造方法
- ArrayList:提供了三个构造方法。
-
public ArrayList(int initialCapacity)
:用于构造一个具有指定初始容量的空列表。public ArrayList()
:默认构造一个初始容量为 10 的空列表。public ArrayList(Collection<? extends E> c)
:构造一个包含指定Collection
元素的列表。
- Vector:具备四个构造方法。
-
public Vector()
:使用指定的初始容量和等于 0 的容量增量构造一个空向量。public Vector(int initialCapacity)
:构造一个空向量,使其内部数据数组的大小为指定值,标准容量增量为零。public Vector(Collection<? extends E> c)
:构造一个包含指定Collection
中元素的向量。public Vector(int initialCapacity, int capacityIncrement)
:使用指定的初始容量和容量增量构造一个空的向量。
相同点
- 底层结构:二者底层均采用可变长数组来实现。
- 元素特性:存储的元素都是有序且可重复的。
不同点
- 线程安全性:
-
- ArrayList:属于非同步操作,即线程不安全。在多线程环境下,若多个线程同时对
ArrayList
进行读写操作,可能会产生数据不一致等不确定的结果。不过,在单线程环境中,由于无需进行线程同步的开销,其性能表现更好。 - Vector:是线程同步的,这意味着多线程访问同一
Vector
实例时,不会产生不确定的结果,保证了线程安全。从源码中可以看到,Vector
类中的很多方法都使用了synchronized
关键字进行修饰。但这种线程同步机制带来了额外的性能开销,使得Vector
在效率上无法与ArrayList
相比。
- ArrayList:属于非同步操作,即线程不安全。在多线程环境下,若多个线程同时对
- 扩容机制:
-
- ArrayList:当数组空间不足时,扩容后的长度为之前长度的 1.5 倍。
- Vector:扩容后长度为之前的 2 倍。并且,
Vector
可以设置增长因子(即容量增量),而ArrayList
不支持此功能。
- 使用历史:
Vector
是一种较早出现的动态数组,由于其线程同步机制导致效率较低,在现代 Java 开发中一般不建议使用。
适用场景分析
- ArrayList:如果不考虑线程安全因素,在大多数单线程或可以通过外部同步机制保证线程安全的场景下,使用
ArrayList
能获得更高的性能和效率。 - Vector:在必须保证线程安全的多线程环境中,且对性能要求不是特别苛刻时,可以考虑使用
Vector
。另外,当集合中元素的数目可能会大幅超过目前集合数组的长度,且需要处理大量数据时,由于Vector
的扩容倍数相对较大,可能在一定程度上减少扩容操作的次数,具有一定优势。
ArrayList 和 LinkedList
数据结构与性能特点
- ArrayList
-
- 底层实现:基于动态数组的数据结构,内存中元素地址连续存储。
- 优势:由于地址连续,查询操作可通过索引直接定位,时间复杂度为 O (1),数据检索效率极高。例如,在需要频繁读取元素的场景(如分页查询、数据遍历统计)中表现优异。
- 不足:插入和删除操作时,需要移动后续元素以保持连续性,时间复杂度为 O (n)。特别是在数组中间位置进行操作时,性能损耗明显。
- LinkedList
-
- 底层实现:采用双向链表结构,每个节点包含数据及前后节点引用,内存地址不连续。
- 优势:新增和删除操作仅需修改节点引用,无需移动大量数据,时间复杂度为 O (1) 。在频繁进行数据增删(如聊天消息列表、任务队列)或需要在头尾快速操作的场景中表现出色。
- 不足:查询元素时需从链表头或尾开始遍历,时间复杂度为 O (n),检索性能较差。
适用场景
- ArrayList:适用于以查询、遍历操作为主的场景,如数据展示、报表生成等,追求快速读取数据的需求。
- LinkedList:更适合频繁进行插入、删除操作的场景,如实时消息推送、任务调度队列等,对数据动态变更要求较高的场景。
共性与差异
- 相同点:
-
- 元素特性:均支持元素有序存储且允许重复。
- 线程安全:都属于非同步操作,在单线程环境下执行效率较高,多线程环境中需额外同步机制保障数据安全。
- 不同点:
-
- 底层结构:ArrayList 基于数组,LinkedList 基于双向链表,这一本质差异决定了二者性能表现的不同。
- 操作效率:ArrayList 擅长查询,LinkedList 则在增删操作上具备明显优势。
ArrayList
、LinkedList
、Vector
对比
ArrayList
、LinkedList
和 Vector
都是实现了 List
接口的集合类
底层数据结构
- ArrayList:底层采用可变长度的数组来存储元素。数组在内存中是连续存储的,这使得它能够通过索引快速定位元素。
- LinkedList:使用双向非循环链表作为底层数据结构。链表中的每个节点包含数据以及指向前一个节点和后一个节点的引用。
- Vector:同样基于可变长度的数组实现,其数据存储方式与
ArrayList
类似。
性能特点
- 查询操作:
-
ArrayList
和Vector
由于底层是数组,通过索引访问元素的时间复杂度为 O(1),因此查询速度较快。LinkedList
要查询元素,需要从头节点或尾节点开始遍历链表,时间复杂度为 O(n),查询速度相对较慢。
- 增删操作:
-
ArrayList
和Vector
在进行增删操作时,可能需要移动大量元素,尤其是在数组中间或开头进行操作时,时间复杂度为 O(n),效率较低。LinkedList
只需修改节点的引用,在链表的头部或尾部进行增删操作的时间复杂度为 O(1),在中间位置进行增删操作的平均时间复杂度为 O(n),总体来说增删效率较高。
线程安全性与效率
- ArrayList:所有方法都是非同步操作,不保证线程安全。但在单线程环境下,由于无需进行线程同步的开销,因此效率较高。
- LinkedList:同样是非线程安全的,在单线程环境中操作效率高。
- Vector:所有方法都是同步操作,保证了线程安全。然而,线程同步会带来额外的开销,导致其在性能上低于
ArrayList
和LinkedList
,效率较低。
扩容机制
- ArrayList:在需要扩容时,新数组的长度为原数组长度的 1.5 倍。
- Vector:每次扩容时,新数组长度为原数组长度的 2 倍,扩容成本相对较高。
元素存储特点
这三个集合类都允许存储重复元素。
适用场景
- ArrayList:适用于需要频繁进行查询操作,而增删操作较少的场景。
- LinkedList:适合增删操作较多,查询操作相对较少的场景。
- Vector:在多线程环境下,若需要保证线程安全,可以使用
Vector
,但要考虑其性能开销。
List和Set的区别
在 Java 集合框架中,List
和 Set
均继承自 Collection
接口,它们在元素存储特性、操作性能和遍历方式等方面存在显著差异,而 Map
接口则独立于 Collection
体系,用于存储键值对,与 List
、Set
属于不同类型的集合。
List 特点
- 元素有序性:
List
中的元素严格按照放入顺序进行存储,可通过索引(下标)访问元素,例如list.get(0)
能获取第一个添加到列表中的元素。 - 元素重复性:允许存储重复元素,即同一个对象或相等对象可以多次添加到
List
中。 - 遍历与操作:支持使用传统
for
循环(通过下标遍历)和Iterator
迭代器遍历;在数据结构上与数组类似,具备动态扩容能力。查询元素效率较高,因为可直接通过索引定位;但在进行插入和删除操作时,尤其是在列表中间位置操作,会导致后续元素的位置移动,因此效率相对较低 。
Set 特点
- 元素无序性:
Set
中的元素没有固定的放入顺序,其内部存储位置由元素的hashCode
值决定。虽然从外部看来元素是无序的,但在Set
内部,元素的存储位置相对固定。 - 元素唯一性:不允许存储重复元素,当尝试添加重复元素时,新元素会覆盖已存在的相同元素。这里判断元素是否相同,依赖于元素类正确实现
equals()
方法和hashCode()
方法 —— 只有当两个元素的equals()
方法返回true
且hashCode()
返回值相同,Set
才认定它们是重复元素。 - 遍历与操作:仅支持使用
Iterator
迭代器进行遍历,无法通过下标获取元素;由于其底层数据结构(如HashSet
基于哈希表,TreeSet
基于红黑树)的特性,在插入和删除元素时效率较高,且不会影响其他元素的位置,但检索元素时需要通过哈希计算或树结构的查找,因此效率相对较低。
3、Set(存储无序,不可重复数据)
HashSet:Set 实现类,底层是 HashMap 散列表(数组 + 链表 + 红黑树 jdk1.8及之后)。所有添加到 HashSet 中的元素实际存储到了 HashMap 的 key中。
LinkedHashSet:HashSet 子类,使用 LinkedHashMap 来存储它的元素,存储的值插入到 LinkedHashMap 的可以 key中,底层实现(数组 + 链表 + (红黑树 jdk1.8及之后)+ 链表),可以记录插入的顺序
TreeSet:Set 实现类,底层是 TreeMap(红黑树实现),存入到 TreeSet 中的元素,实际存储到了 TreeMap 中,根据存储元素的大小可以进行排序。
HashSet
特点
- 底层结构:基于哈希表实现,本质上是使用
HashMap
来存储元素,元素存放在HashMap
的key
部分。 - 元素特性:元素无序且唯一,允许存储
null
元素。 - 线程安全:线程不安全,不过在单线程环境下存取效率高。
- 初始化参数:默认初始化容量为 16,加载因子为 0.75。
唯一性保证机制
HashSet
通过元素的 hashCode()
和 equals()
方法来保证元素的唯一性。具体比较过程如下:
- 当向
HashSet
中添加元素时,首先调用该元素的hashCode()
方法生成一个int
类型的哈希码。 - 将哈希码与当前
HashSet
的容量取模,得到元素在底层数组中的存储位置。 - 若该位置没有元素,则直接存储该元素。
- 若该位置已有元素,比较两个元素的
hashCode
值:
-
- 若
hashCode
不相等,则认为两个元素不同,使用解决地址冲突的算法(如链表法)存储新元素。 - 若
hashCode
相等,再调用equals()
方法比较两个元素的内容:
- 若
-
-
- 若
equals()
返回true
,则认为是同一个元素,不进行存储。 - 若
equals()
返回false
,则使用解决地址冲突的算法存储新元素。
- 若
-
hashCode () 与 equals () 的相关规定
- 相等与 hashcode 关系:若两个对象相等,那么它们的 hashcode 值必定相同。但反之,两个对象具有相同的 hashcode 值,它们却不一定相等。因为 hashcode 值只是一种哈希映射结果,可能存在不同对象哈希值相同的哈希冲突情况。
- 对象相等的判定:当两个对象相等时,调用它们的 equals 方法会返回 true。
- 方法覆盖的关联性:若一个类中 equals 方法被覆盖重写,为保证哈希相关操作的正确性和一致性,hashCode 方法也必须被覆盖重写 。这是因为 HashSet 等基于哈希原理的数据结构依赖这两个方法共同判断对象的唯一性和相等性。
- hashCode () 默认行为:hashCode () 方法的默认行为是针对堆上的对象生成独特值。若一个类未重写 hashCode () 方法,那么即便两个对象指向相同的数据内容,在哈希比较中它们也不会被判定为相等。因为默认的 hashCode () 实现未考虑对象数据内容,只是基于对象的内存地址等因素生成哈希值。
代码示例
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
public class TestHashSet {
public static void main(String[] args) {
Set<Integer> set = new HashSet<>();
// 添加元素
set.add(1);
set.add(2);
// 获取元素个数
int size = set.size();
// 判断是否包含指定元素
boolean contains = set.contains(1);
ArrayList<Integer> list = new ArrayList<>();
list.add(3);
list.add(4);
list.add(4);
// 将其他集合中的元素添加到 set 集合中
set.addAll(list);
// 删除指定元素
set.remove(2);
// 遍历元素
for (Integer integer : set) {
System.out.println(integer);
}
}
}
LinkedHashSet
特点
- 底层结构:采用链表和哈希表共同实现,内部基于
LinkedHashMap
。 - 元素特性:元素唯一,且元素的顺序与存储顺序一致。
- 线程安全:线程不安全,效率较高。
TreeSet
核心特性
- 底层结构:基于红黑树(一种自平衡的排序二叉树)实现,本质上是通过
TreeMap
存储元素,元素作为TreeMap
的key
部分 。 - 元素特性:具有 有序性、唯一性,不允许存储
null
值。有序性使其主要用于数据排序场景,唯一性确保集合中不存在重复元素。 - 线程安全:与
HashSet
、LinkedHashSet
一样,TreeSet
属于线程不安全的集合,在单线程环境下运行效率较高。
排序机制
- 自然排序:
-
- 当使用无参构造方法创建
TreeSet
时,默认采用自然排序规则。 - 要求集合中存储的元素必须实现
Comparable
接口,并覆写compareTo()
方法。TreeSet
通过compareTo()
方法返回的int
值判断元素顺序,返回 0 时认定为重复元素,不再存储。例如,对于自定义类Person
,若要实现自然排序,需按年龄、姓名等规则覆写compareTo()
方法。
- 当使用无参构造方法创建
- 比较器排序:
-
- 使用有参构造方法创建
TreeSet
时,可传入实现Comparator
接口的比较器对象,或通过匿名内部类重写compare()
方法来自定义排序规则。这种方式灵活性更高,适合对排序逻辑有特殊要求的场景,如倒序排列,或根据元素的非主要属性进行排序。
- 使用有参构造方法创建
唯一性保障
虽然红黑树结构本身能辅助维护元素顺序,但 TreeSet
仍需依赖元素类正确实现 hashCode()
和 equals()
方法,来确保唯一性。若未正确重写这两个方法,可能导致相同元素被错误地多次插入。
应用场景
相比 HashSet
(侧重高效存取,元素无序)和 LinkedHashSet
(保留插入顺序),TreeSet
特别适用于需要对元素进行自动排序的场景,如学生成绩排名统计、商品价格排序展示等。通过合理选择排序方式,能高效处理有序数据集合。
package com.java8;
import java.util.Set;
import java.util.TreeSet;
public class TestTreeSet {
public static void main(String[] args) {
Set<String> all = new TreeSet<>();
all.add("hello");
all.add("123");
all.add("123");
all.add("Hello jake");
System.out.println(all);
}
}
//结果
[123, Hello jake, hello]
ArrayList 与 HashSet
(一)相同点
- 非线程安全:在多线程环境下,
ArrayList
和HashSet
都不是线程安全的,如果有多个线程同时操作集合,可能会出现数据不一致的问题,需要开发者自行进行同步处理。 - 泛型支持:都支持泛型,可以在声明时指定集合中存储元素的类型,以确保类型安全,例如
ArrayList<String>
和HashSet<Integer>
。 - 实现接口:二者都实现了
java.util.Collection
接口,因此都拥有Collection
接口定义的基本方法,如add()
、remove()
、contains()
、size()
等,可用于对集合进行常见的操作。
(二)不同点
- 数据结构
-
- ArrayList:底层是动态数组结构,在内存中元素地址连续存储。这种结构使得它可以通过索引快速访问元素,查询效率高,但插入和删除元素时,尤其是在中间位置操作,需要移动大量元素,效率较低。
- HashSet:底层基于
HashMap
实现,本质上是通过哈希表来存储元素。哈希表通过哈希函数将元素映射到数组的特定位置,若位置冲突则通过链表或红黑树(JDK 1.8 后)解决。这使得HashSet
在插入、删除和查找元素时,平均情况下具有较高的效率,但不支持通过索引访问元素 。
- 元素特性
-
- ArrayList:元素有序,即元素的存储顺序和添加顺序一致,并且允许重复元素存在,同一元素可以多次添加到集合中。
- HashSet:元素无序,其内部元素的存储位置由元素的哈希值决定,不保证元素的存储顺序;同时元素唯一,重复元素会被自动过滤,不会被存储。
- 遍历方式
-
- ArrayList:既支持使用传统的
for
循环通过下标遍历元素(for (int i = 0; i < list.size(); i++)
),也支持使用迭代器Iterator
进行遍历,还可以使用增强型for
循环。 - HashSet:只能通过
Iterator
迭代器或增强型for
循环进行遍历,由于元素无序且没有下标,无法使用传统的for
循环通过下标遍历。
- ArrayList:既支持使用传统的
- 性能表现
-
- 查询性能:对于查询操作,
ArrayList
在已知元素下标的情况下,查询时间复杂度为 O (1),速度很快;HashSet
在理想情况下,通过哈希值快速定位元素,查询时间复杂度也接近 O (1),但在哈希冲突严重时,性能会下降。 - 增删性能:在添加元素时,
ArrayList
可能需要进行数组扩容操作,平均时间复杂度为 O (1) ,但在扩容时开销较大;HashSet
添加元素时,平均时间复杂度也为 O (1) 。在删除元素时,ArrayList
删除中间位置元素需要移动后续元素,时间复杂度为 O (n);HashSet
删除元素平均时间复杂度为 O (1)。
- 查询性能:对于查询操作,
- 适用场景
-
- ArrayList:适用于需要频繁查询元素、按顺序访问元素,且插入和删除操作较少的场景,如存储学生成绩列表并进行成绩查询、展示固定顺序的数据列表等。
- HashSet:适用于需要快速判断元素是否存在、对元素进行去重处理,以及对元素顺序没有要求的场景,如统计文章中出现的不同单词、去除重复的用户 ID 等。
LinkedHashSet 与 TreeSet
(一)相同点
- 元素唯一性:
LinkedHashSet
和TreeSet
都实现了Set
接口,因此都具备元素唯一的特性,不允许集合中存储重复元素,重复元素添加时会被忽略。 - 非线程安全:二者在多线程环境下都不是线程安全的,若多个线程同时操作集合,可能引发数据不一致问题,需开发者手动进行同步控制。
- 泛型支持:都支持泛型,可以指定集合中存储元素的具体类型,保证类型安全,如
LinkedHashSet<Double>
和TreeSet<Person>
。 - 实现接口:都实现了
java.util.Set
接口及其继承的java.util.Collection
接口,因此拥有Collection
接口定义的基本操作方法,如add()
、remove()
、contains()
、size()
等。
(二)不同点
- 数据结构
-
- LinkedHashSet:底层基于
LinkedHashMap
实现,它在HashMap
的基础上,通过双向链表维护元素的插入顺序。这种结构使得它既能利用哈希表快速查找元素,又能保证元素的顺序。 - TreeSet:底层使用
TreeMap
,基于红黑树(一种自平衡的二叉排序树)实现。红黑树的特性保证了元素在插入、删除和查找操作时,时间复杂度均为 O (log n),并且能够自动对元素进行排序。
- LinkedHashSet:底层基于
- 元素顺序
-
- LinkedHashSet:元素按照插入顺序进行存储和遍历,即元素输出的顺序与添加到集合中的顺序一致,能很好地保留元素的操作时序。
- TreeSet:元素按照 ** 自然排序(实现
Comparable
接口)或自定义比较器(Comparator
)** 规则进行排序。例如,对于数值类型,会按照从小到大排序;对于自定义对象,需要实现Comparable
接口的compareTo()
方法或传入Comparator
比较器来确定排序逻辑。
- null 值支持
-
- LinkedHashSet:允许存储一个
null
元素,因为在哈希表中,null
也可以通过特定的哈希处理存储到对应的位置。 - TreeSet:不允许存储
null
值,因为红黑树在进行排序和比较操作时,null
值无法参与自然排序或比较器的比较逻辑。
- LinkedHashSet:允许存储一个
- 性能表现
-
- 查询性能:
LinkedHashSet
在查询元素时,借助哈希表结构,平均时间复杂度接近 O (1);TreeSet
由于需要在红黑树中进行查找,时间复杂度为 O (log n),在数据量较大时,LinkedHashSet
的查询性能更优。 - 增删性能:
LinkedHashSet
添加和删除元素时,平均时间复杂度为 O (1),仅需调整链表指针;TreeSet
在插入和删除元素时,需要维护红黑树的平衡,时间复杂度为 O (log n),相比之下LinkedHashSet
的增删效率更高。
- 查询性能:
- 适用场景
-
- LinkedHashSet:适用于需要保留元素插入顺序、对元素进行去重,同时又希望有较高的插入、删除和查询效率的场景,如记录用户访问网页的顺序、处理按操作顺序输入的不重复数据等。
- TreeSet:适用于需要对元素进行自动排序、范围查询(如查找某一区间内的元素)、最值查找(如获取最大值或最小值)的场景,例如统计学生成绩排名、对商品价格进行排序筛选等。
HashSet、TreeSet 与 LinkedHashSet 对比
基本特性概述
- HashSet:基于哈希表实现,无法保证元素的排列顺序,元素顺序可能发生变化,且该集合不是同步的(即线程不安全)。集合中允许存在一个
null
元素。判断元素唯一性依赖对象的hashCode()
和equals()
方法,当两个对象的hashCode
值相同且equals
方法返回true
时,会被视为重复元素,仅保留一个。 - TreeSet:是
SortedSet
接口的唯一实现类,能确保集合元素处于排序状态。支持自然排序和定制排序两种方式,默认采用自然排序。集合中不允许存储null
值,向其中添加的元素应为同一类的对象。判断两个对象是否相等,依据是equals
方法返回false
或者compareTo
方法比较结果不为 0。 - LinkedHashSet:同样依据元素的
hashCode
值确定存储位置,但使用链表维护元素的次序,使得元素看起来是以插入顺序保存的。在迭代访问集合中的全部元素时,性能优于HashSet
,不过插入时性能稍逊于HashSet
。
排序方式
- 自然排序(TreeSet):使用元素的
compareTo(Object obj)
方法比较元素大小,然后按升序排列。要求元素所属类实现Comparable
接口。 - 定制排序(TreeSet):若不想使用自然排序,可实现
Comparator
接口的int compare(T o1, T o2)
方法,自定义排序规则。
底层数据结构与性能分析
- HashSet:由于采用哈希表,插入、删除和查找元素的平均时间复杂度为 O (1)。在简单的元素存储和查找场景下,性能表现出色。
- TreeSet:基于红黑树(自平衡的二叉排序树),插入、删除和查找元素的时间复杂度为 O (log n)。虽然性能在某些操作上不如
HashSet
,但在需要频繁排序或范围操作时具有优势。
核心差异总结
- 元素顺序:
HashSet
元素无序;TreeSet
元素自动排序;LinkedHashSet
按元素插入顺序排列。 - null 值支持:
HashSet
允许一个null
元素;TreeSet
不允许null
元素;LinkedHashSet
允许一个null
元素。 - 性能表现:插入和查找操作,
HashSet
通常最快;遍历操作,LinkedHashSet
有优势;需要排序或范围操作时,TreeSet
更合适。 - 对象要求:
HashSet
依赖hashCode()
和equals()
方法;TreeSet
要求元素实现Comparable
接口或传入Comparator
比较器;LinkedHashSet
与HashSet
一样依赖hashCode()
和equals()
方法。
适用场景
- HashSet:适用于对元素顺序无要求,追求高效插入、删除和查找操作的场景,如数据去重、快速判断元素是否存在(如用户登录验证时检查用户名是否已注册)。
- TreeSet:适用于需要元素自动排序的场景,如成绩排名展示、商品价格区间筛选;也适用于元素范围查询(如获取价格在某区间内的所有商品)、最值查找(如找出最小值或最大值)的场景。
- LinkedHashSet:适用于需要保持元素插入顺序,且对遍历性能有一定要求的场景,如记录用户操作历史、网页访问记录等。
List和Set应该怎么选?
4、Queue队列
Queue 接口与实现类
- ArrayDeque:基于数组实现的双端队列,支持在队列两端高效地插入和删除元素。作为典型的 FIFO 队列,它常用于需要快速存取元素且对顺序有严格要求的场景,如任务调度队列、消息缓存等。
- LinkedList:虽然主要被用作链表数据结构,但
LinkedList
同时实现了Queue
接口,本质上也是双向链表。它可以作为队列使用,提供了灵活的节点插入和删除操作,适合处理动态变化的数据集合。
PriorityQueue(优先队列)
- 排序特性:
PriorityQueue
打破了普通队列的 FIFO 规则,队列元素按照大小进行排序。它基于数组实现,采用完全二叉树(小顶堆)结构,即任意非叶子节点的权值不大于其左右子节点的权值,确保每次从队列头部取出的元素都是当前队列中的最小值(或根据自定义比较器确定的优先级最高元素)。 - 使用限制:
PriorityQueue
不允许插入null
元素,否则会抛出NullPointerException
异常,这一限制保证了排序逻辑的有效性。
Deque 接口(双端队列)
Deque
是Queue
接口的子接口,代表双端队列,允许在队列的两端进行插入和删除操作,既可以当作队列使用,也可以模拟栈(后进先出,LIFO)的行为。在需要实现栈功能时,推荐使用ArrayDeque
,其性能通常优于Stack
类。这是因为ArrayDeque
基于数组实现,操作效率高,且避免了Stack
类中大量同步方法带来的性能开销 。
三、Map集合
在 Java 集合框架中,Map
是一种专门用于存储键值对(key-value)映射关系的数据结构。它维护着两组数据:唯一的键(key)和与之对应的值(value),二者均可为任意引用类型。凭借键的唯一性,通过特定的key
能够快速检索到对应的value
,这种设计使得数据的查询和存储效率显著提升。
Map
接口与Collection
接口相互独立,不存在继承关系。它聚焦于实现key
到value
的映射功能,开发者可通过key
便捷地进行数据的查找、更新或删除操作。此外,Map
接口提供了三种集合视图:
- key 集合:通过
map.keySet()
获取所有键的集合; - value 集合:使用
map.values()
获取所有值的集合; - key-value 映射集合:以
Entry
对象形式呈现,可通过map.entrySet()
获取完整的键值对集合。
Map
接口拥有多个重要实现类,其中HashMap
、LinkedHashMap
、TreeMap
和Hashtable
应用最为广泛,它们在数据结构、线程安全性、键值限制及元素顺序等方面各具特性:
实现类 | 底层数据结构 | 是否线程安全 | key 能否为 null | 是否有序 |
| 数组 + 链表 + 红黑树(JDK 1.8+) | 否 | 是 | 否(元素无序) |
| 数组 + 链表 + 红黑树 + 双重链接列表 | 否 | 是 | 是(按插入顺序排序) |
| 红黑树 | 否 | 否 | 是(按 key 自然 / 自定义排序) |
| 数组 + 链表 | 是 | 否 | 否(元素无序) |
- HashMap:基于哈希表实现,通过数组、链表及红黑树(JDK 1.8 后优化冲突处理)存储数据,以
Entry
类型保存key-value
键值对。它属于非线程安全类,在单线程环境下存取性能卓越,适用于对数据顺序无要求、追求高效查询与存储的场景,如缓存数据、用户信息存储等。 - LinkedHashMap:作为
HashMap
的子类,在其基础上引入双重链表结构,既能利用哈希表快速查询,又能通过链表维护元素插入顺序。适用于需要按添加顺序遍历数据的场景,例如记录用户操作日志、网页访问历史等。 - TreeMap:基于红黑树实现,自动根据
key
的自然排序(实现Comparable
接口)或自定义比较器(Comparator
)规则对元素进行排序。适合处理需要有序展示或范围查询的数据,如字典序排列的单词统计、按时间顺序排序的事件记录等。 - Hashtable:早期的
Map
实现类,采用数组加链表结构,其对外公开方法几乎均通过synchronized
关键字修饰,确保线程安全。但同步机制会带来性能损耗,在现代开发中逐渐被ConcurrentHashMap
取代,仅适用于对线程安全要求严格且对性能敏感度较低的场景。
Map主要方法
1、HashMap
HashMap是 Map 接口使用频率最高的实现类。
- 允许使用null键和null值,与HashSet一样,不保证映射的顺序。
- 所有的key构成的集合是Set:无序的、不可重复的。所以,key所在的类要重写:equals()和hashCode()
- 所有的value构成的集合是Collection:无序的、可以重复的。所以,value所在的类要重写:equals()
一个key-value构成一个entry - 所有的entry构成的集合是Set:无序的、不可重复的
HashMap 判断两个 key 相等的标准是:两个 key 通过 equals() 方法返回 true,hashCode 值也相等。
HashMap 判断两个 value相等的标准是:两个 value 通过 equals() 方法返回 true。
注:
JDK 7及以前版本:HashMap是数组+链表结构(即为链地址法)
JDK 8版本发布以后:HashMap是数组+链表+红黑树实现。
HashMap 的底层实现
JDK 1.8 之前
在 JDK 1.8 之前,HashMap 的底层采用数组与链表结合的链表散列结构。其核心工作流程如下:
- 哈希值计算与位置确定:HashMap 通过调用 key 的
hashCode
方法获取哈希值,再经过扰动函数(即hash
方法)处理得到最终的 hash 值。使用扰动函数是为了降低因hashCode
方法实现不佳而导致的哈希碰撞概率。得到 hash 值后,通过(n - 1) & hash
(其中 n 为数组长度)来确定当前元素在数组中的存放位置。 - 冲突处理(拉链法):若确定的位置已存在元素,会进一步比较该元素与待存入元素的 hash 值及 key。若两者的 hash 值和 key 都相同,则直接覆盖原元素;若不同,则采用拉链法解决冲突,即将冲突的元素以链表形式连接在该位置上,也就是在数组的对应位置创建链表,将冲突元素添加到链表中。
- 拉链法:
以下是 JDK 1.7 中hash
方法的源码,该方法对哈希值进行了多次扰动操作:
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
相较于 JDK 1.8 的hash
方法,JDK 1.7 的此方法性能稍逊,因其对哈希值扰动了 4 次。
JDK 1.8 及之后
JDK 1.8 对 HashMap 的底层实现进行了优化,在解决哈希冲突方面有显著变化:
- 哈希值计算优化:
hash
方法相比 JDK 1.7 更为简化,原理不变。源码如下:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
- 冲突处理改进:当链表长度超过阈值(默认为 8)且数组长度大于 64 时 ,会将链表转换为红黑树。这是因为链表在长度较长时,查找元素的时间复杂度为 O (n),而红黑树作为一种自平衡的二叉查找树,能将查找、插入和删除操作的时间复杂度降低至 O (log n) ,有效减少搜索时间。
此外,TreeMap、TreeSet 以及 JDK 1.8 之后的 HashMap 底层都运用了红黑树。红黑树的引入是为了克服普通二叉查找树在极端情况下(如数据有序插入)退化为线性结构的缺陷,确保数据结构在各种情况下都能保持较好的性能。
HashMap 长度为 2 的幂次方的原因
为使 HashMap 存取高效、减少碰撞,需将数据尽量均匀分配。Hash 值范围极大,约有 40 亿的映射空间,正常情况下,若哈希函数映射均匀,不易出现碰撞。然而,如此庞大的映射空间无法用数组直接存储,因此在使用 Hash 值确定元素存储位置时,需先对数组长度取模,得到的余数作为数组下标。
在 HashMap 中,数组下标的计算方式为 “(n - 1) & hash”(n 为数组长度),这一设计与 HashMap 长度为 2 的幂次方紧密相关。从算法设计角度看,我们可能先想到用 % 取余操作实现下标计算。但关键在于,当除数是 2 的幂次方时,取余(%)操作等价于与除数减一的按位与(&)操作,即hash % length == hash & (length - 1)
(length 为 2 的 n 次方)。并且,按位与(&)操作作为二进制位操作,相比 % 取余操作,运算效率更高。所以,将 HashMap 的长度设为 2 的幂次方,能在保证数据均匀分配的同时,利用高效的位运算确定元素存储位置,提升整体存取性能。
HashMap 多线程操作导致死循环的问题
在多线程环境下操作 HashMap,主要风险在于并发 Rehash 可能导致元素间形成循环链表。Rehash 是指当 Hash 表中要插入数据时,若检查发现容量超过设定阈值,就需增大 Hash 表尺寸,此时 Hash 表内所有元素都要重新计算存储位置。
在 JDK 1.8 之前的版本中,多线程并发 Rehash 时,由于线程执行顺序的不确定性,可能使元素在重新分配位置过程中形成循环链表。一旦形成循环链表,在后续查找或操作时,就可能陷入死循环。虽然 JDK 1.8 对底层实现进行了优化,解决了循环链表问题,但多线程下使用 HashMap 仍存在数据丢失等其他问题。因此,不建议在并发环境中使用 HashMap,推荐使用线程安全的 ConcurrentHashMap 。
什么是哈希冲突,如何解决哈希冲突
哈希冲突(Hash Collision),也称为哈希碰撞,是指在哈希表(Hash Table)这种数据结构中,不同的键(Key)经过哈希函数(Hash Function)计算后,得到了相同的哈希值(Hash Value),从而导致这些键映射到哈希表的同一个位置的情况。以下为你详细介绍哈希冲突的概念、产生原因和常见的解决方法:
- 产生原因:哈希函数是将任意长度的数据映射为固定长度的哈希值,但由于哈希值的范围是有限的,而键的数量可能是无限的,或者在实际应用中键的数量非常大,这就不可避免地会出现不同的键被映射到同一个哈希值的情况。即使哈希函数设计得非常优秀,能够尽量均匀地分布哈希值,也无法完全避免哈希冲突的发生。
- 解决方法
-
- 链地址法(Separate Chaining):也叫拉链法,是最常用的解决哈希冲突的方法之一。在这种方法中,哈希表的每个位置不再存储单个元素,而是存储一个链表(或其他数据结构,如红黑树)。当发生哈希冲突时,具有相同哈希值的元素将被添加到该位置的链表中。在查找元素时,首先通过哈希函数确定元素所在的位置,然后在对应的链表中顺序查找。例如,在 Java 的
HashMap
中,JDK 1.8 之前采用的就是数组加链表的方式,当链表长度超过一定阈值(默认为 8)且数组长度大于 64 时,会将链表转换为红黑树,以提高查找效率。 - 开放定址法(Open Addressing):当发生哈希冲突时,通过某种探测算法在哈希表中寻找下一个可用的位置来存储冲突的元素。常见的探测算法包括:
- 链地址法(Separate Chaining):也叫拉链法,是最常用的解决哈希冲突的方法之一。在这种方法中,哈希表的每个位置不再存储单个元素,而是存储一个链表(或其他数据结构,如红黑树)。当发生哈希冲突时,具有相同哈希值的元素将被添加到该位置的链表中。在查找元素时,首先通过哈希函数确定元素所在的位置,然后在对应的链表中顺序查找。例如,在 Java 的
-
-
- 线性探测(Linear Probing):从发生冲突的位置开始,依次向后探测下一个位置,直到找到一个空闲的位置。例如,如果键
k
的哈希值对应的位置已经被占用,就探测hash(k)+1
的位置,若仍然被占用,则继续探测hash(k)+2
的位置,以此类推。 - 二次探测(Quadratic Probing):与线性探测类似,但探测的步长不是固定的,而是按照某个二次函数的规律变化。例如,第
i
次探测的位置为hash(k) + i^2
。 - 双重哈希(Double Hashing):使用两个哈希函数。第一个哈希函数用于确定初始的哈希位置,当发生冲突时,使用第二个哈希函数计算一个额外的步长,然后按照这个步长在哈希表中进行探测。
- 线性探测(Linear Probing):从发生冲突的位置开始,依次向后探测下一个位置,直到找到一个空闲的位置。例如,如果键
-
-
- 再哈希法(Rehashing):当发生哈希冲突时,使用另一个哈希函数重新计算键的哈希值,直到找到一个空闲的位置。这种方法需要设计多个哈希函数,并且在每次冲突时都要重新计算哈希值,可能会增加计算成本。
- 建立公共溢出区:将哈希表分为基本表和溢出表两部分。当发生哈希冲突时,将冲突的元素存储到溢出表中。在查找元素时,首先在基本表中查找,如果未找到,则在溢出表中查找。这种方法实现简单,但可能会导致溢出表中的元素分布不均匀,从而影响查找效率。
选择合适的解决哈希冲突的方法取决于具体的应用场景和需求,需要综合考虑哈希表的大小、元素的数量、插入和查找操作的频率等因素。
代码示例
import java.util.HashMap;
public class TestHashMap {
public static void main(String[] args) {
HashMap<Integer, String> sites = new HashMap<>();
sites.put(1, "Google");
sites.put(2, "Runoob");
sites.put(3, "Taobao");
System.out.println(sites);
//1.访问元素
/*System.out.println(sites.get(2));*/
//2.删除元素
/*sites.remove(2);
System.out.println(sites);*/
//3.删除所有键值对
/*sites.clear();
System.out.println(sites);*/
//4.计算大小
/*System.out.println(sites.size());*/
//5.迭代 HashMap
/*for (Integer integer : sites.keySet()) {
System.out.println("key:" + integer + "value" + sites.get(integer));
}*/
/*for (String value : sites.values()) {
System.out.println(value);
}*/
//6.赋值一份 hashMap
/*Object clone = sites.clone();
System.out.println(clone);*/
/*HashMap<Integer, String> clone = (HashMap<Integer, String>)sites.clone();
System.out.println(clone);*/
//7.检查 HashMap 是否为空
/*boolean empty = sites.isEmpty();
System.out.println(empty);*/
//8.将所有键值对插入到 HashMap
/*HashMap<Integer, String> site1 = new HashMap<>();
site1.put(4, "Tom");
site1.put(5, "Wiki");
sites.putAll(site1);
System.out.println(sites);*/
//9.判断 key 是否存在,不存在则将键/值插入到 HashMap 中
/*sites.putIfAbsent(2, "Wiki");
sites.putIfAbsent(4, "Wiki");
System.out.println(sites);*/
//10.检查 hashMap 中是否存在 指定 key 对应的映射关系
/*boolean b = sites.containsKey(2);
System.out.println(b);*/
//11.检查 hashMap 中是否存在 指定 value 对应的映射关系
/*if (sites.containsValue("Taobao")) {
System.out.println("Taobao存在于 sites 中。");
}*/
//12.替换 hashMap 中指定 key 对应的 value
/*String wiki = sites.replace(2, "Wiki");
System.out.println(wiki);
System.out.println(sites);*/
/*boolean replace = sites.replace(2, "Runoob", "Wiki");
System.out.println(replace);
System.out.println(sites);*/
//13.将 hashMap 中所有映射关系替换成给定的函数所执行的结果
/*sites.replaceAll((key, value) -> value.toUpperCase());*/
/*System.out.println(sites);*/
//14.获取指定的 key,不存在返回默认值
/*String wiki = sites.getOrDefault(2, "Wiki");
System.out.println(wiki);*/
//15.forEach()方法的使用
/*sites.forEach((key, value) -> {
value = value + "--HAHA";
System.out.println(key + "=" + value + "");
});*/
//16.返回映射中包含的映射的 set 视图
/*System.out.println(sites.entrySet());*/
//17.获取映射 中所有 key 组成的 set 视图
/*System.out.println(sites.keySet());*/
//18.获取映射 中所有 value 组成的 set 视图
/*System.out.println(sites.values());*/
//19.merge 先判断指定的 key 是否存在,如果不存在,则添加键值对到 hashMap 中
/*String wiki = sites.merge(4, "Wiki", (oldVaule, newValue) -> oldVaule + newValue);
System.out.println(wiki);
System.out.println(sites);*/
//20.compute() 方法对 hashMap 中指定的 key 的值进行重新计算
/*String compute = sites.compute(3, (key, value) -> value + "HAHA");
System.out.println(compute);
System.out.println(sites);*/
//21.对 hashMap 中指定的 key 值进行重新计算,如果不存在这个 key,则添加到 hashMap 中
/*String s = sites.computeIfAbsent(4, key -> "Wiki");
System.out.println(s);
System.out.println(sites);*/
//22.对 hashMap 中指定的 key 的值进行重新计算,前提 是该 key 存在于 hashMap 中
String s = sites.computeIfPresent(3, (key, value) -> value + "HAHAHA");
System.out.println(s);
System.out.println(sites);
}
}
LinkedHashMap
LinkedHashMap继承了HashMap,是Map接口的哈希表和链接列表实现,它维护着一个双重链接列表,此链接列表定义了迭代顺序,该迭代顺序可以是插入顺序或者是访问顺序。
2、HashTable
- Hashtable是早期的字典实现类,可以方便的实现数据的查询
- Hashtable和HashMap从存储结构和实现来讲有很多相似之处,不同的是它承自Dictionary类,而且是线程安全的,另外Hashtable不允许key和value为null,并发性不如ConcurrentHashMap。
- Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换。
import java.util.Hashtable;
import java.util.Map;
public class TestHashtable {
public static void main(String[] args) {
Map<Integer, String> map = new Hashtable<Integer, String>();
map.put(1, "1");
map.put(2, "2");
System.out.println(map);
}
}
//结果
{2=2, 1=1}
Properties
- Properties 类是 Hashtable 的子类,该对象用于处理属性文件
- 由于属性文件里的 key、value 都是字符串类型,所以 Properties 里的 key 和 value 都是字符串类型
- 存取数据时,建议使用setProperty(String key,String value)方法和getProperty(String key)方法
3、TreeMap
Map集合的主要功能是依据key实现数据的查询需求,为了方便进行key排序操作提供了TreeMap集合,
TreeMap实现SortMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序(自然顺序),也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。
public class TestTreeMap {
public static void main(String[] args) {
Map<Integer, String> treeMap = new TreeMap<>();
treeMap.put(2, "bb");
treeMap.put(1, "aa");
treeMap.put(3, "cc");
for (Map.Entry<Integer, String> entry : treeMap.entrySet()) {
System.out.println(entry.getKey() + ":" + entry.getValue());
}
}
}
4、遍历Map
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
public class Test {
public static void main(String[] args) {
Map<String, String> map = new HashMap<String, String>();
map.put("first", "linlin");
map.put("second", "好好学java");
map.put("third", "sihai");
map.put("first", "sihai2");
// 第一种:通过Map.keySet遍历key和value
System.out.println("===================通过Map.keySet遍历key和value:===================");
for (String key : map.keySet()) {
System.out.println("key= " + key + " and value= " + map.get(key));
}
// 第二种:通过Map.entrySet使用iterator遍历key和value
System.out.println("===================通过Map.entrySet使用iterator遍历key和 value:===================");
Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, String> entry = it.next();
System.out.println("key= " + entry.getKey() + " and value= "
+ entry.getValue());
}
// 第三种:通过Map.entrySet遍历key和value
System.out.println("===================通过Map.entrySet遍历key和value:===================");
for (Map.Entry<String, String> entry : map.entrySet()) {
System.out.println("key= " + entry.getKey() + " and value= "
+ entry.getValue());
}
// 第四种:通过Map.values()遍历所有的value,但是不能遍历键key
System.out.println("===================通过Map.values()遍历所有的value:===================");
for (String v : map.values()) {
System.out.println("value= " + v);
}
}
}
5、HashMap 和 HashTable
在 Java 集合框架中,HashMap 和 HashTable 均用于存储键值对数据,但在底层实现、线程安全性、性能表现等方面存在显著差异。
相同点
- 数据结构:底层均采用散列表实现,以数组为基础结构,通过链表处理哈希冲突(JDK 1.8 后 HashMap 在链表长度超过 8 且数组容量大于 64 时,将链表转换为红黑树,优化查找效率)。
- 元素顺序:键值对存储均为无序状态,无法保证插入顺序与遍历顺序一致。
- key 要求:键对象必须实现
hashCode()
和equals()
方法,通过哈希值计算存储位置,并依赖equals()
方法判断键的唯一性。 - 可变对象限制:均不建议使用可变对象作为 key。若 key 在存储后被修改,会导致哈希值变化,进而无法通过原 key 正确获取对应 value。
不同点
- 线程安全性
-
- HashMap:非线程安全,多线程环境下同时读写可能引发数据不一致问题。如需线程安全,可通过
Collections.synchronizedMap
方法包装,或使用ConcurrentHashMap
。 - HashTable:线程安全,其大部分方法使用
synchronized
关键字修饰,确保多线程环境下数据一致性,但同步机制会带来性能开销。
- HashMap:非线程安全,多线程环境下同时读写可能引发数据不一致问题。如需线程安全,可通过
- 空值处理
-
- HashMap:允许一个
null
键和多个null
值。null
键在哈希表中通过特殊逻辑处理,新插入的null
键值对会覆盖原有值。 - HashTable:不允许
null
键或null
值,若尝试插入则抛出NullPointerException
异常。
- HashMap:允许一个
- 性能表现
-
- HashMap:单线程环境下性能更优,无同步开销,适用于对效率要求高且无需线程安全的场景。
- HashTable:因同步机制导致性能损耗,在现代开发中已逐渐被淘汰,仅适用于对线程安全要求严格且对性能敏感度较低的遗留系统。
- 容量策略
-
- 初始容量:HashMap 默认初始容量为 16,HashTable 为 11。
- 扩容机制:HashMap 扩容后容量为原容量的 2 倍;HashTable 扩容后容量为原容量的
2n + 1
。当手动指定容量时,HashMap 会将其调整为大于等于该值的最小 2 的幂次方(通过tableSizeFor()
方法实现),而 HashTable 直接使用指定值。
- 底层优化:JDK 1.8 后的 HashMap 引入红黑树优化哈希冲突,当链表长度超过阈值(默认 8)且数组容量大于 64 时,自动将链表转换为红黑树,提升查找效率;HashTable 未采用此优化机制。
适用场景建议
- HashMap:适用于单线程环境或可自行保证线程安全的场景,如缓存系统、本地数据存储等,追求高效的插入、删除和查找操作。
- HashTable:由于性能劣势,在现代开发中不推荐使用,多线程场景下优先选择
ConcurrentHashMap
。
6、HashMap 和 HashSet 区别
接口实现与存储内容
- HashMap:实现了
Map
接口,用于存储键值对(key-value pairs)。它通过键来唯一标识每个值,允许null
键(最多一个)和多个null
值 。例如,可以将学生姓名作为键,成绩作为值存储在 HashMap 中,方便通过姓名快速查询成绩。 - HashSet:实现了
Set
接口,仅用于存储对象,且对象具有唯一性,不允许重复元素。比如,要存储一个班级里不重复的学生姓名,就可以使用 HashSet 。
元素添加方法
- HashMap:通过
put()
方法向集合中添加键值对。例如,map.put("name", "Alice");
,将键为 "name",值为 "Alice" 的键值对添加到 HashMap 中。 - HashSet:使用
add()
方法添加元素。如set.add("Bob");
,把元素 "Bob" 添加到 HashSet 中。
哈希值计算与元素唯一性判断
- HashMap:使用键(Key)来计算
hashCode
值,通过键的hashCode
和equals
方法确定键值对在哈希表中的存储位置和唯一性。当两个键的hashCode
相等且equals
方法返回true
时,视为同一个键,新值会覆盖旧值。 - HashSet:使用存储的成员对象来计算
hashCode
值。由于不同对象的hashCode
可能相同(哈希冲突) ,所以除了依赖hashCode
,还通过equals
方法来判断对象的相等性,只有hashCode
相等且equals
方法返回true
的对象,才被视为重复元素,不会被添加到集合中。
底层实现关系
从源码角度来看,HashSet 底层是基于 HashMap 实现的。HashSet 中除了clone()
、writeObject()
、readObject()
等少量方法是自身实现外,其他方法大多直接调用 HashMap 中的方法。实际上,HashSet 在存储元素时,是将元素作为键,值使用一个固定的占位对象(在 HashMap 中表现为PRESENT
)存储在 HashMap 中,从而实现了元素的存储和唯一性控制 。
总之,在实际应用中,如果需要存储键值对并通过键快速查找值,应选择 HashMap;如果只需存储不重复的对象,HashSet 则是更合适的选择。
7、HashMap、LinkedHashMap 与 TreeMap 对比
在 Java 集合框架中,LinkedHashMap、HashMap 和 TreeMap 都是用于存储键值对的 Map 接口实现类,但它们在底层结构、元素顺序、性能及适用场景等方面存在差异。
底层实现与特性
- HashMap:
-
- 基于哈希表实现,在 JDK 8 及以后版本,底层采用数组 + 链表 + 红黑树的数据结构。数组作为基础存储,每个元素是一个桶(bucket)。发生哈希冲突时,相同哈希值元素以链表存于桶中,链表长度超 8 且数组长度大于 64 时转为红黑树,提升查找效率。
- 非线程安全。依赖键对象的
hashCode()
和equals()
方法确定键值对位置与唯一性,添加的键类常需重写这俩方法。可通过调整初始容量和负载因子优化空间利用,如合理设初始容量减扩容开销,调负载因子平衡空间占用与哈希冲突 。允许一个null
键和多个null
值 。插入、查找和删除操作平均时间复杂度为 O (1),但最坏情况(元素都在同一链表)退化为 O (n) 。
- LinkedHashMap:
-
- 继承自 HashMap,在其基础上加双向链表,用于维护元素插入顺序或访问顺序 ,将所有键值对连接起来,实现按特定顺序遍历。底层同样是数组 + 链表 + 红黑树(JDK 1.8 及之后)结构。
- 非线程安全。具备 HashMap 高效查询特性,同时能保证元素有序。插入、查找和删除操作时间复杂度为 O (1) ,因需维护链表,会额外消耗空间和时间。支持插入排序和按访问顺序排序(最近使用的移到尾部 ),可通过构造函数
new LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)
设置,accessOrder
为true
代表访问顺序,false
代表插入顺序 。
- TreeMap:
-
- 基于红黑树(自平衡的二叉排序树)实现 ,每个节点存一个键值对,依键的自然顺序或指定比较器顺序排序。
- 非线程安全。利用红黑树特性自动排序键值对,可根据键的自然排序(键类实现
Comparable
接口)或自定义比较器(传入Comparator
对象)确定顺序,无调优选项。插入、查找和删除操作时间复杂度为 O (log n) ,因操作需树的平衡调整,保证树高在 O (log n) 级别。不允许null
键 。
与 Hashtable 的差异
- HashMap:去除 Hashtable 的
contains
方法,新增containsValue()
和containsKey()
方法,功能更细化。非同步,单线程环境效率高于 Hashtable,允许键或值为null
,但键最多一个null
。 - Hashtable:同步,线程安全,多线程环境保证数据一致性,但同步机制带来性能损耗,不允许键或值为
null
。
适用场景
- HashMap:适用于对元素顺序无要求,需频繁插入、删除和定位元素场景,像缓存系统存临时数据,利用其快速查找和插入特性处理数据操作 。例如在网页缓存中,快速存储和获取网页片段数据。
- LinkedHashMap:适用于需保持元素插入顺序或访问顺序场景,如记录用户操作历史、网页访问记录,也可用于实现 LRU(最近最少使用)缓存 。比如记录用户浏览网页顺序,方便分析用户行为路径。
- TreeMap:适用于按自然顺序或自定义顺序遍历键的场景,如统计学生成绩排名(以学生姓名为键、成绩为值,自动排序后按序输出信息 )、按价格排序商品等范围查询或最值查找场景 。
LinkedHashMap 与 TreeMap 异同
- 相同点:元素均有序。
- 不同点:
-
- 底层实现:LinkedHashMap 基于数组 + 链表 + 红黑树(JDK 1.8 及之后)与链表实现;TreeMap 基于红黑树实现。
- 顺序保证:LinkedHashMap 通过链表保证插入顺序或访问顺序;TreeMap 通过红黑树保证键值大小顺序。
8、ConcurrentHashMap 和 Hashtable 的区别
- 底层数据结构
-
- ConcurrentHashMap:JDK 1.7 时,底层采用分段的数组 + 链表结构,即将整个桶数组分割成分段(Segment),每个 Segment 类似一个小的 HashMap,包含一个 HashEntry 数组,用于存储键值对数据 。JDK 1.8 则摒弃了 Segment 概念,采用与 HashMap 1.8 相同的结构,即数组 + 链表 / 红黑树。当链表长度超过阈值(默认为 8)且数组长度大于 64 时,链表会转换为红黑树,以提升查找效率。
-
- Hashtable:与 JDK 1.8 之前的 HashMap 底层数据结构类似,采用数组 + 链表形式。数组是主体结构,链表用于解决哈希冲突,即当不同键的哈希值相同时,将这些键值对以链表形式存储在数组的同一位置 。
- 线程安全实现方式
-
- ConcurrentHashMap:JDK 1.7 时使用分段锁技术,每个 Segment 是一个可重入锁(ReentrantLock),多线程访问不同 Segment 的数据时,不会产生锁竞争,从而提高并发访问率 。JDK 1.8 摒弃分段锁,采用 synchronized 和 CAS(Compare - And - Swap,比较并交换)操作实现并发控制 。对链表头部节点使用 synchronized 同步代码块加锁,结合 CAS 操作进行一些原子性的更新操作,提升了并发性能。
- Hashtable:使用 synchronized 关键字修饰相关方法(如 get、put 等),相当于给整个哈希表加了一把大锁。这意味着同一时刻只有一个线程能访问 Hashtable 进行读写操作,其他线程只能阻塞或轮询等待,在高并发场景下锁竞争激烈,效率低下 。
- 性能表现
-
- ConcurrentHashMap:通过分段锁(JDK 1.7)或更细粒度的锁与 CAS 操作(JDK 1.8),减少了锁的竞争,允许多个线程同时访问和修改不同部分的数据,在高并发环境下具有优秀的性能表现,能有效提升并发访问效率 。
- Hashtable:由于使用全局锁,当多线程并发访问时,锁竞争会随着线程数增加而愈发激烈,导致大量线程阻塞等待,性能显著下降,尤其在高并发场景下效率极低 。
- 迭代时的并发修改支持
-
- ConcurrentHashMap:支持在迭代过程中进行并发修改,不会抛出
ConcurrentModificationException
异常。它采用了一些特殊的数据结构和算法来确保在迭代时,即使其他线程对集合进行修改,迭代操作也能正常进行 。 - Hashtable:在迭代过程中不允许进行并发修改,若其他线程在迭代期间修改集合,会抛出
ConcurrentModificationException
异常 。
- ConcurrentHashMap:支持在迭代过程中进行并发修改,不会抛出
- 对 null 键和 null 值的支持
从 Java 8 开始,ConcurrentHashMap 和 Hashtable 都不允许存储 null 键和 null 值,若尝试插入会抛出NullPointerException
异常 。 - 扩容机制
-
- ConcurrentHashMap:采用渐进式扩容机制。在扩容时,不是一次性将所有数据迁移到新数组,而是逐步进行,每次只迁移部分数据,减少扩容时的锁争用和性能影响 。
- Hashtable:扩容时需要一次性锁定整个数据结构,将所有元素重新计算哈希值并迁移到新的数组中,直到扩容完成才释放锁。在高并发环境下,这种方式可能导致严重的性能下降 。
- 内存开销
-
- ConcurrentHashMap:由于需要维护锁(如分段锁中的 ReentrantLock )和 CAS 操作相关的数据结构(如 Atomic 变量 ),相对来说内存开销较大 。
- Hashtable:内存开销相对较小,但由于其性能瓶颈明显,在现代高并发应用中已较少使用 。
- 出现时间与版本
-
- ConcurrentHashMap:在 JDK 1.5 中引入,目的是解决 Hashtable 在高并发环境下性能不佳的问题 。
- Hashtable:是 Java 中较早的线程安全的 Map 实现,出现在 JDK 1.0 中 。
总体而言,ConcurrentHashMap 在高并发场景下表现更优,是现代 Java 开发中处理高并发读写操作的首选;而 Hashtable 由于其性能缺陷,仅适用于低并发或简单同步需求的场景,在现代开发中已逐渐被取代。
HashTable:
JDK1.7的ConcurrentHashMap:
8、ConcurrentHashMap线程安全的具体实现方式/底层具体实现
JDK 1.7 的分段锁机制
在 JDK 1.7 版本中,ConcurrentHashMap 采用 分段锁(Segmented Locking)策略实现线程安全,其核心结构由Segment
数组和HashEntry
数组组成:
- 数据结构:
-
Segment
继承自ReentrantLock
,本质上是可重入锁,充当锁的角色。每个Segment
对应一个独立的HashEntry
数组,用于存储键值对数据,类似于小型的 HashMap。- 整个 ConcurrentHashMap 包含一个
Segment
数组,通过分段将数据分散存储,每个Segment
守护其内部HashEntry
数组的元素。
- 锁机制:当线程访问 ConcurrentHashMap 时,仅需锁定对应的
Segment
,而非整个集合。例如,多个线程可以同时访问不同Segment
中的数据,避免锁竞争。当对HashEntry
数组进行插入、修改等操作时,必须先获取对应Segment
的锁,确保同一时间只有一个线程能操作该段数据,从而实现线程安全。
JDK 1.8 的 CAS 与细粒度锁优化
JDK 1.8 对 ConcurrentHashMap 进行了重大改进,摒弃了Segment
分段锁,转而采用CAS(Compare - And - Swap)和synchronized
结合的方式,数据结构与 HashMap 1.8 类似,采用数组 + 链表 / 红黑树:
- 数据结构升级:当链表长度超过阈值(默认为 8)且数组长度大于 64 时,链表会转化为红黑树,提高查找效率。
- 并发控制:
-
- CAS 操作:用于无锁的原子性更新,例如在插入新元素时,通过 CAS 尝试将新节点直接放入数组指定位置,避免不必要的锁竞争。
- synchronized 锁:仅锁定当前链表或红黑树的首节点。当多个线程访问不同链表或红黑树时,即使哈希冲突,只要操作的不是同一首节点,就不会产生锁竞争。这种细粒度的锁定方式大幅提升了并发性能,相比 JDK 1.7 进一步减少了锁的粒度和争用开销。
通过这一系列优化,JDK 1.8 的 ConcurrentHashMap 在保证线程安全的同时,显著提升了高并发场景下的读写性能,成为 Java 多线程编程中高效的键值对存储解决方案。
9、comparable 和 Comparator的区别
- 接口定义包路径:
-
Comparable
接口位于java.lang
包中,是 Java 语言的核心接口之一,意味着它在 Java 程序中无需额外导入即可使用。Comparator
接口则在java.util
包中,使用时需要显式导入java.util.Comparator
。
- 方法签名:
-
Comparable
接口只有一个抽象方法compareTo(Object obj)
。该方法用于将当前对象与参数对象obj
进行比较,返回一个整数值。若返回值小于 0,表示当前对象小于参数对象;返回值等于 0,表示两个对象相等;返回值大于 0,表示当前对象大于参数对象。通过实现该方法,类可以定义自身对象的自然排序规则。Comparator
接口的核心方法是compare(Object obj1, Object obj2)
。它用于比较两个参数对象obj1
和obj2
,同样返回一个整数值,含义与compareTo
方法返回值类似。Comparator
接口提供了一种外部定义比较逻辑的方式,允许在不修改对象所属类的情况下,对对象进行自定义排序。
- 使用场景:
-
- 当一个类的对象具有内在的自然排序顺序,且希望在各种集合(如
TreeSet
、TreeMap
)中按照该顺序进行排序时,类应实现Comparable
接口。例如,String
类实现了Comparable
接口,使得String
对象可以按照字典序进行排序。 - 当需要为没有实现
Comparable
接口的类定义排序逻辑,或者希望在不同情况下为同一类对象提供不同的排序方式时,使用Comparator
接口更为合适。比如,在对一个自定义的Person
类对象进行排序时,可以创建一个实现Comparator
接口的比较器类,根据Person
的年龄、姓名等不同属性进行排序。
- 当一个类的对象具有内在的自然排序顺序,且希望在各种集合(如
综上所述,Comparable
接口适用于定义对象的自然排序,而Comparator
接口则提供了更灵活的外部比较方式,开发者可根据具体需求选择使用。
10、Java 快速失败(fail-fast)与安全失败(fail-safe)机制详解
快速失败(fail-fast)机制
快速失败机制主要作用于集合迭代过程。当使用迭代器遍历集合(如List
、Set
、Map
)时,若在遍历期间对集合进行结构修改(新增、删除元素或改变元素关联关系),迭代器会立即抛出ConcurrentModificationException
异常,中断遍历过程。
实现原理:迭代器内部维护一个modCount
变量,该变量记录集合结构修改的次数(如添加、删除操作)。每次调用迭代器的hasNext()
或next()
方法时,迭代器会检查当前集合的modCount
值是否与迭代器初始化时记录的expectedModCount
值相等。若不相等,说明集合在遍历过程中被修改,立即抛出异常。不过,若集合修改时modCount
的变化恰好使二者再次相等(这种情况概率极低),则不会触发异常,因此该机制不能作为可靠的并发控制手段,仅适用于检测潜在的并发修改错误。
应用场景:java.util
包下的集合类(如ArrayList
、HashSet
、HashMap
)均采用快速失败机制。这意味着在多线程环境下,若一个线程进行迭代遍历,另一个线程同时修改集合,会导致异常,故此类集合不适合多线程并发修改场景。
安全失败(fail-safe)机制
安全失败机制通过在遍历前复制原集合数据,确保迭代操作基于副本进行。因此,即使原集合在遍历过程中被修改,迭代器也不会感知到变化,从而避免抛出ConcurrentModificationException
异常,保证遍历过程的完整性。
实现原理:迭代器直接操作原集合的副本数据,与原集合的实时状态解耦。原集合的任何修改(如添加、删除元素)仅影响自身,不会反馈到副本上,使得迭代器始终按副本的初始状态进行遍历。
局限性:由于迭代器依赖副本数据,无法获取原集合在遍历期间的最新修改内容。即迭代器遍历的是开始遍历时刻的集合快照,无法反映后续变更。
应用场景:java.util.concurrent
包下的容器类(如CopyOnWriteArrayList
、ConcurrentHashMap
)采用安全失败机制,支持多线程并发访问与修改,适用于高并发场景,但需注意遍历结果可能滞后于实际数据变化。
避免快速失败的策略
- 单线程场景优化:在单线程遍历集合时,若需删除元素,应调用迭代器(如
ListIterator
)的remove()
方法,而非集合本身的remove()
方法。迭代器的删除操作会同步更新modCount
,避免触发快速失败机制,但该方法仅能删除当前遍历到的元素,无法指定删除目标。 - 多线程场景选择:在多线程环境中,使用
java.util.concurrent
包中的线程安全类替代java.util
包下的普通集合类:
-
- 用
CopyOnWriteArrayList
替代ArrayList
,写入操作通过复制底层数组实现,读取操作直接访问原数组,保证读操作的线程安全; - 用
ConcurrentHashMap
替代HashMap
,通过分段锁或 CAS 机制实现高并发下的读写操作,既保证线程安全,又具备较好的性能表现
- 用
11、Iterator 和 Enumeration 的区别
在 Java 集合框架中,Iterator
(迭代器)和Enumeration
(枚举类)是两种常用的遍历集合元素的方式,它们在接口定义、功能特性、适用场景以及对并发操作的支持等方面存在明显差异。
接口定义与功能差异
Enumeration
接口诞生于 JDK 1.0,仅包含两个函数接口:hasMoreElements()
和nextElement()
。hasMoreElements()
用于判断集合中是否还有下一个元素,nextElement()
则用于获取集合中的下一个元素。由于其接口设计,通过Enumeration
只能进行元素的读取操作,无法对集合中的数据进行修改。
Iterator
接口在 JDK 1.2 引入,包含三个函数接口:hasNext()
、next()
和remove()
。hasNext()
用于检查是否存在下一个可遍历的元素,next()
用于返回下一个元素,而remove()
方法允许在迭代过程中删除当前元素。相较于Enumeration
,Iterator
提供了更丰富的功能,不仅能够读取集合数据,还能对集合进行修改操作。
对并发操作的支持
Enumeration
本身不支持同步机制,但在Vector
、Hashtable
等 JDK 1.0 中引入的类实现Enumeration
时,添加了同步措施,以确保多线程环境下的安全性。然而,Enumeration
并不支持 fail-fast 机制。这意味着在多线程环境中,当一个线程通过Enumeration
遍历集合,另一个线程同时修改集合时,不会抛出异常,遍历可能会产生不确定的结果。
Iterator
支持 fail-fast 机制。当多个线程同时对同一个集合进行操作时,如果在迭代过程中集合的结构被修改(例如添加、删除元素),Iterator
会检测到这种变化,并抛出ConcurrentModificationException
异常,从而及时反馈集合状态的改变,防止产生不一致的结果。
适用场景
Enumeration
主要为Vector
、Hashtable
等早期集合类提供遍历接口,由于其功能相对有限且不支持 fail-fast 机制,在现代 Java 开发中使用较少。
Iterator
是为HashMap
、ArrayList
等现代集合类设计的遍历接口,因其丰富的功能和对 fail-fast 机制的支持,在处理集合遍历和可能的并发操作时更为常用和灵活,广泛应用于各种 Java 程序中。
总之,Iterator
和Enumeration
虽然都用于集合遍历,但Iterator
以其更强大的功能和对并发操作更好的支持,逐渐成为 Java 集合遍历的首选方式,而Enumeration
更多地保留在对早期 Java 类库的兼容性支持中。
四、迭代器 Iterator
1. 简介
Iterator 是一个接口,在 Java 集合框架中扮演着关键角色。几乎所有集合类的实现都在内部提供了对 Iterator 接口的实现。通过 Iterator 接口,能够实现对集合的遍历操作。
其存在具有重要意义:
- 隐藏实现细节:无论集合内部采用何种数据结构实现,都可以通过统一的 Iterator 接口进行操作,而非直接对集合内部结构进行复杂的操作。这使得开发者可以使用一套标准的 API 来处理各种不同类型的集合。
- 支持元素删除:Iterator 允许在遍历集合的过程中删除集合中的元素,为集合操作提供了更多的灵活性。
2. 实例化
每个实现了 Iterator 接口的集合类都提供了 .iterator()
方法,该方法的返回值就是一个迭代器对象。以下是一个简单的示例:
public class TestIterator {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
// 获取集合的迭代器
Iterator<Integer> iterator = list.iterator();
//.hasNext() 判断是否有下一个元素
while (iterator.hasNext()) {
// 获取下一个元素
Integer next = iterator.next();
System.out.println(next);
}
}
}
3. ConcurrentModificationException 异常
在使用增强 for 循环遍历集合的过程中,如果向集合中插入元素或删除元素,就会抛出 ConcurrentModificationException
异常。例如:
public class Test {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
for (Integer integer : list) {
list.remove(integer);
}
}
}
之所以会出现这个异常,是因为增强 for 循环将集合视为一个整体进行操作,在遍历过程中不允许对集合进行结构上的修改(增删元素)。
而使用传统的 for(int i = 0; i < list.size(); i++)
循环时,不会出现该异常。这是因为这种循环本质上是多次调用集合的 get()
方法来获取元素,在调用 get()
方法时,集合对于其他元素的删除或新增操作并没有严格限制。
4. 遍历集合时删除元素的正确方式
如果需要在遍历集合的同时删除元素,正确的做法是使用 Iterator 接口提供的 remove()
方法。示例如下:
public class Test {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
// 获取集合的迭代器
Iterator<Integer> iterator = list.iterator();
//.hasNext() 判断是否有下一个元素
while (iterator.hasNext()) {
// 获取下一个元素
Integer next = iterator.next();
// 删除当前元素
iterator.remove();
}
}
}
另外,Iterator 是 Java 集合的顶层接口之一,它为遍历任何 Collection
类型的集合提供了统一的方式。我们可以通过调用集合对象的迭代器方法来获取 Iterator 实例。Iterator 接口取代了 Java 集合框架中早期的 Enumeration
接口,并且相比 Enumeration
,Iterator 允许调用者在迭代过程中安全地移除元素。以下是一个简单的遍历示例:
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class test {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("q");
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String obj = it.next();
System.out.println(obj);
}
}
}
总之,在遍历并修改 Collection
集合时,使用 Iterator.remove()
方法是唯一正确且安全的方式。