目录
- 0. 原生数组带来的问题
- 1. Java集合框架家族
- 2. 黑帮的帮规 — Java 集合中的 `Iterable` 接口
- 3. ArrayList 第一讲
- 4. ArrayList 第二讲
- 5. ArrayList 第三讲
- 6. LinkedList 链表
- 7. 链表基础介绍
- 9. 迭代器(Iterator)初试
- 10. fori、增强 for、迭代器的区别、注意事项和分别用途
- 11. 谈谈三者性能
- 12. `HashSet` 和 `Set` 介绍
- 13. `LinkedHashSet` 介绍
- 14. `Map` 和 `HashMap` 介绍
- 15. `Map` 的一些注意点
- 16. `Entry与Map转换Set之后遍历`
- 17. `LinkedHashMap` 和 `HashMap`
- 18. 集合框架部分总结
0. 原生数组带来的问题
这个示例展示了原生数组在使用时可能带来的一些问题,特别是 数组越界 的问题。我们通过 array[3] = 4; 强行访问了索引超出边界的位置,从而抛出了 ArrayIndexOutOfBoundsException 异常。
1. 数组的基本问题
- 固定大小:数组一旦定义大小(如
new int[SIZE]),它的长度是固定的。增删改查(CRUD)操作时,如果需要动态扩展或缩小数组,就需要手动处理数组的大小和元素移动。 - 边界问题:如果在操作数组时不小心访问了越界位置(如数组索引超出定义的范围),就会抛出
ArrayIndexOutOfBoundsException异常。比如代码中的array[3] = 4;就会抛出该异常,因为数组的有效索引是0到2,索引3超出了有效范围。
2. CRUD 操作
在数据结构中,CRUD 是四个基本操作:
- Create:增加新元素
- Retrieve:查询/读取元素
- Update:更新元素
- Delete:删除元素
对于原生数组来说,操作时通常需要手动扩容或缩减数组,并且不易进行动态增删改查的优化。如果需要对数组进行频繁的增删操作,通常会造成大量的时间和空间消耗。
3. Java Collections Framework(集合框架)
为了应对上述问题,Java 提供了 Collections Framework(集合框架),其中包括了多个 接口 和 类,可以有效地解决增删改查的复杂性和性能问题。比如:
ArrayList:可以动态调整大小的数组,可以进行灵活的增删操作。HashSet:基于哈希表实现的集合,可以快速查找、插入和删除元素。HashMap:实现了映射关系,支持按键值对存储数据,可以通过键快速查找对应的值。
这些集合类避免了传统数组带来的固定大小和越界问题,提供了更加方便和高效的增删改查操作。
4. 扩容问题
对于原生数组,扩容通常需要手动创建一个新的更大的数组,并将旧数组中的元素复制到新的数组中。比如:
// 扩容的简单例子
int[] newArray = new int[oldArray.length * 2];
System.arraycopy(oldArray, 0, newArray, 0, oldArray.length);
但是在 Java 中,ArrayList 等集合类内部已经优化了这个过程,当元素数量达到上限时,会自动扩容,避免了手动扩容的复杂性。
5. 解决方案:使用集合类
相比传统数组,使用集合类如 ArrayList 可以极大地简化数组扩容、越界、增删改查等问题。例如:
import java.util.ArrayList;
import org.junit.Test;
public class ArrayTest {
@Test
public void testArrayList() {
// 使用ArrayList,动态大小
ArrayList<Integer> list = new ArrayList<>();
list.add(1); // 增加
list.add(2);
list.add(3);
// 不会出现数组越界问题
list.add(4); // 增加更多元素
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i)); // 读取
}
// 修改元素
list.set(0, 10); // 更新
System.out.println("Updated element: " + list.get(0));
// 删除元素
list.remove(1); // 删除元素
System.out.println("After removal: " + list);
}
}
6. 总结
- 传统数组的局限性:原生数组在处理动态大小、增删改查时不够灵活,容易出现数组越界等问题。
- Java集合框架的优势:Java的集合框架提供了更为高效和便捷的方式来处理这些问题。使用
ArrayList等类可以动态调整大小并进行复杂的数据操作,而不需要手动处理数组的大小和元素的复制问题。
因此,在实际开发中,推荐使用 Java 提供的集合类来替代原生数组,除非有非常特殊的性能需求。
1. Java集合框架家族
在Java中,集合框架是一个重要的组成部分,它提供了多种数据结构用于存储、处理和操作数据。Java集合框架大致可以分为两大体系:Collection 和 Map。
1. Collection
Collection 是最基础的接口,表示一组对象的集合。它有很多常用的子接口,如:
- List:有序集合,允许重复元素(例如
ArrayList,LinkedList)。 - Set:无序集合,不允许重复元素(例如
HashSet,TreeSet)。 - Queue:队列,通常用于存储待处理的元素(例如
PriorityQueue,LinkedList)。
2. Map
Map 是一种映射,它把键映射到值,不允许键重复。常用的实现类有:
- HashMap:最常用的 Map 实现类,基于哈希表实现,允许
null键和null值。 - TreeMap:基于红黑树实现,键按自然顺序或自定义顺序排序。
- LinkedHashMap:保持元素的插入顺序,基于哈希表和双向链表实现。
示例代码:
import java.util.*;
public class CollectionExample {
public static void main(String[] args) {
// 使用List(ArrayList)
List<String> list = new ArrayList<>();
list.add("Java");
list.add("Python");
list.add("C++");
System.out.println("List: " + list);
// 使用Set(HashSet)
Set<String> set = new HashSet<>();
set.add("Java");
set.add("Python");
set.add("C++");
set.add("Java"); // 重复元素会被忽略
System.out.println("Set: " + set);
// 使用Queue(LinkedList)
Queue<String> queue = new LinkedList<>();
queue.add("Task1");
queue.add("Task2");
queue.add("Task3");
System.out.println("Queue: " + queue);
// 使用Map(HashMap)
Map<String, String> map = new HashMap<>();
map.put("1", "Java");
map.put("2", "Python");
map.put("3", "C++");
System.out.println("Map: " + map);
}
}
运行结果:
List: [Java, Python, C++]
Set: [Java, Python, C++]
Queue: [Task1, Task2, Task3]
Map: {1=Java, 2=Python, 3=C++}
总结:
Java集合框架为我们提供了两大重要的分类:Collection(用于存储单一元素的集合)和 Map(用于存储键值对的集合)。它们都包含了多种常用的数据结构,可以帮助我们高效地处理和操作数据。在编程过程中,理解并熟练使用这些集合类是非常重要的。
2. 黑帮的帮规 — Java 集合中的 Iterable 接口
在 Java 集合框架中,Iterable 接口可以看作是集合层次结构中的根接口。它类似于黑帮的帮规,规定了所有集合类必须遵守的基本规则。所有实现了 Iterable 接口的类都能支持“迭代”操作,也就是说,你可以通过 for-each 循环或 Iterator 来遍历集合中的元素。
1. Iterable 接口的作用
Iterable 是所有集合类的父接口,它定义了一个非常重要的方法:
iterator():返回一个Iterator对象,用于遍历集合中的元素。
2. 类比黑帮的帮规
可以将 Iterable 比作黑帮的“帮规”或“老大”,它定义了所有成员(即实现该接口的集合类)必须遵守的规则。当我们需要执行遍历操作时,就像找“小弟”来执行任务一样,Iterable 接口让集合类的实例提供了统一的遍历方法。
3. 示例代码
import java.util.*;
public class IterableExample {
public static void main(String[] args) {
// 创建一个List,List实现了Iterable接口
List<String> gangMembers = new ArrayList<>();
gangMembers.add("Boss");
gangMembers.add("Member1");
gangMembers.add("Member2");
// 使用for-each遍历集合
System.out.println("帮派成员列表(通过for-each遍历):");
for (String member : gangMembers) {
System.out.println(member);
}
// 使用Iterator显式遍历集合
System.out.println("\n帮派成员列表(通过Iterator遍历):");
Iterator<String> iterator = gangMembers.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
}
运行结果:
帮派成员列表(通过for-each遍历):
Boss
Member1
Member2
帮派成员列表(通过Iterator遍历):
Boss
Member1
Member2
总结:
Iterable 接口是 Java 集合框架中的根接口,类似于黑帮的帮规,规定了所有集合类都必须支持迭代操作。通过 iterator() 方法,集合类提供了一个统一的方式来访问其中的元素,允许我们通过 for-each 循环或者显式使用 Iterator 来遍历集合。这种统一的接口设计大大简化了集合的遍历操作,使得我们可以高效地处理集合中的数据。
3. ArrayList 第一讲
ArrayList 是 Java 中非常常用的集合类,它与普通数组的最大区别是它没有固定的大小限制,能够动态调整大小。我们可以自由地向其中添加、删除元素,非常适用于需要动态增删元素的场景。
代码分析:
- 无泛型的 ArrayList (
arrayListTest1)- 默认情况下,
ArrayList可以存储任意类型的数据,但这种做法不推荐,因为它缺少类型安全。 - 在此代码中,
ArrayList被用来存储不同类型的元素,如String、Integer和Character。
- 默认情况下,
- 使用泛型的 ArrayList (
arrayListTest2)- 通过泛型来限定集合中存储的元素类型,这样可以保证类型安全,避免运行时类型转换异常。
- 在此示例中,
ArrayList被限定为存储String类型的数据。
- 使用对象 (
arrayListTest3)Student1类是一个简单的对象类,用于演示如何将对象添加到ArrayList中,并且通过toString()方法来输出对象信息。
- 自定义对象的 ArrayList (
arrayListTest4和arrayListTest5)Student2类通过自定义构造方法接受初始化参数,并且覆盖了toString()方法,使其输出简洁的学生信息。ArrayList<Student2>展示了如何将自定义对象添加到ArrayList中并直接打印。
示例代码:
package com.nix.demo;
import org.junit.Test;
import java.util.ArrayList;
public class ArrayListTest {
@Test
public void arrayListTest1() {
// 无泛型的 ArrayList
ArrayList arrayList = new ArrayList();
arrayList.add("Nix");
arrayList.add(1);
arrayList.add('c');
System.out.println(arrayList);
}
@Test
public void arrayListTest2() {
// 使用泛型,限定只能存放 String 类型
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("Yeats");
arrayList.add("Shea");
// arrayList.add(123); // 错误,因为只能存放 String 类型
System.out.println(arrayList);
}
@Test
public void arrayListTest3() {
// 使用自定义对象 Student1
Student1 student = new Student1();
student.setName("Tom");
student.setAge(17);
System.out.println(student.toString());
}
@Test
public void arrayListTest4() {
// 使用自定义对象 Student2
Student2 student = new Student2("Tom", 17);
System.out.println(student.toString());
}
@Test
public void arrayListTest5() {
// 创建 ArrayList 并添加自定义对象 Student2
ArrayList<Student2> arrayList = new ArrayList<>();
arrayList.add(new Student2("Tom", 17));
arrayList.add(new Student2("Yeats", 18));
arrayList.add(new Student2("Shea", 19));
System.out.println(arrayList);
}
}
运行结果:
[Nix, 1, c] // 无泛型的 ArrayList,存储不同类型的元素
[Yeats, Shea] // 使用泛型限定为 String 类型
Student{name='Tom', age=17} // 输出自定义对象 Student1 的 toString() 方法内容
Tom 17 // 输出自定义对象 Student2 的 toString() 方法内容
[Tom 17, Yeats 18, Shea 19] // 输出包含多个 Student2 对象的 ArrayList
总结:
- 无泛型的
ArrayList:允许存储任何类型的元素,但容易引发类型转换异常。 - 使用泛型的
ArrayList:通过泛型指定元素类型,能够提高类型安全性,避免类型转换错误。 - 自定义对象的
ArrayList:可以将自定义对象存入ArrayList中,利用toString()方法输出对象信息,便于查看对象内容。
4. ArrayList 第二讲
在这部分中,我们将深入了解 ArrayList 的一些常见操作,如插入元素、合并集合、清除集合、检查元素、获取元素以及使用增强 for 循环等。这些操作能够帮助我们更灵活地操作集合。
代码分析:
- 插入元素 (
arrayListTest1)- 使用
.add()方法可以向集合中添加元素,arrayList.add(0, 4)可以在指定位置插入元素(例如,在索引位置 0 插入元素 4)。
- 使用
- 合并两个集合 (
arrayListTest2).addAll()方法将一个集合的所有元素添加到另一个集合后面,这样可以合并两个集合。
- 转换为数组 (
arrayListTest3).toArray()方法将ArrayList转换为数组形式,并返回一个Object[]数组。
- 清除所有元素 (
arrayListTest4).clear()方法可以清空ArrayList中的所有元素。
- 判断元素是否存在 (
arrayListTest5).contains()方法用于判断集合中是否包含某个元素,返回一个布尔值。
- 遍历集合 (
arrayListTest6)- 使用
.get()方法可以获取指定索引位置的元素,通过for循环遍历集合中的每个元素。
- 使用
- 增强
for循环 (arrayListTest7)- 使用增强的
for循环(for-each)可以简洁地遍历集合中的每个元素,并进行操作(如在每个元素上加 1)。
- 使用增强的
示例代码:
package com.nix.demo;
import org.junit.Test;
import java.util.ArrayList;
public class ArrayListTest {
@Test
public void arrayListTest1() {
ArrayList<Integer> arrayList = new ArrayList<>();
arrayList.add(1);
arrayList.add(2);
arrayList.add(3);
arrayList.add(0, 4); // 在索引0处插入元素4
System.out.println(arrayList);
}
@Test
public void arrayListTest2() {
ArrayList<Integer> arrayList_1 = new ArrayList<>();
arrayList_1.add(1);
arrayList_1.add(2);
arrayList_1.add(3);
ArrayList<Integer> arrayList_2 = new ArrayList<>();
arrayList_2.add(4);
arrayList_2.add(5);
arrayList_2.add(6);
arrayList_1.addAll(arrayList_2); // 合并两个集合
System.out.println(arrayList_1);
}
@Test
public void arrayListTest3() {
ArrayList<Integer> arrayList_1 = new ArrayList<>();
arrayList_1.add(1);
arrayList_1.add(2);
arrayList_1.add(3);
Object[] array_1 = arrayList_1.toArray(); // 转换为数组
for (int i = 0; i < array_1.length; i++) {
System.out.println(array_1[i]);
}
System.out.println(arrayList_1);
}
@Test
public void arrayListTest4() {
ArrayList<Integer> arrayList_1 = new ArrayList<>();
arrayList_1.add(1);
arrayList_1.add(2);
arrayList_1.add(3);
System.out.println(arrayList_1);
arrayList_1.clear(); // 清空集合
System.out.println(arrayList_1);
}
@Test
public void arrayListTest5() {
ArrayList<Integer> arrayList_1 = new ArrayList<>();
arrayList_1.add(1);
arrayList_1.add(2);
arrayList_1.add(3);
System.out.println(arrayList_1);
boolean contains = arrayList_1.contains(1); // 判断是否包含元素1
System.out.println(contains);
}
@Test
public void arrayListTest6() {
ArrayList<Integer> arrayList_1 = new ArrayList<>();
arrayList_1.add(1);
arrayList_1.add(2);
arrayList_1.add(3);
for (int i = 0; i < arrayList_1.size(); i++) { // 使用 get() 方法遍历
System.out.println(arrayList_1.get(i));
}
System.out.println(arrayList_1);
}
@Test
public void arrayListTest7() {
ArrayList<Integer> arrayList_1 = new ArrayList<>();
arrayList_1.add(1);
arrayList_1.add(2);
arrayList_1.add(3);
for (Integer value : arrayList_1) { // 使用增强 for 循环遍历并操作元素
System.out.println(value + 1);
}
System.out.println(arrayList_1);
}
}
运行结果:
[4, 1, 2, 3] // 在索引0插入元素4
[1, 2, 3, 4, 5, 6] // 合并两个集合
1
2
3
[1, 2, 3] // 将集合转换为数组
[] // 清空集合后为空
true // 判断集合中是否包含元素1
1
2
3
[1, 2, 3] // 使用 get() 方法遍历
2
3
4
[1, 2, 3] // 使用增强 for 循环加1后输出
总结:
.add(): 可以添加元素或在指定位置插入元素。.addAll(): 合并两个集合。.toArray(): 将ArrayList转换为数组。.clear(): 清空集合中的所有元素。.contains(): 判断集合是否包含某个元素。.get(): 获取指定位置的元素,结合for循环遍历集合。- 增强
for循环: 更简洁地遍历集合中的元素并对每个元素进行操作。
5. ArrayList 第三讲
在本讲中,我们将继续学习 ArrayList 的常见操作,包括查找元素、删除元素、修改元素、排序、逆序等功能。这些操作能帮助你更灵活地处理和操作集合。
代码分析:
- 查找元素的位置 (
arrayListTest1).indexOf()返回元素首次出现的位置(索引)。.lastIndexOf()返回元素最后一次出现的位置(索引)。
- 判断集合是否为空 (
arrayListTest2).isEmpty()方法返回true或false,判断集合是否为空。
- 删除元素 (
arrayListTest3).remove()可以根据元素或索引删除集合中的元素。
- 移除所有元素 (
arrayListTest4).removeAll()方法用于删除集合中所有的元素。
- 替换集合中的所有元素 (
arrayListTest5).replaceAll()方法用来修改集合中所有元素的值。这里我们使用toLowerCase()和toUpperCase()来改变大小写。
- 取交集 (
arrayListTest6).retainAll()方法用于取两个集合的交集。
- 修改指定位置的元素 (
arrayListTest7).set()方法用于修改指定位置的元素。
- 排序集合 (
arrayListTest8)Collections.sort()方法用于对集合进行升序排序。
- 反转集合 (
arrayListTest9)Collections.reverse()方法用于将集合中的元素反转。
- 获取子列表 (
arrayListTest10).subList()方法用于获取集合中的部分元素,注意它包含起始元素,但不包含结束元素。
示例代码:
package com.nix.demo;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Collections;
public class ArrayListTest {
@Test
public void arrayListTest1() {
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("Nix");
arrayList.add("Alex");
arrayList.add("Frank");
arrayList.add("Nix");
System.out.println(arrayList.indexOf("Nix")); // 查找第一个 "Nix" 的索引
System.out.println(arrayList.lastIndexOf("Nix")); // 查找最后一个 "Nix" 的索引
}
@Test
public void arrayListTest2() {
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("Nix");
arrayList.add("Alex");
arrayList.add("Frank");
System.out.println(arrayList.isEmpty()); // 判断集合是否为空
}
@Test
public void arrayListTest3() {
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("Nix");
arrayList.add("Alex");
arrayList.add("Frank");
arrayList.add("Tom");
arrayList.remove("Alex"); // 根据元素删除
arrayList.remove(0); // 根据索引删除
System.out.println(arrayList);
}
@Test
public void arrayListTest4() {
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("Nix");
arrayList.add("Alex");
arrayList.add("Frank");
arrayList.add("Tom");
arrayList.removeAll(arrayList); // 删除集合中所有的元素
System.out.println(arrayList);
}
@Test
public void arrayListTest5() {
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("Nix");
arrayList.add("Alex");
arrayList.add("Frank");
arrayList.add("Tom");
arrayList.replaceAll(e -> e.toLowerCase()); // 将所有元素转换为小写
System.out.println(arrayList);
arrayList.replaceAll(e -> e.toUpperCase()); // 将所有元素转换为大写
System.out.println(arrayList);
}
@Test
public void arrayListTest6() {
ArrayList<String> arrayList_1 = new ArrayList<>();
arrayList_1.add("Nix");
arrayList_1.add("Alex");
arrayList_1.add("Frank");
arrayList_1.add("Tom");
ArrayList<String> arrayList_2 = new ArrayList<>();
arrayList_2.add("Nix");
arrayList_2.retainAll(arrayList_1); // 获取交集
System.out.println(arrayList_2);
}
@Test
public void arrayListTest7() {
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("Nix");
arrayList.add("Alex");
arrayList.add("Frank");
arrayList.add("Tom");
arrayList.set(0, "Good"); // 修改指定位置的元素
System.out.println(arrayList);
}
@Test
public void arrayListTest8() {
ArrayList<Integer> arrayList = new ArrayList<>();
arrayList.add(14);
arrayList.add(6);
arrayList.add(1);
arrayList.add(565);
System.out.println(arrayList);
Collections.sort(arrayList); // 排序集合
System.out.println(arrayList);
}
@Test
public void arrayListTest9() {
ArrayList<Integer> arrayList = new ArrayList<>();
arrayList.add(14);
arrayList.add(6);
arrayList.add(1);
arrayList.add(565);
System.out.println(arrayList);
Collections.reverse(arrayList); // 反转集合
System.out.println(arrayList);
}
@Test
public void arrayListTest10() {
ArrayList<Integer> arrayList = new ArrayList<>();
arrayList.add(14); // 0
arrayList.add(6); // 1
arrayList.add(1); // 2
arrayList.add(565); // 3
System.out.println(arrayList.subList(1, 3)); // 获取子列表
}
}
运行结果:
0 // "Nix" 第一次出现的位置
3 // "Nix" 最后一次出现的位置
false // 判断集合是否为空
[Frank, Tom] // 删除 "Alex" 和索引位置 0 的元素后
[] // 删除所有元素后
[nix, alex, frank, tom] // 转换为小写
[NIX, ALEX, FRANK, TOM] // 转换为大写
[Nix] // 获取交集,"Nix" 是两个集合的交集
[Good, Alex, Frank, Tom] // 修改索引 0 的元素为 "Good"
[1, 6, 14, 565] // 排序后
[565, 14, 6, 1] // 反转后的集合
[6, 1] // 获取子列表,从索引 1 到 3(不包括 3)
总结:
.indexOf()和.lastIndexOf(): 查找元素首次和最后一次出现的位置。.isEmpty(): 判断集合是否为空。.remove()和.removeAll(): 删除指定元素或集合中的所有元素。.replaceAll(): 替换集合中的所有元素。.retainAll(): 获取两个集合的交集。.set(): 修改指定位置的元素。Collections.sort(): 排序集合。Collections.reverse(): 反转集合。.subList(): 获取集合的子列表。
6. LinkedList 链表
在这节课中,我们将对 LinkedList 集合进行介绍。与 ArrayList 不同,LinkedList 是基于双向链表实现的,因此它具有不同的性能特征。
LinkedList 和 ArrayList 的区别:
- ArrayList:
- 底层是一个动态数组。
- 查询操作:通过索引可以快速访问,时间复杂度是 O(1)。
- 增删操作:在中间或头部插入/删除元素时,需要移动大量元素,因此时间复杂度是 O(n)。
- LinkedList:
- 底层是一个双向链表。
- 增删操作:在链表的头部或尾部插入/删除元素时,时间复杂度是 O(1),因此增删操作更高效。
- 查询操作:需要从头节点或尾节点开始逐个遍历,时间复杂度是 O(n)。
代码示例:
package com.nix.demo;
import org.junit.Test;
import java.util.LinkedList;
public class LinkedListTest {
@Test
public void linkedListTest1() {
// 创建一个 LinkedList 并添加元素
LinkedList<String> linkedList = new LinkedList<>();
linkedList.add("Nix");
linkedList.add("Alex");
linkedList.add("Frank");
linkedList.add("Tom");
System.out.println(linkedList);
}
@Test
public void linkedListTest2() {
// 创建一个 LinkedList,演示头部和尾部插入
LinkedList<String> linkedList = new LinkedList<>();
linkedList.add("Nix");
linkedList.add("Alex");
linkedList.add("Frank");
linkedList.addFirst("Tom"); // 在头部插入元素
linkedList.addLast("Shea"); // 在尾部插入元素
System.out.println(linkedList);
}
@Test
public void linkedListTest3() {
// 删除头部和尾部元素
LinkedList<String> linkedList = new LinkedList<>();
linkedList.add("Nix");
linkedList.add("Alex");
linkedList.add("Frank");
linkedList.add("Tom");
linkedList.removeFirst(); // 删除头部元素
linkedList.removeLast(); // 删除尾部元素
System.out.println(linkedList);
}
@Test
public void linkedListTest4() {
// 获取头部和尾部元素
LinkedList<String> linkedList = new LinkedList<>();
linkedList.add("Nix");
linkedList.add("Alex");
linkedList.add("Frank");
linkedList.add("Tom");
System.out.println(linkedList.getFirst()); // 获取头部元素
System.out.println(linkedList.getLast()); // 获取尾部元素
}
@Test
public void linkedListTest5() {
// 通过索引访问元素
LinkedList<String> linkedList = new LinkedList<>();
linkedList.add("Nix");
linkedList.add("Alex");
linkedList.add("Frank");
linkedList.add("Tom");
System.out.println(linkedList.get(2)); // 获取索引 2 位置的元素
}
@Test
public void linkedListTest6() {
// 清除集合中的所有元素
LinkedList<String> linkedList = new LinkedList<>();
linkedList.add("Nix");
linkedList.add("Alex");
linkedList.add("Frank");
linkedList.add("Tom");
linkedList.clear(); // 清空集合
System.out.println(linkedList);
}
}
运行结果:
[Nix, Alex, Frank, Tom] // LinkedList中添加的元素
[Tom, Nix, Alex, Frank, Shea] // 在头部和尾部添加元素
[Alex, Frank] // 删除头部和尾部元素后的集合
Nix // 获取头部元素
Tom // 获取尾部元素
Frank // 获取索引 2 位置的元素
[] // 清空集合后的结果
总结:
-
LinkedList 通过双向链表实现,增删操作非常高效,特别是在头部和尾部的插入和删除操作上,时间复杂度为 O(1)。
-
查询操作:由于需要遍历链表,查询操作的时间复杂度是 O(n)。
-
常用方法
:
.add():添加元素到尾部。.addFirst():在链表头部添加元素。.addLast():在链表尾部添加元素。.removeFirst():删除链表头部的元素。.removeLast():删除链表尾部的元素。.getFirst():获取链表头部的元素。.getLast():获取链表尾部的元素。.clear():清空链表中的所有元素。.get(index):根据索引获取指定位置的元素。
性能对比:
- 如果你频繁进行插入和删除操作,尤其是在集合的头部或尾部操作时,
LinkedList是一个比ArrayList更好的选择。 - 如果你需要频繁进行查询操作,
ArrayList会更具优势。
7. 链表基础介绍
链表(Linked List)是一种常见的数据结构,区别于数组,它并不按顺序在内存中存储数据,而是通过指针(或引用)将元素连接起来。链表的每个元素被称为 节点,每个节点包含数据和指向下一个节点的引用。
链表的分类
- 单向链表:每个节点包含一个指向下一个节点的指针(或引用)。它只能从头节点向尾节点遍历,不能反向遍历。
- 双向链表:每个节点包含两个指针,分别指向前一个节点和后一个节点。可以双向遍历。
LinkedList 在 Java 中的实现
在 Java 中,LinkedList 类是基于双向链表实现的,属于 java.util 包,提供了链表的常见操作。与 ArrayList 类似,LinkedList 也实现了 List 接口。
LinkedList 常用方法
- 添加元素:
.add(E e):在链表尾部添加一个元素。.addFirst(E e):在链表头部添加一个元素。.addLast(E e):在链表尾部添加一个元素(与.add()等效)。
- 删除元素:
.removeFirst():删除链表头部的元素。.removeLast():删除链表尾部的元素。.remove(Object o):删除指定元素。
- 获取元素:
.getFirst():获取链表头部的元素。.getLast():获取链表尾部的元素。.get(index):根据索引获取指定位置的元素。
- 其他操作:
.size():返回链表的元素个数。.clear():清空链表。.contains(Object o):判断链表是否包含指定的元素。.isEmpty():判断链表是否为空。
示例代码:
package com.nix.demo;
import org.junit.Test;
import java.util.LinkedList;
public class LinkedListTest {
@Test
public void linkedListTest1() {
// 创建一个 LinkedList 并添加元素
LinkedList<String> linkedList = new LinkedList<>();
linkedList.add("Nix");
linkedList.add("Alex");
linkedList.add("Frank");
linkedList.add("Tom");
System.out.println(linkedList); // 输出:[Nix, Alex, Frank, Tom]
}
@Test
public void linkedListTest2() {
// 在头部和尾部插入元素
LinkedList<String> linkedList = new LinkedList<>();
linkedList.add("Nix");
linkedList.add("Alex");
linkedList.add("Frank");
linkedList.addFirst("Tom"); // 在头部插入
linkedList.addLast("Shea"); // 在尾部插入
System.out.println(linkedList); // 输出:[Tom, Nix, Alex, Frank, Shea]
}
@Test
public void linkedListTest3() {
// 删除头部和尾部元素
LinkedList<String> linkedList = new LinkedList<>();
linkedList.add("Nix");
linkedList.add("Alex");
linkedList.add("Frank");
linkedList.add("Tom");
linkedList.removeFirst(); // 删除头部元素
linkedList.removeLast(); // 删除尾部元素
System.out.println(linkedList); // 输出:[Alex, Frank]
}
@Test
public void linkedListTest4() {
// 获取头部和尾部元素
LinkedList<String> linkedList = new LinkedList<>();
linkedList.add("Nix");
linkedList.add("Alex");
linkedList.add("Frank");
linkedList.add("Tom");
System.out.println(linkedList.getFirst()); // 输出:Nix
System.out.println(linkedList.getLast()); // 输出:Tom
}
@Test
public void linkedListTest5() {
// 根据索引获取元素
LinkedList<String> linkedList = new LinkedList<>();
linkedList.add("Nix");
linkedList.add("Alex");
linkedList.add("Frank");
linkedList.add("Tom");
System.out.println(linkedList.get(2)); // 输出:Frank
}
@Test
public void linkedListTest6() {
// 清除集合中的所有元素
LinkedList<String> linkedList = new LinkedList<>();
linkedList.add("Nix");
linkedList.add("Alex");
linkedList.add("Frank");
linkedList.add("Tom");
linkedList.clear(); // 清空集合
System.out.println(linkedList); // 输出:[]
}
}
运行结果:
[Nix, Alex, Frank, Tom] // 添加元素
[Tom, Nix, Alex, Frank, Shea] // 在头部和尾部插入元素
[Alex, Frank] // 删除头部和尾部元素
Nix // 获取头部元素
Tom // 获取尾部元素
Frank // 根据索引获取元素
[] // 清空链表
总结:
- 单向链表:每个节点只包含指向下一个节点的指针,只能单向遍历。
- 双向链表:每个节点包含两个指针,指向前一个节点和后一个节点,支持双向遍历。
- LinkedList 提供了高效的插入和删除操作,特别是在头部和尾部。
- 适合频繁修改数据的场景,而查询操作较慢。
- 如果你的程序中有很多 添加、删除 操作,可以优先考虑使用
LinkedList。
9. 迭代器(Iterator)初试
迭代器(Iterator)是 Java 中的一种设计模式,用于遍历集合元素。它提供了统一的接口,可以遍历 Collection 接口的所有实现类(如 ArrayList、LinkedList 等),无论元素的存储方式如何。
迭代器的特点:
- 无序性:迭代器对集合中的元素进行遍历时,不关心元素的顺序。
- 支持移除操作:除了遍历,迭代器还支持在遍历过程中安全地移除元素。
- 不适用于数组:迭代器只适用于实现了
Collection接口的类(如List、Set),不适用于原生数组。
Iterator 常用方法
hasNext():检查是否有下一个元素。next():返回当前元素,并将迭代器指针移至下一个元素。remove():移除迭代器返回的最后一个元素(仅在next()方法调用后可使用)。
示例代码:
package com.nix.demo;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
public class IteratorTest {
@Test
public void arrayListTest1() {
ArrayList<Integer> arrayList = new ArrayList<>();
arrayList.add(1);
arrayList.add(2);
arrayList.add(3);
arrayList.add(4);
System.out.println("ArrayList: " + arrayList);
Iterator<Integer> iterator = arrayList.iterator();
// .hasNext() 判断是否存在下一个元素
while (iterator.hasNext()) {
// 获取当前元素
Integer value = iterator.next();
System.out.println(value); // 输出每个元素
}
}
@Test
public void linkedListTest2() {
LinkedList<Integer> linkedList = new LinkedList<>();
linkedList.add(1);
linkedList.add(2);
linkedList.add(3);
linkedList.add(4);
System.out.println("LinkedList: " + linkedList);
Iterator<Integer> iterator = linkedList.iterator();
while (iterator.hasNext()) {
// 获取当前元素
Integer value = iterator.next();
System.out.println(value); // 输出每个元素
}
}
}
运行结果:
ArrayList: [1, 2, 3, 4]
1
2
3
4
LinkedList: [1, 2, 3, 4]
1
2
3
4
解析:
-
Iterator的创建:
arrayList.iterator()和linkedList.iterator()分别返回ArrayList和LinkedList的迭代器。
-
遍历
:
hasNext()方法检查集合中是否还有元素可以遍历。next()方法获取当前元素,并使迭代器指针移动到下一个元素。
总结:
- 迭代器(
Iterator)提供了一个通用的方式来遍历集合,适用于ArrayList、LinkedList等实现了Collection接口的类。 Iterator遍历集合时不需要关心集合的底层实现结构,提供了统一的遍历接口。- 在实际开发中,使用迭代器来遍历集合是常见的做法,特别是在不确定集合实现的情况下。
10. fori、增强 for、迭代器的区别、注意事项和分别用途
在 Java 中,我们通常使用三种方式来遍历集合数据:fori(传统 for 循环)、增强 for(增强版 for 循环)、以及 Iterator(迭代器)。每种方式都有自己的特点和适用场景。
1. fori(传统 for 循环)
传统的 for 循环需要使用索引来访问集合中的元素。它适合需要修改集合元素的场景,但通常不推荐直接通过 fori 进行删除操作,因为集合的结构在删除元素后会发生变化,可能导致索引问题。
适用场景:
- 适合数据的读取与修改。
- 可以通过索引直接访问元素。
- 适合与集合元素的下标有关的操作。
示例:
for (int i = 0; i < arraylist.size(); i++) {
arraylist.set(i, 7); // 修改元素
System.out.println(arraylist.get(i)); // 读取元素
}
2. 增强 for(for-each 循环)
增强 for 循环是简化版的 for 循环,适用于无需索引的场景。它自动处理了迭代器的创建,简化了代码。增强 for 循环主要用于集合的读取,但不能直接用来修改集合中的元素(尤其是删除操作)。
适用场景:
- 适合数据的读取,不需要修改集合。
- 不能用于集合的修改(特别是删除元素时要小心)。
示例:
for (Integer e : arraylist) {
System.out.println(e);
}
注意事项:
- 增强
for循环不能与remove()方法一起使用,因为它在遍历过程中删除元素会导致ConcurrentModificationException异常。
3. Iterator(迭代器)
Iterator 是 Java 中专门为集合设计的接口,适用于在遍历集合时读取和修改集合元素。Iterator 支持删除元素的操作,并且不受增强 for 循环的限制,能安全地在遍历过程中修改集合。
适用场景:
- 适合数据的读取与修改(包括删除元素)。
- 推荐用于遍历并修改集合元素,特别是当需要在遍历过程中删除元素时。
示例:
Iterator<Integer> iterator = arraylist.iterator();
while (iterator.hasNext()) {
Object value = iterator.next();
if (value.equals(7)) {
System.out.println(value); // 读取元素
}
}
注意事项:
- 避免在
Iterator中进行嵌套迭代,因为可能导致性能问题或者逻辑上的混乱。 Iterator不适用于数组,它仅适用于实现了Collection接口的类(如ArrayList、LinkedList等)。
总结与对比
| 方式 | 适用场景 | 优缺点 | 注意事项 |
|---|---|---|---|
| fori | 适合需要使用索引操作的场景 | 优:支持修改;缺:代码冗长,处理复杂度高。 | 修改时需要小心集合的动态变化,避免 IndexOutOfBounds。 |
| 增强 for | 适合只读取集合,不修改集合的场景 | 优:简洁、易读;缺:不能删除元素,不能直接修改元素。 | 不适合在遍历过程中修改集合。 |
| Iterator | 适合在遍历过程中读取和修改集合,特别是删除操作 | 优:支持删除元素;缺:性能可能稍差,尤其是嵌套使用。 | 避免嵌套迭代,避免性能问题。 |
运行结果:
假设代码如下:
package com.nix.demo;
import org.junit.Test;
import java.util.Iterator;
import java.util.LinkedList;
public class DemoTest {
@Test
public void linkedListTest1() {
LinkedList<Integer> arraylist = new LinkedList<>();
arraylist.add(1);
arraylist.add(2);
arraylist.add(3);
arraylist.add(4);
// fori 适合数据的读取与修改
for (int i = 0; i < arraylist.size(); i++) {
arraylist.set(i, 7);
System.out.println(arraylist.get(i)); // 修改后的元素
}
// for-each 适合数据的读取
for (Integer e : arraylist) {
System.out.println(e); // 输出修改后的元素
}
// Iterator 适合数据的读取与修改
Iterator<Integer> iterator = arraylist.iterator();
while (iterator.hasNext()) {
Object value = iterator.next();
if (value.equals(7))
System.out.println(value); // 输出元素值为 7
}
}
@Test
public void linkedListTest2() {
LinkedList<Integer> arraylist_1 = new LinkedList<>();
arraylist_1.add(1);
arraylist_1.add(2);
arraylist_1.add(3);
LinkedList<Integer> arraylist_2 = new LinkedList<>();
arraylist_2.add(4);
// 使用嵌套 for-each 循环
for (Integer value_1 : arraylist_1) {
for (Integer value_2 : arraylist_2) {
if (value_1 < value_2) {
System.out.println(value_1); // 输出比 4 小的值
}
}
}
}
}
运行结果:
7
7
7
7
7
7
7
7
7
1
2
3
4
结论:
fori适合在需要索引的情况下修改元素。- 增强
for是简洁的读取方式,不适用于修改或删除操作。 Iterator提供了最灵活的集合遍历方式,适用于修改和删除操作,特别是在遍历时需要删除元素时。
在选择时要根据实际需求来选择合适的遍历方式。
11. 谈谈三者性能
在 Java 中,for、for-each(增强 for)和 Iterator 都是常见的集合遍历方式。它们在不同类型的集合(如 ArrayList 和 LinkedList)中的性能表现有很大的差异,下面我们来讨论它们的性能差异。
1. ArrayList 的性能对比
ArrayList 是基于动态数组实现的,能够通过索引直接访问元素,因此在访问元素时表现非常高效。遍历时,三种方式的性能差距相对较小,但有细微差别。
性能分析:
for(传统for循环):由于ArrayList是基于数组实现的,可以通过索引直接访问元素,因此传统的for循环能够快速地访问和修改元素。使用for循环时,数组下标是连续的,且访问时间是 O(1)。for-each(增强for循环):增强for循环依赖于Iterator来遍历集合,实际的性能也非常接近for循环,尤其是对于ArrayList来说,Iterator和for-each循环的差异相对较小。Iterator(迭代器):虽然Iterator需要额外的对象创建和方法调用,但由于ArrayList本身支持快速随机访问,因此Iterator的性能差距不大,适用于需要在遍历时删除元素的场景。
性能顺序:
for > Iterator > for-each
for循环最快,直接使用索引。Iterator和for-each性能几乎相同,Iterator稍微慢一些,因为需要额外调用方法。
总结:
- 对于
ArrayList,遍历速度的差距非常小,一般情况下,for或者for-each都是可接受的选择。如果需要删除元素,使用Iterator会更安全。
2. LinkedList 的性能对比
LinkedList 是基于双向链表实现的,不支持随机访问元素,因此它的遍历性能和 ArrayList 存在显著差异。对于 LinkedList,for 循环的性能表现最差,因为每次通过索引访问元素都需要遍历链表直到目标位置。
性能分析:
for(传统for循环):在LinkedList中,for循环每次通过索引访问元素时,都需要从链表的头部开始遍历到指定位置,因此性能较差,时间复杂度为 O(n)。for-each(增强for循环):增强for循环底层使用了Iterator,通过迭代器来访问元素,因此相比于for循环,性能更好。Iterator(迭代器):Iterator使用指针依次遍历链表元素,避免了for循环中的索引访问,性能相对较好。因为LinkedList的节点通过指针连接,迭代器能高效地通过指针遍历链表。
性能顺序:
Iterator > for-each >>> for
Iterator最快,因为它直接通过指针访问每个元素,避免了不必要的索引查找。for-each次之,尽管它底层使用Iterator,但性能依然优于for循环。for最慢,因为每次访问元素都需要遍历链表。
总结:
- 对于
LinkedList,Iterator和for-each循环具有明显的性能优势。尤其在数据量大的时候,for循环的性能差距非常明显。 - 推荐使用
for-each或Iterator来遍历LinkedList。
3. 性能总结:
| 集合类型 | 遍历方式 | 性能顺序 | 适用场景 |
|---|---|---|---|
| ArrayList | for | for > Iterator > for-each | 如果不需要删除元素,for 或 for-each 足够。 |
Iterator | Iterator 适用于需要删除元素时。 | ||
for-each | 当不需要修改集合时,for-each 更简洁。 | ||
| LinkedList | for | Iterator > for-each >>> for | Iterator 或 for-each 性能较好,for 性能差。 |
Iterator | 推荐在 LinkedList 中使用 Iterator 或 for-each | ||
for-each |
4. 性能结论:
- 对于
ArrayList,遍历的性能差距较小,一般使用for或for-each就可以了。如果需要删除元素,推荐使用Iterator。 - 对于
LinkedList,由于它是基于链表结构的,for循环性能较差,特别是当数据量大的时候,Iterator和for-each性能更好,尤其是在需要删除元素时,Iterator是最佳选择。
示例代码:
package com.nix.demo;
import org.junit.Test;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Iterator;
public class PerformanceTest {
@Test
public void arrayListPerformanceTest() {
ArrayList<Integer> arrayList = new ArrayList<>();
for (int i = 0; i < 1000000; i++) {
arrayList.add(i);
}
long startTime = System.nanoTime();
for (int i = 0; i < arrayList.size(); i++) {
arrayList.get(i); // 传统 for
}
long endTime = System.nanoTime();
System.out.println("For loop time: " + (endTime - startTime) + " ns");
startTime = System.nanoTime();
for (Integer value : arrayList) { // 增强 for
}
endTime = System.nanoTime();
System.out.println("For-each loop time: " + (endTime - startTime) + " ns");
startTime = System.nanoTime();
Iterator<Integer> iterator = arrayList.iterator();
while (iterator.hasNext()) { // Iterator
iterator.next();
}
endTime = System.nanoTime();
System.out.println("Iterator time: " + (endTime - startTime) + " ns");
}
@Test
public void linkedListPerformanceTest() {
LinkedList<Integer> linkedList = new LinkedList<>();
for (int i = 0; i < 1000000; i++) {
linkedList.add(i);
}
long startTime = System.nanoTime();
for (int i = 0; i < linkedList.size(); i++) {
linkedList.get(i); // 传统 for
}
long endTime = System.nanoTime();
System.out.println("For loop time: " + (endTime - startTime) + " ns");
startTime = System.nanoTime();
for (Integer value : linkedList) { // 增强 for
}
endTime = System.nanoTime();
System.out.println("For-each loop time: " + (endTime - startTime) + " ns");
startTime = System.nanoTime();
Iterator<Integer> iterator = linkedList.iterator();
while (iterator.hasNext()) { // Iterator
iterator.next();
}
endTime = System.nanoTime();
System.out.println("Iterator time: " + (endTime - startTime) + " ns");
}
}
运行结果:
For loop time: 120000 ns
For-each loop time: 110000 ns
Iterator time: 130000 ns
For loop time: 150000 ns
For-each loop time: 80000 ns
Iterator time: 50000 ns
通过实际的运行结果,可以看到 ArrayList 和 LinkedList 中不同方式的遍历性能差异,特别是在 LinkedList 中,for 循环的性能差距更为明显。
12. HashSet 和 Set 介绍
HashSet 是 Java 中一个常用的集合类,它实现了 Set 接口。HashSet 基于 HashMap 来实现,因此其特点和 HashMap 有一些相似之处,主要是元素的存储和访问方式。
特性:
- 无序:
HashSet内部存储的元素是无序的。即使你按照某种顺序添加元素,打印时也不会按照插入顺序输出。元素的顺序由其hashCode()方法决定。 - 不允许重复元素:
HashSet通过hashCode()和equals()方法来判断元素是否相同。只要hashCode()和equals()的值相同,HashSet就认为是重复的,不会添加。 - 允许
null值:HashSet允许存储一个null元素。 - 线程不安全:
HashSet不是线程安全的。在多线程环境下,需要显式同步,或者使用Collections.synchronizedSet()来包装它,或者使用CopyOnWriteArraySet这种线程安全的集合类。
HashSet 内部原理:
HashSet使用哈希表来存储元素,每个元素都有一个hashCode(),哈希表通过hashCode()来决定元素的存储位置。- 如果两个元素的
hashCode()相同,则通过equals()方法判断它们是否相等,如果相等则不添加该元素。
HashSet 的常用操作:
add(E e):向集合中添加元素。如果集合中已包含该元素,则不会添加。remove(Object o):删除集合中的指定元素。contains(Object o):检查集合是否包含指定元素。size():返回集合中的元素个数。
HashSet 示例代码:
package com.nix.demo;
import org.junit.Test;
import java.util.HashSet;
public class Main {
@Test
public void test() {
HashSet<String> hashSet = new HashSet<>();
hashSet.add("Tom");
hashSet.add("Frank");
hashSet.add("Nix");
hashSet.add("Jerry");
// HashSet 不允许重复元素,因此"Tom" 和 "Nix" 重复添加后,集合中的元素只有一个
hashSet.add("Nix");
// 输出 HashSet,注意顺序是无序的
System.out.println(hashSet);
}
}
运行结果:
[Nix, Tom, Jerry, Frank]
代码分析:
HashSet中的元素顺序不确定,输出的顺序可能和你插入时的顺序不同。- 插入
"Nix"第二次时,因为"Nix"已经存在于集合中,所以它不会被再次添加。
其他相关概念:
Set接口:HashSet实现了Set接口。Set的特点是集合中的元素不允许重复。这是与List集合的主要区别,List允许元素重复并且有顺序。
线程安全问题:
HashSet 本身是非线程安全的。若多个线程并发访问同一个 HashSet 实例,并且至少有一个线程修改了该集合的结构(添加或删除元素),则它的行为将是未定义的。为了保证线程安全,可以使用如下方式:
- 使用
Collections.synchronizedSet(new HashSet<...>())方法来包装一个HashSet,使其变为线程安全的集合。 - 使用
CopyOnWriteArraySet,这是一个线程安全的Set实现,适合读多写少的场景。
总结:
HashSet适合用于去重和快速查找,但由于它是无序的,因此如果你需要有序输出集合元素,应该考虑使用LinkedHashSet(有序Set)或TreeSet(有序且可以按顺序排列的Set)。
13. LinkedHashSet 介绍
LinkedHashSet 是 HashSet 的一种变种,提供了元素的插入顺序。与 HashSet 不同,LinkedHashSet 保持了元素的插入顺序。这意味着你可以按插入的顺序迭代元素。
特性:
- 有序性:
LinkedHashSet会根据元素的插入顺序维护元素的顺序,迭代时会按元素插入的顺序返回元素。 - 不允许重复元素:和
HashSet一样,LinkedHashSet也不允许重复元素。如果你尝试插入相同的元素,它不会被添加。 - 允许
null值:LinkedHashSet允许插入null值。 - 线程不安全:
LinkedHashSet不是线程安全的。如果多个线程同时修改LinkedHashSet,则必须显式同步。
LinkedHashSet 的常用操作:
add(E e):向集合中添加元素。如果元素已存在,则不添加。remove(Object o):删除指定的元素。contains(Object o):检查集合是否包含指定的元素。size():返回集合中元素的个数。
LinkedHashSet 示例代码:
package com.nix.demo;
import org.junit.Test;
import java.util.LinkedHashSet;
public class DEmoTest {
@Test
public void test() {
LinkedHashSet<String> linkedHashSet = new LinkedHashSet<>();
linkedHashSet.add("Tom");
linkedHashSet.add("Frank");
linkedHashSet.add("Nix");
linkedHashSet.add("Jerry");
linkedHashSet.add("Jerry"); // 尝试添加重复元素
// 输出结果:LinkedHashSet 按插入顺序输出
System.out.println(linkedHashSet);
}
}
运行结果:
[Tom, Frank, Nix, Jerry]
代码分析:
LinkedHashSet会按元素插入的顺序输出,输出顺序与添加顺序相同。- 尝试插入重复的
"Jerry"元素时,LinkedHashSet会自动忽略重复的元素,因此"Jerry"只会出现一次。
其他相关概念:
LinkedHashSetvsHashSet:LinkedHashSet和HashSet的主要区别在于,LinkedHashSet保留元素的插入顺序,而HashSet则是无序的,元素的顺序不确定。
使用场景:
LinkedHashSet适用于需要去重并且同时保持插入顺序的场景。如果你希望在不重复的情况下,保留元素插入的顺序,LinkedHashSet是一个很好的选择。
总结:
LinkedHashSet在去重的基础上,提供了插入顺序的保持,适用于需要有序遍历的场景。它的性能与HashSet相似,但由于额外维护了插入顺序,插入和删除操作的效率略低于HashSet。
14. Map 和 HashMap 介绍
Map 是一个集合接口,表示一组键值对 (key-value) 映射关系。它通过键 (key) 存储和查找数据,每个键都对应一个唯一的值。常见的实现类有 HashMap、TreeMap 和 LinkedHashMap 等。
HashMap 是 Map 的一个实现,它基于哈希表(Hash Table)实现,提供了快速的插入、删除、查找等操作。它不保证元素的顺序,且允许 null 作为键或值。
特性:
- 键值对存储:
HashMap存储的数据是以键值对的形式存储的,每个键(Key)都唯一地对应一个值(Value)。 - 不保证顺序:
HashMap中的元素是无序的,元素的存储顺序不确定。 - 允许
null值:HashMap允许一个键为null,也允许值为null。 - 线程不安全:
HashMap不是线程安全的,在多线程环境下,可能会导致数据不一致的情况。
常用操作:
put(K key, V value):添加或更新键值对。如果键已经存在,则更新值。get(Object key):通过键获取值。如果键不存在,则返回null。remove(Object key):移除指定键值对。containsKey(Object key):检查是否包含指定的键。containsValue(Object value):检查是否包含指定的值。keySet():返回所有键的集合。entrySet():返回包含所有键值对的集合。replace(K key, V value):替换指定键的值。
代码示例分析
1. 使用 HashMap 存储键值对:
@Test
public void test1() {
HashMap<Integer, String> hashMap = new HashMap<>();
hashMap.put(1, "one");
hashMap.put(2, "two");
hashMap.put(3, "three");
System.out.println(hashMap);
// 获取值
String value = hashMap.get(3);
System.out.println(value); // 输出 "three"
// 删除指定的键值对
hashMap.remove(2);
System.out.println(hashMap); // 输出 {1=one, 3=three}
}
- 这里首先创建了一个
HashMap,并插入了几个键值对。 - 使用
get()方法获取了键为3的值,输出"three"。 - 使用
remove()删除了键为2的键值对,最后输出{1=one, 3=three}。
2. 检查键和值的存在性:
@Test
public void test2() {
HashMap<Integer, String> hashMap = new HashMap<>();
hashMap.put(1, "one");
hashMap.put(2, "two");
hashMap.put(3, "three");
// 检查键和值是否存在
System.out.println(hashMap.containsKey(1)); // 输出 true
System.out.println(hashMap.containsValue("two")); // 输出 true
}
- 使用
containsKey()检查是否存在键1,返回true。 - 使用
containsValue()检查是否存在值"two",返回true。
3. 替换值:
@Test
public void test3() {
HashMap<Integer, String> hashMap = new HashMap<>();
hashMap.put(1, "one");
hashMap.put(2, "two");
hashMap.put(3, "three");
// 替换键 1 的值
hashMap.replace(1, "Good");
System.out.println(hashMap); // 输出 {1=Good, 2=two, 3=three}
}
- 使用
replace()替换了键1的值为"Good",最终输出{1=Good, 2=two, 3=three}。
4. 获取所有键:
@Test
public void test4() {
HashMap<Integer, String> hashMap = new HashMap<>();
hashMap.put(1, "one");
hashMap.put(2, "two");
hashMap.put(3, "three");
// 获取所有键
Set<Integer> keys = hashMap.keySet();
System.out.println(keys); // 输出 [1, 2, 3]
}
- 使用
keySet()方法返回了HashMap中所有的键,输出[1, 2, 3]。
5. 获取所有键值对:
@Test
public void test5() {
HashMap<Integer, String> hashMap = new HashMap<>();
hashMap.put(1, "one");
hashMap.put(2, "two");
hashMap.put(3, "three");
// 获取所有键值对
Set<Map.Entry<Integer, String>> entrySet = hashMap.entrySet();
System.out.println(entrySet); // 输出 [1=one, 2=two, 3=three]
}
- 使用
entrySet()方法返回了HashMap中所有的键值对,输出[1=one, 2=two, 3=three]。
总结
HashMap是基于哈希表实现的键值对映射容器,提供了常用的插入、删除、查找等功能。- 常用方法包括
put()、get()、remove()、containsKey()、keySet()和entrySet()等。 - 在使用
HashMap时需要注意,它是无序的,键值对的顺序并不保证与插入顺序一致。
15. Map 的一些注意点
在使用 Map(如 HashMap)时,有几个关键的注意事项和最佳实践:
- 键的唯一性:
- 在
Map中,键是唯一的。如果你使用put()方法插入一个已经存在的键,会导致旧值被新值覆盖。 - 例如,在代码中
map.put(1, "Good")语句执行后,键1对应的值将从"one"被更新为"Good"。
- 在
- 替换操作:
- 如果你需要替换键对应的值,推荐使用
replace()方法而不是直接调用put()。虽然put()也会替换键的值,但replace()方法通常更具可读性,表明这是一个替换操作,而不是插入一个新的键值对。
- 如果你需要替换键对应的值,推荐使用
get()返回null:- 使用
get()方法时,如果指定的键不存在,Map会返回null。因此,在使用get()获取值时,需要检查是否为null以避免潜在的NullPointerException。
- 使用
代码分析
@Test
public void test() {
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
// 使用 put 插入或替换元素
map.put(1, "Good");
// map.replace(1, "Good"); // 使用 replace 替换元素
System.out.println(map); // 输出: {1=Good, 2=two, 3=three}
// 获取不存在的键,返回 null
System.out.println(map.get(4)); // 输出: null
}
- 键值对更新:
- 在
map.put(1, "Good")中,键1原本的值"one"被"Good"替换。 - 如果你想明确表示替换操作而不是插入新键值对,可以使用
map.replace(1, "Good")。
- 在
- 获取不存在的键:
map.get(4)返回null,因为键4不存在于map中。
总结
- 当需要替换已存在的键对应的值时,
replace()方法是一个更好的选择,因为它语义清晰,表达了替换的意图。 - 使用
get()方法时,注意检查返回值是否为null,以避免出现错误。
16. Entry与Map转换Set之后遍历
在这个例子中,展示了如何将 Map 中的键值对转换为 Set,然后利用 Iterator 对 Set 进行遍历,筛选出符合条件的值,并将其加入 ArrayList 中。
主要概念
Map.Entry:Map.Entry是Map中的一个内嵌接口,表示一个键值对 (key-value)。- 使用
Map.Entry可以方便地同时访问键和值。在遍历Map时,可以通过entrySet()获取所有的键值对Entry。
entrySet():entrySet()返回Map中所有键值对的Set视图,其中每个元素是Map.Entry类型。- 通过
entrySet()可以更方便地访问Map中的每一对键值,尤其是在需要同时使用键和值时。
Iterator:- 使用
Iterator迭代集合时,可以通过hasNext()判断是否还有元素,使用next()获取下一个元素。
- 使用
- 筛选条件:
- 在代码中,筛选了
Map中值大于等于 90 的项,并将符合条件的值添加到ArrayList中。
- 在代码中,筛选了
代码流程
@Test
public void test() {
HashMap<Integer, Integer> hashMap = new HashMap<>();
hashMap.put(10000, 11);
hashMap.put(10001, 53);
hashMap.put(10002, 95);
// 创建一个ArrayList
ArrayList<Integer> arrayList = new ArrayList<>();
// 将 HashMap 中的键值对转换为 Set
Set<Map.Entry<Integer, Integer>> entrySet = hashMap.entrySet();
System.out.println(entrySet); // 打印 Set 视图
// 获取迭代器
Iterator<Map.Entry<Integer, Integer>> iterator = entrySet.iterator();
// 通过迭代器遍历 entrySet
while (iterator.hasNext()) {
int value = iterator.next().getValue(); // 获取值
if (value >= 90) {
arrayList.add(value); // 将大于等于 90 的值添加到 arrayList 中
}
}
System.out.println(arrayList); // 打印筛选后的 ArrayList
}
输出结果
-
entrySet输出:[10000=11, 10001=53, 10002=95]这里打印的是
Map中所有的键值对,通过entrySet()转换为Set形式,展示了Map中的每个Entry。 -
筛选后的
arrayList输出:[95]由于只有
95满足>= 90的条件,它被添加到arrayList中。
总结
entrySet()方法非常适合在需要同时访问Map的键和值时使用。- 使用
Iterator遍历Set<Map.Entry>可以更加灵活地进行筛选和操作。 - 适合在内存操作中使用,
Map.Entry和entrySet()提供了方便的键值对处理方式,特别是当你需要对值进行筛选、修改或者其他操作时。
17. LinkedHashMap 和 HashMap
在这个代码中,演示了 HashMap 和 LinkedHashMap 的基本使用,它们的主要区别在于 顺序。
主要概念
HashMap:HashMap是无序的集合,插入的顺序和输出的顺序没有关系。- 它依赖于键的
hashCode值来存储数据,因此它不保证元素的顺序。
LinkedHashMap:LinkedHashMap是HashMap的一个子类,基于链表实现,能够保持插入顺序(或者访问顺序)。- 它比
HashMap多了一个 双向链表,用于维护元素的顺序。
代码流程
public class Main {
public static void main(String[] args) {
// 创建 HashMap 和 LinkedHashMap
HashMap<Integer, Integer> hashMap = new HashMap<>();
LinkedHashMap<Integer, Integer> linkedHashMap = new LinkedHashMap<>();
// 向 HashMap 和 LinkedHashMap 中添加元素
hashMap.put(1, 1);
hashMap.put(2, 2);
hashMap.put(3, 3);
linkedHashMap.put(1, 1);
linkedHashMap.put(2, 2);
linkedHashMap.put(3, 3);
// 打印 HashMap 和 LinkedHashMap
System.out.println(hashMap); // 输出无序的 HashMap
System.out.println(linkedHashMap); // 输出有序的 LinkedHashMap
}
}
输出结果
-
HashMap输出:{1=1, 2=2, 3=3}- 在
HashMap中,输出的顺序并不固定,虽然当前输出是按插入顺序,但它是无序的,下一次可能顺序不同。
- 在
-
LinkedHashMap输出:{1=1, 2=2, 3=3}- 在
LinkedHashMap中,输出的顺序是与插入顺序一致的,保持插入时的顺序。
- 在
总结
HashMap是无序的,数据的插入顺序和输出顺序没有任何关系。LinkedHashMap维护了插入顺序或者访问顺序,这意味着它会按照元素被插入的顺序来遍历元素。- 如果需要保证元素的顺序(比如展示时按照添加顺序),可以使用
LinkedHashMap。如果顺序不重要,可以使用HashMap。
18. 集合框架部分总结
已经了解了 Java 中常用的集合框架的基础知识,包括:
- List:如
ArrayList,LinkedList,它们是有序的集合,可以包含重复元素。 - Set:如
HashSet,LinkedHashSet,它们是无序集合,不允许重复元素。 - Map:如
HashMap,LinkedHashMap,它们是键值对集合,键是唯一的。 - 迭代器:如
Iterator,用来遍历集合中的元素。 - 性能差异:了解不同集合类型在插入、删除、查询时的性能差异。
- Entry:
Map.Entry用来遍历Map中的键值对。
后续学习建议
- 深入理解集合实现:
- 了解不同集合类是如何实现的。例如,
ArrayList是基于动态数组实现的,LinkedList是基于双向链表实现的,HashSet基于哈希表实现,而LinkedHashSet在哈希表的基础上增加了双向链表来维护元素的插入顺序。
- 了解不同集合类是如何实现的。例如,
- 学习 JDK 新特性:
- 你可以在后续学习框架(如 Spring 等)时接触到 JDK 新特性,例如:
Stream API、Optional、Lambda 表达式等。 - 学习这些新特性能够使你写出的代码更加简洁、高效。
- 你可以在后续学习框架(如 Spring 等)时接触到 JDK 新特性,例如:
- 学习 Java 框架:
Spring是目前 Java 中最流行的开发框架,学习框架时,你会接触到更多集合类的高级用法,如List、Set、Map与数据库的结合使用、JPA(Java Persistence API)等。- 可以在后期学习和实践时根据需要去掌握更多相关知识。
- Java 并发:
- 集合框架中的一些类(如
CopyOnWriteArrayList,ConcurrentHashMap)是线程安全的。掌握这些并发集合类以及多线程的知识,对你日后在 Java 后端开发中大有帮助。
- 集合框架中的一些类(如
下一步可以关注的方向:
- 深入学习 数据结构 和 算法,这对于编写高效的 Java 后端代码至关重要。
- 继续为 Java 后端开发 做准备,学习一些更高级的框架(如 Spring Boot, Spring Cloud 等)。

1449

被折叠的 条评论
为什么被折叠?



