Java API:8. Java collections framework

目录

0. 原生数组带来的问题

这个示例展示了原生数组在使用时可能带来的一些问题,特别是 数组越界 的问题。我们通过 array[3] = 4; 强行访问了索引超出边界的位置,从而抛出了 ArrayIndexOutOfBoundsException 异常。

1. 数组的基本问题
  • 固定大小:数组一旦定义大小(如 new int[SIZE]),它的长度是固定的。增删改查(CRUD)操作时,如果需要动态扩展或缩小数组,就需要手动处理数组的大小和元素移动。
  • 边界问题:如果在操作数组时不小心访问了越界位置(如数组索引超出定义的范围),就会抛出 ArrayIndexOutOfBoundsException 异常。比如代码中的 array[3] = 4; 就会抛出该异常,因为数组的有效索引是 02,索引 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集合框架大致可以分为两大体系:CollectionMap

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 中非常常用的集合类,它与普通数组的最大区别是它没有固定的大小限制,能够动态调整大小。我们可以自由地向其中添加、删除元素,非常适用于需要动态增删元素的场景。

代码分析:
  1. 无泛型的 ArrayList (arrayListTest1)
    • 默认情况下,ArrayList 可以存储任意类型的数据,但这种做法不推荐,因为它缺少类型安全。
    • 在此代码中,ArrayList 被用来存储不同类型的元素,如 StringIntegerCharacter
  2. 使用泛型的 ArrayList (arrayListTest2)
    • 通过泛型来限定集合中存储的元素类型,这样可以保证类型安全,避免运行时类型转换异常。
    • 在此示例中,ArrayList 被限定为存储 String 类型的数据。
  3. 使用对象 (arrayListTest3)
    • Student1 类是一个简单的对象类,用于演示如何将对象添加到 ArrayList 中,并且通过 toString() 方法来输出对象信息。
  4. 自定义对象的 ArrayList (arrayListTest4arrayListTest5)
    • 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 循环等。这些操作能够帮助我们更灵活地操作集合。

代码分析:
  1. 插入元素 (arrayListTest1)
    • 使用 .add() 方法可以向集合中添加元素,arrayList.add(0, 4) 可以在指定位置插入元素(例如,在索引位置 0 插入元素 4)。
  2. 合并两个集合 (arrayListTest2)
    • .addAll() 方法将一个集合的所有元素添加到另一个集合后面,这样可以合并两个集合。
  3. 转换为数组 (arrayListTest3)
    • .toArray() 方法将 ArrayList 转换为数组形式,并返回一个 Object[] 数组。
  4. 清除所有元素 (arrayListTest4)
    • .clear() 方法可以清空 ArrayList 中的所有元素。
  5. 判断元素是否存在 (arrayListTest5)
    • .contains() 方法用于判断集合中是否包含某个元素,返回一个布尔值。
  6. 遍历集合 (arrayListTest6)
    • 使用 .get() 方法可以获取指定索引位置的元素,通过 for 循环遍历集合中的每个元素。
  7. 增强 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 的常见操作,包括查找元素、删除元素、修改元素、排序、逆序等功能。这些操作能帮助你更灵活地处理和操作集合。

代码分析:
  1. 查找元素的位置 (arrayListTest1)
    • .indexOf() 返回元素首次出现的位置(索引)。
    • .lastIndexOf() 返回元素最后一次出现的位置(索引)。
  2. 判断集合是否为空 (arrayListTest2)
    • .isEmpty() 方法返回 truefalse,判断集合是否为空。
  3. 删除元素 (arrayListTest3)
    • .remove() 可以根据元素或索引删除集合中的元素。
  4. 移除所有元素 (arrayListTest4)
    • .removeAll() 方法用于删除集合中所有的元素。
  5. 替换集合中的所有元素 (arrayListTest5)
    • .replaceAll() 方法用来修改集合中所有元素的值。这里我们使用 toLowerCase()toUpperCase() 来改变大小写。
  6. 取交集 (arrayListTest6)
    • .retainAll() 方法用于取两个集合的交集。
  7. 修改指定位置的元素 (arrayListTest7)
    • .set() 方法用于修改指定位置的元素。
  8. 排序集合 (arrayListTest8)
    • Collections.sort() 方法用于对集合进行升序排序。
  9. 反转集合 (arrayListTest9)
    • Collections.reverse() 方法用于将集合中的元素反转。
  10. 获取子列表 (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 是基于双向链表实现的,因此它具有不同的性能特征。

LinkedListArrayList 的区别:
  • 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)是一种常见的数据结构,区别于数组,它并不按顺序在内存中存储数据,而是通过指针(或引用)将元素连接起来。链表的每个元素被称为 节点,每个节点包含数据和指向下一个节点的引用。

链表的分类
  1. 单向链表:每个节点包含一个指向下一个节点的指针(或引用)。它只能从头节点向尾节点遍历,不能反向遍历。
  2. 双向链表:每个节点包含两个指针,分别指向前一个节点和后一个节点。可以双向遍历。
LinkedList 在 Java 中的实现

在 Java 中,LinkedList 类是基于双向链表实现的,属于 java.util 包,提供了链表的常见操作。与 ArrayList 类似,LinkedList 也实现了 List 接口。

LinkedList 常用方法

  1. 添加元素
    • .add(E e):在链表尾部添加一个元素。
    • .addFirst(E e):在链表头部添加一个元素。
    • .addLast(E e):在链表尾部添加一个元素(与 .add() 等效)。
  2. 删除元素
    • .removeFirst():删除链表头部的元素。
    • .removeLast():删除链表尾部的元素。
    • .remove(Object o):删除指定元素。
  3. 获取元素
    • .getFirst():获取链表头部的元素。
    • .getLast():获取链表尾部的元素。
    • .get(index):根据索引获取指定位置的元素。
  4. 其他操作
    • .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 接口的所有实现类(如 ArrayListLinkedList 等),无论元素的存储方式如何。

迭代器的特点:
  1. 无序性:迭代器对集合中的元素进行遍历时,不关心元素的顺序。
  2. 支持移除操作:除了遍历,迭代器还支持在遍历过程中安全地移除元素。
  3. 不适用于数组:迭代器只适用于实现了 Collection 接口的类(如 ListSet),不适用于原生数组。

Iterator 常用方法

  1. hasNext():检查是否有下一个元素。
  2. next():返回当前元素,并将迭代器指针移至下一个元素。
  3. 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

解析:

  1. Iterator 的创建

    • arrayList.iterator()linkedList.iterator() 分别返回 ArrayListLinkedList 的迭代器。
  2. 遍历

    • hasNext() 方法检查集合中是否还有元素可以遍历。
    • next() 方法获取当前元素,并使迭代器指针移动到下一个元素。

总结:

  • 迭代器(Iterator)提供了一个通用的方式来遍历集合,适用于 ArrayListLinkedList 等实现了 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 接口的类(如 ArrayListLinkedList 等)。

总结与对比

方式适用场景优缺点注意事项
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 中,forfor-each(增强 for)和 Iterator 都是常见的集合遍历方式。它们在不同类型的集合(如 ArrayListLinkedList)中的性能表现有很大的差异,下面我们来讨论它们的性能差异。

1. ArrayList 的性能对比

ArrayList 是基于动态数组实现的,能够通过索引直接访问元素,因此在访问元素时表现非常高效。遍历时,三种方式的性能差距相对较小,但有细微差别。

性能分析
  • for(传统 for 循环):由于 ArrayList 是基于数组实现的,可以通过索引直接访问元素,因此传统的 for 循环能够快速地访问和修改元素。使用 for 循环时,数组下标是连续的,且访问时间是 O(1)。
  • for-each(增强 for 循环):增强 for 循环依赖于 Iterator 来遍历集合,实际的性能也非常接近 for 循环,尤其是对于 ArrayList 来说,Iteratorfor-each 循环的差异相对较小。
  • Iterator(迭代器):虽然 Iterator 需要额外的对象创建和方法调用,但由于 ArrayList 本身支持快速随机访问,因此 Iterator 的性能差距不大,适用于需要在遍历时删除元素的场景。
性能顺序
for > Iterator > for-each
  • for 循环最快,直接使用索引。
  • Iteratorfor-each 性能几乎相同,Iterator 稍微慢一些,因为需要额外调用方法。
总结
  • 对于 ArrayList,遍历速度的差距非常小,一般情况下,for 或者 for-each 都是可接受的选择。如果需要删除元素,使用 Iterator 会更安全。

2. LinkedList 的性能对比

LinkedList 是基于双向链表实现的,不支持随机访问元素,因此它的遍历性能和 ArrayList 存在显著差异。对于 LinkedListfor 循环的性能表现最差,因为每次通过索引访问元素都需要遍历链表直到目标位置。

性能分析
  • 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 最慢,因为每次访问元素都需要遍历链表。
总结
  • 对于 LinkedListIteratorfor-each 循环具有明显的性能优势。尤其在数据量大的时候,for 循环的性能差距非常明显。
  • 推荐使用 for-eachIterator 来遍历 LinkedList

3. 性能总结:

集合类型遍历方式性能顺序适用场景
ArrayListforfor > Iterator > for-each如果不需要删除元素,forfor-each 足够。
IteratorIterator 适用于需要删除元素时。
for-each当不需要修改集合时,for-each 更简洁。
LinkedListforIterator > for-each >>> forIteratorfor-each 性能较好,for 性能差。
Iterator推荐在 LinkedList 中使用 Iteratorfor-each
for-each

4. 性能结论:

  • 对于 ArrayList,遍历的性能差距较小,一般使用 forfor-each 就可以了。如果需要删除元素,推荐使用 Iterator
  • 对于 LinkedList,由于它是基于链表结构的,for 循环性能较差,特别是当数据量大的时候,Iteratorfor-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

通过实际的运行结果,可以看到 ArrayListLinkedList 中不同方式的遍历性能差异,特别是在 LinkedList 中,for 循环的性能差距更为明显。


12. HashSetSet 介绍

HashSet 是 Java 中一个常用的集合类,它实现了 Set 接口。HashSet 基于 HashMap 来实现,因此其特点和 HashMap 有一些相似之处,主要是元素的存储和访问方式。

特性
  1. 无序HashSet 内部存储的元素是无序的。即使你按照某种顺序添加元素,打印时也不会按照插入顺序输出。元素的顺序由其 hashCode() 方法决定。
  2. 不允许重复元素HashSet 通过 hashCode()equals() 方法来判断元素是否相同。只要 hashCode()equals() 的值相同,HashSet 就认为是重复的,不会添加。
  3. 允许 nullHashSet 允许存储一个 null 元素。
  4. 线程不安全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 介绍

LinkedHashSetHashSet 的一种变种,提供了元素的插入顺序。与 HashSet 不同,LinkedHashSet 保持了元素的插入顺序。这意味着你可以按插入的顺序迭代元素。

特性
  1. 有序性LinkedHashSet 会根据元素的插入顺序维护元素的顺序,迭代时会按元素插入的顺序返回元素。
  2. 不允许重复元素:和 HashSet 一样,LinkedHashSet 也不允许重复元素。如果你尝试插入相同的元素,它不会被添加。
  3. 允许 nullLinkedHashSet 允许插入 null 值。
  4. 线程不安全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" 只会出现一次。

其他相关概念:

  • LinkedHashSet vs HashSetLinkedHashSetHashSet 的主要区别在于,LinkedHashSet 保留元素的插入顺序,而 HashSet 则是无序的,元素的顺序不确定。
使用场景
  • LinkedHashSet 适用于需要去重并且同时保持插入顺序的场景。如果你希望在不重复的情况下,保留元素插入的顺序,LinkedHashSet 是一个很好的选择。

总结:

  • LinkedHashSet 在去重的基础上,提供了插入顺序的保持,适用于需要有序遍历的场景。它的性能与 HashSet 相似,但由于额外维护了插入顺序,插入和删除操作的效率略低于 HashSet

14. MapHashMap 介绍

Map 是一个集合接口,表示一组键值对 (key-value) 映射关系。它通过键 (key) 存储和查找数据,每个键都对应一个唯一的值。常见的实现类有 HashMapTreeMapLinkedHashMap 等。

HashMapMap 的一个实现,它基于哈希表(Hash Table)实现,提供了快速的插入、删除、查找等操作。它不保证元素的顺序,且允许 null 作为键或值。

特性
  • 键值对存储HashMap 存储的数据是以键值对的形式存储的,每个键(Key)都唯一地对应一个值(Value)。
  • 不保证顺序HashMap 中的元素是无序的,元素的存储顺序不确定。
  • 允许 nullHashMap 允许一个键为 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)时,有几个关键的注意事项和最佳实践:

  1. 键的唯一性
    • Map 中,键是唯一的。如果你使用 put() 方法插入一个已经存在的键,会导致旧值被新值覆盖。
    • 例如,在代码中 map.put(1, "Good") 语句执行后,键 1 对应的值将从 "one" 被更新为 "Good"
  2. 替换操作
    • 如果你需要替换键对应的值,推荐使用 replace() 方法而不是直接调用 put()。虽然 put() 也会替换键的值,但 replace() 方法通常更具可读性,表明这是一个替换操作,而不是插入一个新的键值对。
  3. 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,然后利用 IteratorSet 进行遍历,筛选出符合条件的值,并将其加入 ArrayList 中。

主要概念

  1. Map.Entry:
    • Map.EntryMap 中的一个内嵌接口,表示一个键值对 (key-value)。
    • 使用 Map.Entry 可以方便地同时访问键和值。在遍历 Map 时,可以通过 entrySet() 获取所有的键值对 Entry
  2. entrySet():
    • entrySet() 返回 Map 中所有键值对的 Set 视图,其中每个元素是 Map.Entry 类型。
    • 通过 entrySet() 可以更方便地访问 Map 中的每一对键值,尤其是在需要同时使用键和值时。
  3. Iterator:
    • 使用 Iterator 迭代集合时,可以通过 hasNext() 判断是否还有元素,使用 next() 获取下一个元素。
  4. 筛选条件
    • 在代码中,筛选了 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
}

输出结果

  1. entrySet 输出:

    [10000=11, 10001=53, 10002=95]
    

    这里打印的是 Map 中所有的键值对,通过 entrySet() 转换为 Set 形式,展示了 Map 中的每个 Entry

  2. 筛选后的 arrayList 输出:

    [95]
    

    由于只有 95 满足 >= 90 的条件,它被添加到 arrayList 中。

总结

  • entrySet() 方法非常适合在需要同时访问 Map 的键和值时使用。
  • 使用 Iterator 遍历 Set<Map.Entry> 可以更加灵活地进行筛选和操作。
  • 适合在内存操作中使用,Map.EntryentrySet() 提供了方便的键值对处理方式,特别是当你需要对值进行筛选、修改或者其他操作时。

17. LinkedHashMapHashMap

在这个代码中,演示了 HashMapLinkedHashMap 的基本使用,它们的主要区别在于 顺序

主要概念

  1. HashMap:
    • HashMap 是无序的集合,插入的顺序和输出的顺序没有关系。
    • 它依赖于键的 hashCode 值来存储数据,因此它不保证元素的顺序。
  2. LinkedHashMap:
    • LinkedHashMapHashMap 的一个子类,基于链表实现,能够保持插入顺序(或者访问顺序)。
    • 它比 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
    }
}

输出结果

  1. HashMap 输出:

    {1=1, 2=2, 3=3}
    
    • HashMap 中,输出的顺序并不固定,虽然当前输出是按插入顺序,但它是无序的,下一次可能顺序不同。
  2. LinkedHashMap 输出:

    {1=1, 2=2, 3=3}
    
    • LinkedHashMap 中,输出的顺序是与插入顺序一致的,保持插入时的顺序。

总结

  • HashMap 是无序的,数据的插入顺序和输出顺序没有任何关系。
  • LinkedHashMap 维护了插入顺序或者访问顺序,这意味着它会按照元素被插入的顺序来遍历元素。
  • 如果需要保证元素的顺序(比如展示时按照添加顺序),可以使用 LinkedHashMap。如果顺序不重要,可以使用 HashMap

18. 集合框架部分总结

已经了解了 Java 中常用的集合框架的基础知识,包括:

  1. List:如 ArrayList, LinkedList,它们是有序的集合,可以包含重复元素。
  2. Set:如 HashSet, LinkedHashSet,它们是无序集合,不允许重复元素。
  3. Map:如 HashMap, LinkedHashMap,它们是键值对集合,键是唯一的。
  4. 迭代器:如 Iterator,用来遍历集合中的元素。
  5. 性能差异:了解不同集合类型在插入、删除、查询时的性能差异。
  6. EntryMap.Entry 用来遍历 Map 中的键值对。

后续学习建议

  1. 深入理解集合实现
    • 了解不同集合类是如何实现的。例如,ArrayList 是基于动态数组实现的,LinkedList 是基于双向链表实现的,HashSet 基于哈希表实现,而 LinkedHashSet 在哈希表的基础上增加了双向链表来维护元素的插入顺序。
  2. 学习 JDK 新特性
    • 你可以在后续学习框架(如 Spring 等)时接触到 JDK 新特性,例如:Stream APIOptionalLambda 表达式等。
    • 学习这些新特性能够使你写出的代码更加简洁、高效。
  3. 学习 Java 框架
    • Spring 是目前 Java 中最流行的开发框架,学习框架时,你会接触到更多集合类的高级用法,如 ListSetMap 与数据库的结合使用、JPA(Java Persistence API)等。
    • 可以在后期学习和实践时根据需要去掌握更多相关知识。
  4. Java 并发
    • 集合框架中的一些类(如 CopyOnWriteArrayList, ConcurrentHashMap)是线程安全的。掌握这些并发集合类以及多线程的知识,对你日后在 Java 后端开发中大有帮助。

下一步可以关注的方向:

  • 深入学习 数据结构算法,这对于编写高效的 Java 后端代码至关重要。
  • 继续为 Java 后端开发 做准备,学习一些更高级的框架(如 Spring Boot, Spring Cloud 等)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值