JavaSE从零开始到精通(八) - 单列集合类

1. 集合概述

集合类是一种用于存储对象的容器,Java 提供了一组预定义的集合类,它们位于 java.util 包中。这些集合类提供了各种数据结构,包括列表、集合、队列、映射等,每种数据结构都有其独特的特性和用途。

Java 中的集合类是开发中不可或缺的重要部分,它们提供了一种管理和操作数据集合的有效方式。无论是简单的数组还是复杂的映射结构,Java 的集合框架都提供了广泛的选择,以满足不同场景下的需求。

2. 继承体系和顶级接口

1. Collection 接口

  1. 存储一组对象:Collection 接口是 Java 集合框架中最基本的接口之一,用于存储一组对象。

  2. 不固定大小:Collection 的实现类可以动态增长和缩减,没有固定大小的限制。

  3. 单元素访问:可以通过迭代器(Iterator)或者 foreach 循环来访问集合中的单个元素。

  4. 无序性:不同的 Collection 类可能以不同的方式存储和排序其元素。List(列表)接口的实现类按插入顺序存储元素;Set(集合)接口的实现类不允许重复元素,并且可能不维护特定的顺序。

  5. 泛型支持:从 Java 5 开始,Collection 接口及其实现类都支持泛型,允许在编译时检查元素的类型安全性。

  6. 基本操作方法:提供了基本的增加、删除、查找、遍历等操作方法,如 add、remove、contains、size 等。

  7. 不支持基本数据类型:Collection 接口不支持存储基本数据类型(int、float、char 等),只能存储对象。

  8. 继承关系:Collection 接口是所有集合框架的根接口,它派生出了 List、Set 和 Queue 接口,这些接口又分别对应了不同的集合类型和特性。

  1. add(Object element): 将指定的元素添加到集合中。
  2. remove(Object element): 从集合中移除指定的元素(如果存在)。
  3. clear(): 清空集合中的所有元素。
  4. isEmpty(): 判断集合是否为空,返回布尔值。
  5. size(): 返回集合中的元素数量。
  6. toArray(): 将集合转换为数组。
  7. contains(Object element): 判断集合是否包含指定的元素。
java 复制代码
 public static void main(String[] args) {
        //创建Collection对象
        Collection<String> c = new ArrayList<>();
        //完成添加
        c.add("张三");
        c.add("李四");
        c.add("王五");

        System.out.println(c);

        //删除指定的元素
        c.remove("张三");
        System.out.println(c);

        //判断当前集合是不是为空
        System.out.println(c.isEmpty());

        //判断当前集合中有没有包含指定的元素
        System.out.println(c.contains("李四"));

        //把当前集合中的元素转换到数组中返回
        Object[] objects = c.toArray();
        System.out.println(Arrays.toString(objects));


        //情况当前集合
        //c.clear();
        //System.out.println(c);

        //Collection集合的遍历
        System.out.println("==================");
        for (String name : c) {
            System.out.println(name);
        }

        //获取元素的个数
        System.out.println(c.size());
        //普通for循环 Collection不支持索引操作,所以Collection无法使用普通for循环遍历
        /*for (int i = 0; i < c.size(); i++) {
            //获取指定索引i处的元素

        }*/
    }

2. Map接口

  1. 键的唯一性 :每个键必须是唯一的,即同一个 Map 对象中不能包含重复的键。

  2. 键和值的映射关系:每个键都映射到一个值上,这种映射关系允许存储和获取值。

  3. 无序性Map 接口不保证元素的顺序,具体的顺序可能取决于具体的实现类。

  4. 允许 null 值和 null 键:HashMap 和 HashTable类的一个区别

  1. clear(): 清空 map 中的所有映射关系。
  2. containsKey(Object key): 判断 map 是否包含指定的键。
  3. containsValue(Object value): 判断 map 是否包含指定的值。
  4. entrySet(): 返回包含 map 中所有键值对映射关系的 Set 集合。
  5. equals(Object o): 比较指定对象与 map 是否相等。
  6. get(Object key) : 返回指定键所映射的值,如果键不存在则返回 null
  7. hashCode(): 返回 map 的哈希码值。
  8. isEmpty(): 判断 map 是否为空,返回布尔值。
  9. keySet(): 返回 map 中所有键的 Set 视图。
  10. put(K key, V value): 将指定的值与指定的键关联在 map 中,如果键已经存在则替换其对应的值。
  11. putAll(Map<? extends K, ? extends V> m): 将指定 map 中的所有映射关系复制到此 map 中。
  12. remove(Object key): 从 map 中移除指定键的映射关系,如果存在则返回与键关联的值。
  13. size(): 返回 map 中键值对的数量。
  14. values(): 返回 map 中所有值的 Collection 视图。

3. List集合

继承自 Collection 接口

特点:

  1. 元素可以重复
  2. 支持索引操作
  3. 存和取的顺序一致

程序运行时,数据主要存储在内存中。数组可提供更高的内存空间效率,而链表则在内存使用上更加灵活。

  • add(int index, Object element): 将指定的元素插入到列表中的指定位置。
  • get(int index): 获取列表中指定位置的元素。
  • remove(int index): 移除列表中指定位置的元素。
  • set(int index, Object element): 替换列表中指定位置的元素为新的元素值。

3.1 ArrayList 类

ArrayList 是基于动态数组实现的列表,支持随机访问,插入和删除元素相对较快。

1. 原理介绍:查询快的优点

数组在内存上是开辟的一块连续的空间。因此可以计算索引。这也是数组查询快的原因。

2. 原理介绍:插入和删除慢的缺点

在编程中,有优点就会有缺点,什么时候使用这个东西:在你使用它的便捷的时候,看你能否接受它带来的弊端。

  1. 固定大小: 数组的大小在创建时就确定了,如果需要插入或删除元素,通常需要进行元素的移动操作或者需要扩容。例如,如果在数组的中间位置插入一个元素,需要将插入位置之后的所有元素都向后移动一个位置,以腾出空间。

  2. 线性结构: 数组是一种线性结构,元素在内存中是连续存储的。因此,当需要移动元素时,需要按顺序逐个移动,这样的操作复杂度是 O(n),其中 n 是数组中元素的个数。

ArrayList底层扩层原理:扩展因子计算的长度小于实际的长度以实际长度,主要是不一定只添加一个元素。有可能会使用addAll()一次性添加多个元素 。

3.2 LinkedList 类

LinkedList 是基于双向链表实现的列表,适合插入和删除操作较频繁的场景。

  1. 不连续空间的优势:

内存空间是所有程序的公共资源,在一个复杂的系统运行环境下,空闲的内存空间可能散落在内存各处。我们知道,存储数组的内存空间必须是连续的,而当数组非常大时,内存可能无法提供如此大的连续空间。此时链表的灵活性优势就体现出来了。

这样插入和删除快的优势就体现出来了:

在链表中插入和删除节点非常容易。如图 4-6 所示,假设我们想在相邻的两个节点 n0n1 之间插入一个新节点 P则只需改变两个节点引用(指针)即可,时间复杂度为 𝑂(1) 。

  1. 不连续空间的劣势:

不连续空间互相是没有关联的。如果要保持关系,链表节点 ListNode 除了包含值,还需额外保存一个引用 (指针)。

在链表中访问节点的效率较低。如上一节所述,我们可以在 𝑂(1) 时间下访问数组中的任意元素。链表则不然,程序需要从头节点出发,逐个向后遍历,直至找到目标节点。也就是说,访问链表的第 𝑖 个节点需要循环 𝑖−1 轮,时间复杂度为 𝑂(𝑛) 。

4 Set 接口

继承自 Collection 接口

Set 是一种不允许重复元素的集合。

  1. 元素唯一

  2. 存和取的顺序可能不一致

  3. 不支持索引操作

4.1 TreeSet 类

TreeSet 是基于红黑树(一种自平衡二叉查找树)实现的有序集合。

  1. 有序原理

根节点出发,根据当前节点值和 num 的比较关系(自然比较器和自定义比较器)循环向下搜索,直到越过叶节点时跳出循环。

会按照自然排序或者自定义排序,从跟开始比较,一般:二者比较负数的去左子树比较,正数的去右子树比较

上述是Integer类型底层会自定义实现比较器,会进行自定义比较器。

1.1 自然排序

因为红黑树也属于二叉平衡搜索树,所以底层采用中序遍历,自然就是有序的了。

  1. 基本数据类型:底层包装类自动实现了,Comparable 接口,按照ASCII码表自然排序。

    上述跟踪Integer源码已经介绍

  2. 基本数据类型:需要自定义实现Comparable 接口,重写int compare(T o1, T o2)自定义排序规则。

    java 复制代码
    public class Student implements Comparable<Student> {
        private String name;
        private int age;
    
        public Student() {
        }
    
        public Student(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public int getAge() {
            return age;
        }
    
        public void setAge(int age) {
            this.age = age;
        }
    
        @Override
        public String toString() {
            return "Student{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    
        //提供比较规则: 固定的规则
        @Override
        public int compareTo(Student o) {
            return this.age - o.age;
        }
    }

1.2 比较器排序

在构造方法中传递比较器,外部比较器Comparator接口是一个函数式接口,

Comparator 一个比较函数,它对某些对象集合施加 总排序 。可以将比较器传递给排序方法(例如 Collections. sort 或 Arrays. sort),以允许对排序顺序进行精确控制。比较器还可用于控制某些数据结构(例如 排序集 或 排序映射)的顺序,或为没有 自然排序的对象集合提供排序。

会按照自然排序或者自定义排序,从跟开始比较,一般:二者比较负数的去左子树比较,正数的去右子树比较。

java 复制代码
 TreeSet<Student> stus2 = new TreeSet<>(new Comparator<Student>() {
            @Override
            public int compare(Student o1, Student o2) {
                return o2.getAge()-o1.getAge();
            }
        });

按照学生的年龄,升序排序

一般这种比较器都是:二分(用在有序数列中,预判无序要插入的位置)+插入

o1代表无序数列(未排序),o2代表有序数列(已排序)

  1. 负数:表示当前要插入的元素是小的,放在前面
  2. 正数:表示当前要插入的元素是大的,放在后面
  3. 0:表示当前要插入的元素跟现在的元素比是一样的们也会放在后面,但TreeSet会舍弃。

1.3 二者优先级对比

  1. 构造方法提供的比较器优先级高于自然排序: 如果你在创建 TreeSet 时使用了构造方法 TreeSet(Comparator<? super E> comparator),则 TreeSet 将使用这个比较器来排序元素,而不管元素是否实现了 Comparable 接口。这种情况下,比较器提供的排序方式优先于元素自然顺序。

  2. 自然排序: 如果没有提供比较器或者使用了 TreeSet 的默认构造方法 TreeSet(),那么 TreeSet 将使用元素的自然顺序(即元素类实现了 Comparable 接口,并定义了 compareTo 方法)来进行排序。这种情况下,只有当元素类实现了 Comparable 接口并且没有提供额外的比较器时,才会使用自然排序。

  3. 去重原理

按照自然排序和外部比较器排序,如果返回0,TreeSet会舍弃。

4.2 HashSet 类

HashSet 是基于哈希表实现的集合,不保证元素的顺序,但插入、删除和查找操作都很快速。

哈希表底层:数组+链表+红黑树 数组中的每个元素都是一个链表,如果hash冲突直接挂在链表下面,但在链表长度超过8,并且数组长度不小于64时会转换为红黑树,以提高查找效率。

字典就像一个"哈希表",能够快速查找目标词条。

  1. 去重原理

重写hashcode和equals方法,重写了,hashcode他是通过每个内容进行计算hashCode的值,然后求和进行返回的。

java 复制代码
   @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return age == student.age && Objects.equals(name, student.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }

注意:在Java中,三个连续的句点(...)在方法参数声明中表示可变长度参数(Varargs)。这种语法允许方法接受数量可变的参数。

为什么要重写hashcode()和equlas()方法?

  • 当使用默认的Object类的equals()方法时,它会比较两个对象的地址值是否相同,这与使用==操作符的效果类似。同样地,hashCode() 方法会根据对象的地址值计算散列码。散列码用于在散列存储结构中快速定位对象 ,因为它可以帮助确定对象的存储地址
  • 但是所有类都直接或者间接继承Object,默认都会存在一个父类的equals()和hashcode()方法,这种默认行为存在一个显著缺陷 :即使两个对象的所有属性都相同,它们通过equals()方法比较仍然会返回false,因为它们是由不同的内存空间实例化的,其地址值不同。这种情况在实际开发中并不符合预期,因为我们通常希望将所有属性相同的对象视为相等
  • 为了解决这个问题,我们可以重写equals()方法来定义对象相等的条件,使得即使是通过不同的内存地址实例化的对象,在所有属性相同的情况下,equals()方法也会返回true。然而,仅仅重写equals()方法是不够的。
  • 另一个需要注意的问题是,如果重写了equals()方法但没有同时重写hashCode()方法,那么虽然a.equals(b)返回true,但它们的hashCode()值可能不同。在使用散列集合(如HashMap、HashSet等)存储对象时,这会导致问题。因为散列集合根据对象的hashCode()值确定对象的存储位置,如果两个相等的对象具有不同的hashCode()值,它们可能会被存储在散列表的不同位置。这种情况下,尝试使用这两个对象获取数据可能会出现问题,因为它们实际上被视为不同的对象。
  • 因此,为了确保自定义类能够正确地与Java集合类协作,我们需要同时重写equals()和hashCode()方法。重写equals()方法以定义对象相等的条件,同时重写hashCode()方法以确保相等的对象具有相同的hashCode()值,从而保证它们在散列集合中的正确存储和检索行为。
  • 这样可以确保在使用自定义类与Java集合类(如HashMap、HashSet等)时,能够正确地处理对象的相等性和散列码的计算,从而避免潜在的错误和不一致行为。

5. Queue 接口

Queue 是一种先进先出(尾增头删)的数据结构,通常用于实现任务调度等场景。

顾名思义,队列模拟了排队现象,即新来的人不断加入队列尾部,而位于队列头部的人逐个离开。

  • peek(): 返回队列头部的元素,但不移除该元素。如果队列为空,则返回 null。
  • poll(): 检索并移除队列头部的元素。如果队列为空,则返回 null。

5.1 ArrayDeque 类

  • ArrayDeque 是基于数组实现的双端队列,支持在两端高效地插入和删除元素。

5.2 LinkedList 类

  • LinkedList 除了作为列表,也可以实现队列的功能,支持在头部和尾部插入和删除元素。

5.3 共同点和区别

  1. addFirst(E e)addLast(E e):

    • 将元素分别添加到双端队列的头部和尾部。
  2. offerFirst(E e)offerLast(E e):

    • 将元素分别添加到双端队列的头部和尾部,如果成功则返回 true,否则返回 false
  3. removeFirst()removeLast():

    • 移除并返回双端队列的头部和尾部元素,如果队列为空则抛出异常。
  4. pollFirst()pollLast():

    • 移除并返回双端队列的头部和尾部元素,如果队列为空则返回 null
  5. getFirst()getLast():

    • 获取但不移除双端队列的头部和尾部元素,如果队列为空则抛出异常。
  6. peekFirst()peekLast():

    • 获取但不移除双端队列的头部和尾部元素,如果队列为空则返回 null
  • ArrayDeque 使用动态数组实现,因此在大多数操作上比较高效,尤其是随机访问和尾部操作。
  • LinkedList 使用双向链表实现,插入和删除操作效率高,但随机访问较差。
  • 双端队列的特有方法主要是针对头部和尾部操作的优化,适合需要频繁在两端进行操作的场景。

5.4 应用场景

  • 淘宝订单 。购物者下单后,订单将加入队列中,系统随后会根据顺序处理队列中的订单。在双十一期间,短时间内会产生海量订单,高并发成为工程师们需要重点攻克的问题。这种订单系统就是RabbitMQ等消息队列创建的意义。

  • 各类待办事项。任何需要实现"先来后到"功能的场景,例如打印机的任务队列、餐厅的出餐队列等,队列在这些场景中可以有效地维护处理顺序。

  • 广度优先搜索(BFS):

    • 在图论中,广度优先搜索使用队列来实现。它按层次顺序逐个访问图中的顶点,从而非常适合用于寻找最短路径或者在状态空间中找到最短路径的问题。
  • 缓存处理:

    • 队列常用于实现缓存机制,例如最近最少使用(LRU,Least Recently Used)缓存。新的数据项被添加到队列的尾部,而最老的数据项则被从队列的头部移除。
  • 任务调度:

    • 在操作系统中,队列可以用于管理任务调度。例如,作业调度和进程调度算法可以使用队列来存储和管理待处理的任务或进程。
  • 消息传递:

    • 在并发编程和系统设计中,消息队列常用于不同组件之间的通信和消息传递。消息队列可以帮助解耦系统的不同部分,并确保消息的有序处理。
  • 广告请求处理:

    • 在互联网广告技术中,队列可以用来管理广告请求的排队和处理,确保广告请求按照正确的顺序被服务。
  • 并行计算:

    • 在并行计算和分布式系统中,队列可以用于任务分发和结果收集,帮助管理和协调多个计算单元的工作。
  • 事件驱动:

    • 队列可以作为事件驱动系统的一部分,用于存储和处理异步事件,如用户交互事件、网络消息等。
  • 数据流处理:

    • 在数据流处理系统中,队列常用于流水线处理,确保数据按顺序被处理和传递。

6. Stack 实现类

++栈++ 是一种遵循先入后出 (头增头删)逻辑的线性数据结构。

我们可以将栈类比为桌面上的一摞盘子,如果想取出底部的盘子,则需要先将上面的盘子依次移走。我们将盘子替换为各种类型的元素(如整数、字符、对象等),就得到了栈这种数据结构。

栈可以视为一种受限制的数组或链表。换句话说,我们可以 "屏蔽" 数组或链表的部分无关操作,使其对外表现的逻辑符合栈的特性。

数组 实现额外支持随机访问,但这已超出了栈的定义范畴,因此一般不会用到。效率并不高。

  1. push(E element):

    • 将元素推入堆栈的顶部。即将元素压入堆栈。
  2. pop():

    • 移除并返回堆栈顶部的元素。即从堆栈中弹出元素。
  3. peek():

    • 返回堆栈顶部的元素而不将其从堆栈中移除。即查看堆栈顶部的元素但不移除。
  4. empty():

    • 检查堆栈是否为空。如果堆栈中没有元素,则返回true,否则返回false。
  5. search(Object o):

    • 查找给定元素在堆栈中的位置,并返回距离堆栈顶部最近的位置编号。如果元素不存在于堆栈中,则返回-1。

实际是属于List接口的实现类,为了方便介绍栈才单独放一个标题。

栈在算法的使用

  1. 函数调用

    • 许多编程语言使用栈来管理函数调用。每当一个函数被调用时,其局部变量和函数参数都会被压入栈中,当函数执行完毕时,这些数据就会从栈中弹出,控制权返回到调用函数。
  2. 表达式求值

    • 栈可以用于计算和解析数学表达式,如中缀表达式转换为后缀表达式(逆波兰表达式),以及后缀表达式的计算。这是因为栈能有效地处理操作符和操作数的顺序。
  3. 深度优先搜索(DFS)

    • 在图论中,DFS通常使用递归或显式栈来实现。它通过在每一步选择最后发现的顶点来遍历图,因此栈非常适合保存当前路径的顶点序列。
  4. 迷宫求解和路径查找

    • 使用深度优先搜索时,栈可以帮助跟踪从起点到目标点的路径,以及找到迷宫的解决方案。
  5. 语法分析

    • 栈在编译器和解析器中用于处理语法分析和语法树的构建。语法分析器通常使用栈来验证和处理语法结构。
  6. 回溯算法

    • 回溯算法是一种递归算法,通常使用栈来保存当前路径和状态。当需要进行回溯时,可以将状态从栈中弹出,以返回之前的状态并继续搜索。
  7. 括号匹配

    • 栈非常适合解决括号匹配问题,例如检查一个字符串中的括号是否匹配和嵌套是否正确。

7. 总结

  1. 学习Java集合框架对于成为高效的Java开发人员至关重要。Java集合框架提供了丰富的数据结构,如List、Set、Map等,每种结构都有其独特的特点和适用场景。通过学习集合框架,开发人员能够选择最适合特定需求的数据结构,从而优化程序的性能和内存使用效率。比如,ArrayList适合需要快速访问和遍历的场景,HashSet则适合需要快速查找和保证唯一性的情况。
  2. 掌握集合框架不仅仅是了解各种数据结构,还包括对集合操作和算法的熟练掌握。例如,熟悉如何在集合中添加、删除和查找元素,以及如何使用迭代器进行遍历操作。这些技能不仅提升了代码编写的效率,还能够提高代码的可读性和可维护性。
  3. 在实际项目中,集合框架被广泛应用于数据管理、算法实现、缓存处理等各个方面。了解每种集合类的时间复杂度和空间复杂度对于优化算法实现和提高程序性能至关重要。此外,集合框架的深入理解也是面试中常见的考察点,能够展示出开发者对Java核心技术的掌握程度和实际应用能力。
  4. 总之,学习Java集合框架不仅是提高个人技术水平的必经之路,也是成为一名优秀Java开发人员的基础。精通集合框架将帮助开发者在项目中更加高效地处理数据和算法,从而提升整体的开发效率和质量。

这篇文章肝了好久,如果对你有帮助的话,希望你点个赞,此外晚安。

相关推荐
轮到我狗叫了32 分钟前
栈的应用,力扣394.字符串解码力扣946.验证栈序列力扣429.N叉树的层序遍历力扣103.二叉树的锯齿形层序遍历
java·算法·leetcode
小柯J桑_36 分钟前
C++:探索AVL树旋转的奥秘
开发语言·c++·avl树
冰之杍2 小时前
Vscode进行Java开发环境搭建
java·ide·vscode
skaiuijing2 小时前
Sparrow系列拓展篇:消息队列和互斥锁等IPC机制的设计
c语言·开发语言·算法·操作系统·arm
雯0609~4 小时前
c#:winform调用bartender实现打印(学习整理笔记)
开发语言·c#
胜天半子_王二_王半仙5 小时前
c++源码阅读__smart_ptr__正文阅读
开发语言·c++·开源
沐泽Mu5 小时前
嵌入式学习-C嘎嘎-Day08
开发语言·c++·算法
Non importa5 小时前
汉诺塔(hanio)--C语言函数递归
c语言·开发语言·算法·学习方法
LinuxST5 小时前
27、基于Firefly-rk3399中断休眠唤醒实验(按键中断)
linux·开发语言·stm32·嵌入式硬件
Tony_long74835 小时前
Python学习——猜拳小游戏
开发语言·python·学习