目录
三、ArrayList,LinkedList,Vector的特性
一、LinkedList类
- LinkedHashSet 是 HashSet 的子类,继承HashSet,实现了Set接口;
- LinkedHashSet 底层是一个 LinkedHashMap,底层维护了一个 数组+双向链表;
- LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,同时使用链表维护元素的次序,这使得元素看起来是以插入顺序保存的;
- LinkedHashSet 不允许添加重复元素;
优点:插入、删除元素效率高
缺点:遍历和随机访问效率低下
适用场景:适合数据频繁的添加和删除操作,写多读少的场景
1. 初始化
无参初始化
LinkedList() 构造一个空列表。
LinkedList<String> linkedList = new LinkedList<String>();
有参初始化
LinkedList(Collection<? extends E> c):创建一个包含指定集合中的元素的LinkedList对象。集合中的元素将按照迭代器返回的顺序添加到LinkedList中。
List<String> collection = new ArrayList<>(); collection.add("Element 1"); collection.add("Element 2"); LinkedList<String> list = new LinkedList<>(collection);
2. 常用方法
LinkedList新增方法:
方法 功能说明 void addFirst(Object obj) 在链表头部插入一个元素 void addLast(Object obj) 在链表尾部添加一个元素 Object getFirst() 获取第一个元素 Object getlast)() 获取最后一个元素 Object removeFirst() 删除头元素 Object removeLast() 删除尾元素 Object peek() 获取但不移除第一个元素 Object poll() 获取并移除第一个元素
添加元素
add(E element)
:在链表末尾添加一个元素。- addAll(Collection<? extends E> c) 添加集合C 内所有元素到当前集合
- addAll(int index,Collection<? extends E> c) 从指定的位置开始,将指定 collection 中的所有元素插入到此列表中
addFirst(E element)
:在链表开头添加一个元素。(LinkedList中独有的)addLast(E element)
:在链表末尾添加一个元素。(LinkedList中独有的)LinkedList<String> linkedList = new LinkedList<String>(); // 添加元素 linkedList.add("宋江"); linkedList.add("晁盖"); linkedList.add("宋江"); linkedList.add(1, "李鑫"); linkedList.addFirst("武大郎");// LinkedList中独有的 linkedList.addLast("潘金莲"); // LinkedList中独有的 linkedList.addAll(Arrays.asList("李白", "杜甫")); linkedList.addAll(0, Arrays.asList("张三", "李四")); System.out.println("【集合中的元素为】:" + linkedList);//【集合中的元素为】:[张三, 李四, 武大郎, 宋江, 李鑫, 晁盖, 宋江, 潘金莲, 李白, 杜甫]
获取元素
get(int index)
:获取指定位置的元素。getFirst()
:获取链表的第一个元素。(LinkedList中独有的)getLast()
:获取链表的最后一个元素。(LinkedList中独有的)// 获取元素 String item1 = linkedList.get(2); String item2 = linkedList.getFirst();// LinkedList中独有的 String item3 = linkedList.getLast();// LinkedList中独有的 System.out.println("下标2对应的元素:" + item1);//下标2对应的元素:武大郎 System.out.println("首元素:" + item2);//首元素:张三 System.out.println("尾元素:" + item3);//尾元素:杜甫
修改删除
- set(int index, E element):修改指定索引位置的元素
remove(int index)
:删除指定位置的元素。removeFirst()
:删除链表的第一个元素。(LinkedList中独有的)removeLast()
:删除链表的最后一个元素。(LinkedList中独有的)// 改删 String oldValue = linkedList.set(1, "王五"); System.out.println(oldValue);//李四 String str1=linkedList.remove();// LinkedList中独有的 String str2=linkedList.removeFirst();// LinkedList中独有的 String str3=linkedList.removeLast();// LinkedList中独有的 System.out.println("删除的第一个元素为:"+str1);//删除的第一个元素为:张三 System.out.println("删除的第一个元素为:"+str2);//删除的第一个元素为:王五 System.out.println("删除最后一个元素为:"+str3);//删除最后一个元素为:杜甫 System.out.println("【集合中的元素为】:" + linkedList);//【集合中的元素为】:[武大郎, 宋江, 李鑫, 晁盖, 宋江, 潘金莲, 李白]
3. 遍历
使用for循环和get方法遍历:
for (int i = 0; i < linkedList.size(); i++) { System.out.print(linkedList.get(i)+" "); } 运行结果:武大郎 宋江 李鑫 晁盖 宋江 潘金莲 李白
使用增强型for循环遍历:
for (String string : linkedList) { System.out.print(string+" "); } 运行结果:武大郎 宋江 李鑫 晁盖 宋江 潘金莲 李白
使用迭代器(Iterator)遍历:
System.out.println("=======迭代器遍历======="); //遍历的方式2:迭代器 Iterator<String> iterator=linkedList.iterator(); while(iterator.hasNext()) { System.out.print(iterator.next()+" "); } System.out.println(); ListIterator<String> it=linkedList.listIterator(linkedList.size()); while(it.hasPrevious()) { System.out.print(it.previous()+" "); } 运行结果: =======迭代器遍历======= 武大郎 宋江 李鑫 晁盖 宋江 潘金莲 李白 李白 潘金莲 宋江 晁盖 李鑫 宋江 武大郎
4. LinkedList源码分析(⭐)
- LinkedList 实现了双向链表和双端队列的特点
- 可以添加任意元素(元素可以重复),包括null;
- 线程不安全,没有实现同步
- LinkedList底层维护了一个双向链表;
- LinkedList中维护了两个属性first和last分别指向 首节点 和 尾节点;
- 每个节点(Node对象),里面又维护了prev、next、item三个属性,其中通过prev指向前一个,通过next指向后一个节点,最终完成双向链表;所以 LinkedList的元素的添加和删除不是通过数组完成的,相对来说效率较高;
- 它的查找是分两半查找,先判断index是在链表的哪一半,然后再去对应区域查找,这样最多只要遍历链表的一半节点即可找到
- add、remove操作对于LinkedList其运行时间是O(1);get方法的调用为O(N)操作。要是使用一个增强的for循环,对于任意List的运行时间都是O(N),因为迭代器将有效地从一项到下一项推进。
数据结构:
链表有若干个Node对象组成,在每个节点里存了上一个节点和下一个节点的地址
成员变量:
// 首节点 transient Node<E> first; // 尾部节点 transient Node<E> last; // 集合容量 transient int size = 0; // 更改次数 protected transient int modCount = 0;
构造方法:
// 1.无参构造 public LinkedList() { } //3.构造一个包含指定集合的元素的列表 public LinkedList(Collection<? extends E> c) { this(); addAll(c); }
核心函数分析
LinkedList 插入元素的过程实际上就是链表链入节点的过程。
add函数
/** 在链表尾部插入元素 */ public boolean add(E e) { // 添加到末尾 linkLast(e); return true; } 说明:add函数用于向LinkedList中添加一个元素,并且添加到链表尾部。具体添加到尾部的逻辑是由linkLast函数完成的。
进入linkLast方法
添加方法默认是添加到LinkedList的尾部,首先将last指定的节点赋值给l节点,然后新建节点newNode ,此节点的前驱指向l节点,data = e , next = null , 并将新节点赋值给last节点,它成为了最后一个节点,根据当前List是否为空做出相应的操作。若不为空将l的后继指针修改为newNodw。 size +1 , modCount+1void linkLast(E e) { // 保存尾结点,l为final类型,不可更改 final Node<E> l = last; // 新生成结点的前驱为l,后继为null final Node<E> newNode = new Node<>(l, e, null); // 重新赋值尾结点 last = newNode; if (l == null) // 尾结点为空 first = newNode; // 赋值头结点 else // 尾结点不为空 l.next = newNode; // 尾结点的后继为新生成的结点 // 大小加1 size++; // 结构性修改加1 modCount++; } }
add有参
/** 在链表指定位置插入元素 */ public void add(int index, E element) { // 调用checkPositionIndex检查index位置是否合法,当index>=0并且index checkPositionIndex(index); // 判断 index 是不是链表尾部位置,如果是,直接将元素节点插入链表尾部即可 if (index == size) linkLast(element); else // 添加到链表中间, 调用node方法得到index插入位置的节点 linkBefore(element, node(index)); }
进入linkBefore方法
/** 将元素节点插入到 succ 之前的位置 */ void linkBefore(E e, Node succ) { // 先得到插入位置的前驱节点pred final Node pred = succ.prev; // 新建一个节点,前驱节点指向pred,后继节点指向插入位置的节点 final Node newNode = new Node(pred, e, succ); // 修改插入位置节点的前驱节点指向新建的节点 succ.prev = newNode; // 如果新节点是头节点,让first指向新节点 if (pred == null) first = newNode; else // 如果不是,将pred的后继节点指向新建节点 pred.next = newNode; // 节点个数+1 size++; // 并发修改次数+1 modCount++; }
5. ArrayList和LinkedList区别
ArrayList
LinkedList
获取指定元素
速度很快
需要从头开始查找元素
添加元素到末尾
速度很快
速度很快
在指定位置添加/删除
需要移动元素
不需要移动元素
内存占用
少
较大
集合 底层结构 增删的效率 改查的效率 ArrayList 可变数组 较低,数组扩容 较高 LinkedList 双向链表 较高,通过链表追加 较低
二、Vector类(了解)
Vector的底层实现是基于Object数组的,每次增加元素时都会检查当前容量是否足够,如果不够则会自动扩容,同时将原数组中的元素复制到新数组中。Vector类的主要特点如下:
- 线程安全,支持多线程并发访问;
- 可以存储任意类型对象,包括null;
- 可以自动扩容,增量为当前容量的一半;
- 提供了一些常用的操作方法,如添加、删除、修改、查找等。
在相对于ArrayList来说,Vector线程是安全的,也就是说是同步的。创建了一个向量类的对象后,可以往其中随意地插入不同的类的对象,既不需顾及类型也不需预先选定向量的容量,并可方便地进行查找。对于预先不知或不愿预先定义数组大小,并需频繁进行查找、插入和删除工作的情况,可以考虑使用向量类。向量类提供了种构造方法:
无参初始化:
// Vector:基于Object[] elementData数组来实现 // 无参初始化,初始化的容量为10,数组扩容2n Vector<String> vector1 = new Vector<String>(); vector1.add("张三");
单参的初始化:
// 单参的初始化:初始化的容量为指定的数字容量 Vector<String> vector2 = new Vector<String>(100);
双参数的初始化:
// 双参数的初始化:自定义数组的初始化容量和扩容的增长数 Vector<String> vector3 = new Vector<String>(100,20); vector3.add("张三");
一些细节
1、底层也是一个数组。 2、初始化容量:10 3、怎么扩容的? 扩容之后是原容量的2倍。 10--> 20 --> 40 --> 80 4、Vector中所有的方法都是线程同步的,都带有synchronized关键字,是线程安全的。效率比较低,使用较少了。 5、怎么将一个线程不安全的ArrayList集合转换成线程安全的呢? 使用集合工具类: java.util.Collections; java.util.Collection 是集合接口。 java.util.Collections 是集合工具类。 Collections.synchronizedList();//将及格转换为线程安全的。
Vector和ArrayList对比:
底层均是数组
无参初始化:Vector默认初始化容量为10,ArrayList默认初始化为0,首次元素添加为10
扩容:ArrayList1.5倍扩容,Vector默认是2倍扩容,也可以自定义扩容
Vector线程安全,效率低。ArrayList线程不安全,效率高
三、ArrayList,LinkedList,Vector的特性
ArrayList:动态数组,使用的时候,只需要操作即可,内部已经实现扩容机制。
- 线程不安全
- 有顺序,会按照添加进去的顺序排好
- 基于数组实现,随机访问速度快,插入和删除较慢一点
- 可以插入
null
元素,且可以重复
LinkedList:链表结构,继承了
AbstractSequentialList
,实现了List
,Queue
,Cloneable
,Serializable
,既可以当成列表使用,也可以当成队列,堆栈使用。主要特点有:
- 线程不安全,不同步,如果需要同步需要使用
List list = Collections.synchronizedList(new LinkedList());
- 实现
List
接口,可以对它进行队列操作- 实现
Queue
接口,可以当成堆栈或者双向队列使用- 实现Cloneable接口,可以被克隆,浅拷贝
- 实现
Serializable
,可以被序列化和反序列化
Vector和前面说的
ArrayList
很是类似,这里说的也是1.8版本,它是一个队列,但是本质上底层也是数组实现的。同样继承AbstractList
,实现了List
,RandomAcess
,Cloneable
,java.io.Serializable
接口。具有以下特点:
- 提供随机访问的功能:实现
RandomAcess
接口,这个接口主要是为List
提供快速访问的功能,也就是通过元素的索引,可以快速访问到。- 可克隆:实现了
Cloneable
接口- 是一个支持新增,删除,修改,查询,遍历等功能。
- 可序列化和反序列化
- 容量不够,可以触发自动扩容
- *最大的特点是:线程安全的,相当于线程安全的
ArrayList
。
异同
Java提供了许多集合类来处理和操作数据,其中ArrayList、LinkedList和Vector是常见的几种。这些集合类具有相似的功能,但在实现和性能方面存在一些区别。本文将详细介绍ArrayList、LinkedList和Vector的相同点和区别,并提供相应的源代码示例。
相同点:
- 都实现了List接口:ArrayList、LinkedList和Vector都实现了Java的List接口,因此它们都支持有序的元素集合,并且允许元素重复。
- 支持动态添加和删除元素:这三个集合类都提供了添加、删除和修改元素的方法,可以根据需要动态地调整集合的大小。
- 支持迭代:所有这些集合类都可以使用迭代器(Iterator)来遍历集合中的元素。
- 可以存储任意类型的对象:ArrayList、LinkedList和Vector都可以存储任意类型的对象,包括基本类型的包装类。
区别:
- 底层实现方式:ArrayList和Vector底层都使用数组实现,而LinkedList使用双向链表实现。这导致它们在插入和删除元素时的性能表现有所不同。ArrayList和Vector在随机访问时性能较好,而LinkedList在插入和删除元素时性能更佳。
- 线程安全性:Vector是线程安全的,它的所有方法都经过同步处理,可以在多线程环境下使用。而ArrayList和LinkedList则不是线程安全的,如果在多线程环境下使用它们,需要自行处理线程同步问题。
- 扩容方式:当集合需要扩容时,ArrayList默认增加当前容量的一半(1.5倍),而Vector默认增加当前容量的一倍(2倍)。这也是为什么在大量数据操作时,Vector的性能可能会比ArrayList略差的原因之一。
四、Stack类(了解)
Stack是Vector的一个子类,它实现标准的FILO先进后出堆栈。Stack只定义了创建空堆栈的默认构造方法。
// 栈:先进后出 FILO Stack<String> s1 = new Stack<String>();
push:入栈
// 栈:先进后出 FILO Stack<String> s1 = new Stack<String>(); // push:入栈 s1.push("赵云"); s1.push("马超"); s1.push("张飞"); s1.push("鲁班"); s1.push("zkt"); System.out.println(s1);//[赵云, 马超, 张飞, 鲁班, zkt]
获取元素:出栈pop()--获取栈顶元素并让元素出栈
// 获取元素:出栈pop()--获取栈顶元素并让元素出栈 String str1 = s1.pop(); System.out.println(str1);//zkt System.out.println(s1);//[赵云, 马超, 张飞, 鲁班]
获取元素:peek() 获取栈顶的元素不出栈
// 获取元素:peek() 获取栈顶的元素不出栈 String str2 = s1.peek(); System.out.println(str2);//鲁班 System.out.println(s1);//[赵云, 马超, 张飞, 鲁班]
全部出栈
System.out.println(s1);//[赵云, 马超, 张飞, 鲁班] while (!s1.isEmpty()) { System.out.println(s1.pop()); } System.out.println(s1.size()); 运行结果: [赵云, 马超, 张飞, 鲁班] 鲁班 张飞 马超 赵云 0
对字符串进行反转:
public static void main(String[] args) { String str = "天王盖地虎,宝塔镇河妖"; revers1(str); } private static void revers1(String str) { Stack<Character> s1 = new Stack<Character>(); for (int i = 0; i < str.length(); i++) { s1.push(str.charAt(i)); } StringBuilder sb = new StringBuilder(); while (!s1.isEmpty()) { sb.append(s1.pop()); } System.out.println(sb.toString()); } 运行结果: 妖河镇塔宝,虎地盖王天
五、Set接口
- Set接口是Collection的子接口,Set集合中的元素是无序,同时不可重复的
- 可以存放null元素
- Set接口的实现类有HashSet、LinkedHashSet、treeSet
六、HashSet
HashSet
是 Java 中的一个集合类,它实现了Set
接口。Set
是一种不允许包含重复元素的集合,而HashSet
则是Set
接口的一个具体实现。因此,HashSet
用于存储一组唯一的元素,不允许重复。
- 元素是没有重复的,而且是无序的,
- 允许有null值
- 是无序的,即不会记录插入的顺序
- 不是线程安全的, 如果多个线程尝试同时修改 HashSet,则最终结果是不确定的
1. 初始化
要使用
HashSet
,首先需要创建一个HashSet
对象。可以使用以下方式进行创建和初始化:
1.1 创建空的 HashSet
Set<String> set = new HashSet<>();
上述代码创建了一个空的
HashSet
对象,用于存储字符串类型的元素。
1.2 创建包含元素的 HashSet
Set<Integer> numbers = new HashSet<>(Arrays.asList(1, 2, 3, 4, 5));
上述代码创建了一个包含整数元素的 HashSet,并初始化了一组元素。使用 Arrays.asList() 方法将元素添加到 HashSet 中。
2. 常用方法
添加元素
要向
HashSet
中添加元素,可以使用add()
方法:HashSet<String> hs1 = new HashSet<String>(); hs1.add("鲁班"); hs1.add("小乔"); hs1.add("米莱迪"); hs1.add("韩信"); hs1.add("鲁班"); hs1.add("小乔");
添加一个集合:
//添加一个集合 hs1.addAll(Arrays.asList("庄周","瑶","成吉思汗"));
不能添加重复元素:
HashSet<String> hs1 = new HashSet<String>(Arrays.asList("菜鸟", "滴滴", "语雀")); boolean b = hs1.add("淘宝"); System.out.println("第一次添加元素:" + b);//第一次添加元素:true boolean b1 = hs1.add("淘宝"); System.out.println("第二次添加元素:" + b1);//第二次添加元素:false
// HashSet添加元素:(源码分析见后面) // 1.hashcode()判断是否存在相同的元素 // 2.如果有相同的hashcode,用equals来进行元素的比较 // 如果hashcode不同,直接添加 HashSet<Book> hashSet = new HashSet<Book>(); Book b1 = new Book("《java从入门到精通》", 243, 22.34); Book b2 = new Book("《数据结构》", 231, 20.00); Book b3 = new Book("《操作系统》", 301, 45); Book b4 = new Book("《数据结构》", 231, 20.00); System.out.println(b2.hashCode());//1475392618 System.out.println(b4.hashCode());//1475392618 System.out.println(b2==b4);//false hashSet.add(b1); hashSet.add(b2); hashSet.add(b3); hashSet.add(b4); for (Book book : hashSet) { System.out.println(book); } }
删除元素
boolean b2 = hs1.remove("淘宝"); System.out.println("移除元素淘宝是否成功:"+b2);//移除元素淘宝是否成功:true
判断元素是否存在
可以使用
contains()
方法来检查元素是否存在于HashSet
中://判断元素是否存在 boolean b =hs1.contains("瑶"); System.out.println("是否存在元素瑶:"+b);//是否存在元素瑶:true System.out.println(hs1);//[瑶, 成吉思汗, 韩信, 小乔, 鲁班, 米莱迪, 庄周]
其他方法
public void clear() 清空集合元素
public int size() 返回集合中元素的数量
hs1.clear(); System.out.println(hs1);//[] System.out.println(hs1.size());//0
3. 遍历
使用增强型 for 循环遍历
//1.foreach for (String string : hs1) { System.out.print(string+" "); } 运行结果:瑶 成吉思汗 韩信 小乔 鲁班 米莱迪 庄周
使用迭代器遍历
//2.迭代器进行遍历 Iterator<String> it = hs1.iterator(); while(it.hasNext()) { System.out.print(it.next()+" "); } 运行结果:瑶 成吉思汗 韩信 小乔 鲁班 米莱迪 庄周
4. 案例
给定的字符串
s
中找到第一个重复的字符并将其返回public class Demo03 { public static void main(String[] args) { String s = "abcdejavac"; char ch = repeatFirstChar(s); System.out.println(ch); } public static Character repeatFirstChar(String str) { // 记录首次重复的元素 Character ch = null; HashSet<Character> hs1 = new HashSet<Character>(); for (int i = 0; i < str.length(); i++) { if (!hs1.add(str.charAt(i))) {//添加失败则返回其字符 ch = str.charAt(i); break; } } return ch; } } 运行结果:a
七、LinkedHashSet
LinkedHashSet
是 Java 集合框架中的一种类,它继承自HashSet
,因此具有哈希表的查找性能,同时又使用链表维护元素的插入顺序。这意味着LinkedHashSet
具有以下两个主要特性:
- 有序性(Order):
LinkedHashSet
会保持元素的插入顺序,即元素被添加到集合中的顺序就是它们在集合中的顺序。- 唯一性(Uniqueness):与
HashSet
一样,LinkedHashSet
保证元素的唯一性,不允许重复元素。因此,
LinkedHashSet
是一个适用于需要按照插入顺序存储唯一元素的场景的理想选择特点总结
- LinkedHashSet的底层使用LinkedHashMap存储元素;
- LinkedHashSet是有序的,维护了元素的插入顺序;
- LinkedHashSet是不支持按访问顺序对元素排序的,只能按插入顺序排序。
以下基本同hashset相同
1. 初始化
要创建一个
LinkedHashSet
对象,您需要使用构造函数来初始化它。以下是几种初始化方法:
1.1 默认构造函数
LinkedHashSet<String> linkedHashSet = new LinkedHashSet<>();
这将创建一个空的
LinkedHashSet
对象,初始容量为 16,加载因子为 0.75。
1.2 指定容量和加载因子
int initialCapacity = 20; float loadFactor = 0.5f; LinkedHashSet<String> linkedHashSet = new LinkedHashSet<>(initialCapacity, loadFactor);
通过指定初始容量和加载因子,可以更精细地控制
LinkedHashSet
的性能和内存占用。
1.3 从现有集合创建
您还可以从现有的集合(如
List
或Set
)创建一个LinkedHashSet
,以便在不同集合类型之间进行转换:Set<String> existingSet = new HashSet<>(Arrays.asList("A", "B", "C")); LinkedHashSet<String> linkedHashSet = new LinkedHashSet<>(existingSet);
这将使用现有集合中的元素来初始化新的
LinkedHashSet
。
2. 常用方法
与hashset基本相同
添加:
public static void main(String[] args) { HashSet<String> hs1 = new HashSet<String>(); hs1.add("鲁班"); hs1.add("小乔"); hs1.add("米莱迪"); hs1.add("韩信"); System.out.println(hs1); LinkedHashSet<String> linkedset1 = new LinkedHashSet<String>(); linkedset1.add("鲁班"); linkedset1.add("小乔"); linkedset1.add("米莱迪"); linkedset1.add("韩信"); System.out.println(linkedset1); } 运行结果: [韩信, 小乔, 鲁班, 米莱迪] [鲁班, 小乔, 米莱迪, 韩信]
hashset 无序
LinkedHashSet 有序
3. 遍历
与hashset基本相同
4. 案例
双色球规则:双色球每注投注号码由6个红色球号码和1个蓝色球号码组成。红色球号码从1—33中选择;蓝色球号码从1—16中选择;请随机生成1注双色球号码。(要求同色号码不重复)
int blue=(int)(Math.random()*16+1); LinkedHashSet<Integer> hs1=new LinkedHashSet<Integer>(); while(hs1.size()!=6) { int red=(int)(Math.random()*33+1); hs1.add(red); } System.out.println("红色区域:"+hs1); System.out.println("蓝色区域:"+blue);
5. 使用注意事项
在使用
LinkedHashSet
时,有一些注意事项需要考虑:
LinkedHashSet
是线程不安全的,如果在多线程环境中使用,需要考虑线程同步的问题,或者考虑使用线程安全的集合类如ConcurrentLinkedHashSet
。LinkedHashSet
允许存储一个null
元素,但通常建议避免将null
作为有效元素存储,以免混淆和错误。- 当使用自定义对象作为 LinkedHashSet 元素时,需要正确实现 hashCode() 和 equals() 方法,以确保对象在集合中的唯一性和正确性。
LinkedHashSet
的性能通常是很高的,但在处理大量数据时,应注意负载因子的设置,以避免频繁的扩容操作。
LinkedHashSet
是 Java 集合框架中的一种有序、唯一元素存储的数据结构。它继承自HashSet
,因此具有哈希表的快速查找特性,并且使用链表来维护元素的插入顺序。这使得它在需要保持元素有序且唯一的情况下非常有用。在使用
LinkedHashSet
时,您可以根据需要控制容量和加载因子,以平衡性能和内存占用。同时,确保实现了自定义对象的hashCode()
和equals()
方法,以便正确地处理元素的唯一性。
八、TreeSet
TreeSet
是 Java 集合框架中的一种有序集合,它实现了Set
接口,因此具有不允许重复元素的特性。与HashSet
不同,TreeSet
使用红黑树数据结构来存储元素,这使得元素在集合中保持有序。这里需要理解两个主要特性:
- 有序性(Order):
TreeSet
中的元素按照自然排序(元素的自然顺序)或者指定的排序方式(通过比较器)排列。这意味着您可以遍历TreeSet
得到的元素是按照一定的顺序排列的。- 唯一性(Uniqueness):与
HashSet
一样,TreeSet
也保证元素的唯一性,不允许重复元素。因此,
TreeSet
是一个适用于需要有序存储唯一元素的场景的理想选择。特点总结
- 最大的特点就是一个可排序-的去重集合容器。
- 集合中的元素不保证插入顺序,而是默认使用元素的自然排序(字典排序),不过可以自定义排序器
- jdk8以后,集合中的元素不可以是null
- 集合不是线程安全
- 相对于HashSet, 性能更差
数据结构:
TreeSet实际上是TreeMap实现的,底层用到的数据结果是红黑树。当我们构造TreeSet时;若使用不带参数的构造函数,则TreeSet的使用自然比较器;若用户需要使用自定义的比较器,则需要使用带比较器的参数。
1. TreeSet 的内部实现
要深入理解
TreeSet
,我们需要了解它的内部实现机制,即红黑树。红黑树是一种自平衡二叉搜索树(Self-Balancing Binary Search Tree),它具有以下特性:
- 每个节点要么是红色,要么是黑色。
- 根节点是黑色。
- 每个叶子节点(NIL 节点,空节点)是黑色的。
- 如果一个节点是红色的,则它的两个子节点都是黑色的。
- 从任意节点到其每个叶子节点的所有路径都包含相同数目的黑色节点。
即
左根右、根叶黑、不红红、黑路同
①每个节点是黑或红②根必须黑③叶节点Nil都黑④两个红色不能相邻⑤对每一个节点,从该节点到其后代叶节点地简单路径上,包含相同数目的黑色节点
这些规则确保了树的平衡,从而保证了树的高度不会过高,使得查找、插入和删除操作的性能稳定。
在
TreeSet
中,元素被存储在红黑树的节点中,根据元素的大小关系构建树结构。这意味着,插入、删除和查找操作的时间复杂度为 O(log n),其中 n 是集合中的元素个数。由于红黑树的平衡性质,这些操作的性能是可预测的。
2. 初始化
要使用
TreeSet
,首先需要创建和初始化它。以下是一些常见的初始化方法:
2.1 默认构造函数
使用默认构造函数创建一个空的
TreeSet
对象://使用TreeSet时需要注意的事项 //1.TreeSet的泛型必须要实现Comparable接口--有比较规则 //2.如果不是Comparable类型的接口,必须要创建一个new Compaator类型的对象 //1.无参构造 TreeSet<Book> hashSet = new TreeSet<Book>();
这将创建一个初始容量为 16 的
TreeSet
,加载因子为 0.75。
2.2 指定排序方式的构造函数
可以使用带有
Comparator
参数的构造函数来指定元素的排序方式。//2.有参 public TreeSet(Comparator<? super E> comparator) TreeSet<Book> hashSet = new TreeSet<Book>(new Comparator<Book>() { @Override public int compare(Book o1, Book o2) { int page1=o1.getPage(); int page2=o2.getPage(); if(page1== page2) { return o1.getName().compareTo(o2.getName()); } return page1-page2; } });
如果自定义类没有实现comparable接口,重写comparaTo()方法,会报java.lang.ClassCastException: person cannot be cast to java.lang.Comparable,java.lang.Comparable说明自定义类需要实现comparable接口,重写comparaTo()方法
2.3 从现有集合创建
可以从现有的集合(如
List
或Set
)创建一个TreeSet
,以便在不同集合类型之间进行转换:Set<String> existingSet = new HashSet<>(Arrays.asList("A", "B", "C")); TreeSet<String> treeSetFromSet = new TreeSet<>(existingSet);
这将使用现有集合中的元素来初始化新的
TreeSet
。
3. 常用方法
与hashset基本相同
添加元素
//TreeSet定义了一个可排序的去重的集合 TreeSet<String> ts1 = new TreeSet<String>(); TreeSet<Integer> ts2 = new TreeSet<Integer>(); ts1.add("12"); ts1.add("2"); ts1.add("234"); ts1.add("21"); ts1.add("3"); ts1.add("1"); ts2.add(2); ts2.add(23); ts2.add(1); ts2.add(234); ts2.add(3); System.out.println(ts1); System.out.println("==========="); System.out.println(ts2); 运行结果: [1, 12, 2, 21, 234, 3] =========== [1, 2, 3, 23, 234]
4. 遍历
与hashset基本相同
5. TreeSet 使用注意事项
在使用
TreeSet
时,有一些注意事项需要考虑,以确保正确、高效地使用该集合。5.1. 唯一性
TreeSet
是一个有序的集合,它确保了元素的唯一性。这意味着集合中不会包含重复的元素。如果您尝试将重复元素添加到TreeSet
中,它们将被忽略。因此,如果您需要处理重复元素,可能需要考虑其他集合类型,如ArrayList
或LinkedList
。5.2. 自然顺序
TreeSet
默认按照元素的自然顺序进行排序。如果元素类型实现了Comparable
接口,它将使用compareTo
方法来确定元素之间的顺序。如果元素类型没有实现Comparable
接口,并且没有提供自定义的比较器,添加元素时可能会引发ClassCastException
。5.3. 自定义比较器
如果需要根据不同的排序规则来处理元素,可以提供自定义的比较器。自定义比较器必须实现
Comparator
接口,并在创建TreeSet
时传递给构造函数。这样,您可以控制元素的排序方式,而不仅仅依赖于自然顺序。5.4. 性能考虑
TreeSet
的插入、删除和查询操作的平均时间复杂度为 O(log n),其中 n 是集合中的元素数量。这意味着TreeSet
对于大型数据集合是高效的。然而,在某些情况下,其他数据结构,如HashSet
,可能会更快,因为它们的性能更接近于 O(1)。5.5. 并发性
TreeSet
不是线程安全的,如果多个线程同时访问和修改同一个TreeSet
实例,可能会导致不一致的结果或并发问题。如果需要在多线程环境中使用TreeSet
,请考虑使用Collections.synchronizedSortedSet
来创建一个线程安全的集合。5.6. 遍历顺序
TreeSet
的元素是按照排序顺序存储的。因此,通过迭代器或增强的 for 循环遍历时,元素的顺序是有序的。这可以用于按顺序访问元素,但请注意,这可能与元素插入的顺序不同。5.7. 空集合
TreeSet
可以包含空元素(null
),但请小心使用。如果您要在集合中包含null
元素,请确保您的比较器或元素类型不会导致意外的行为。总之,
TreeSet
是一个强大的有序集合,但在使用时需要注意其唯一性、排序方式、性能、并发性等方面的问题。根据具体需求选择合适的集合类型,并确保正确处理和操作数据以避免潜在的问题。