Java 集合框架,泛型,包装类

文章目录

  • 集合框架
  • 泛型
    • Java 中的泛型
    • 裸类型(了解)
    • 原理
    • 泛型的上界
    • 泛型方法
    • 通配符
  • 包装类
  • ArrayList
    • 构造
    • 常见操作
  • LinkedList
  • Stack
  • Queue
  • PriorityQueue
  • Map
    • Map.Entry<K, V>
    • Map 常用方法
  • Set
    • 常用方法

集合框架

img

  • Vector 一个古老的集合类,实现了一个动态数组,现在已经不常用
  • Stack
  • ArrayList 顺序表
  • LinkedList 链表+队列+双端队列
  • PriorityQueue 优先级队列
  • HashSet 集合(不重复)(哈希表实现)
  • HashMap 哈希表
  • TreeSet 有序集合(红黑树)
  • TreeMap 有序键值对(红黑树)

泛型

Java 中的泛型

Java 中的泛型是通过在类、接口或方法的声明中使用尖括号 < > 来实现的。

public class Box<T> { // 类型参数也可以指定多个,用 ',' 分隔
    private T value;

    public Box(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }
}

Java 中泛型的类型参数不能直接是基本数据类型, 而应该是引用类型

Box<int> intBox = new Box<>(42); // 错误的
Box<Integer> intBox = new Box<>(42); // 正确的
Box<Integer> intBox = new Box<Integer>(42); // 后面的Integer可以写也可以省略

规范:类型形参一般使用一个大写字母表示,常用的名称有:

  • E 表示 Element
  • K 表示 Key
  • V 表示 Value
  • N 表示 Number
  • T 表示 Type
  • S, U, V 等等 - 第二、第三、第四个类型

裸类型(了解)

裸类型就是不指定类型实参

Box box = new box();

注意:一般不用裸类型,裸类型是 Java 为了兼容老版本

原理

Java 泛型的原理是基于类型擦除(Type Erasure)的概念。在编译时,Java 编译器会擦除泛型类型的信息,将泛型代码转换为普通的非泛型代码。这样,泛型的类型信息只存在于编译期,而在运行时是不可见的。

  1. 类型参数擦除: 在编译时,泛型类型参数会被替换为它们的边界或者 Object 类型。例如,List<String> 在运行时会被擦除为 List<Object>
  2. 桥方法(Bridge Methods): 泛型类和泛型接口的类型擦除可能导致擦除后的类或接口缺少某些方法。为了解决这个问题,编译器会生成桥方法,以确保子类或实现类仍然具有正确的类型。
  3. 泛型数组的限制: 由于数组在运行时需要知道元素的确切类型,Java 不允许创建泛型数组。因此,使用泛型数组可能会导致编译器警告或错误。

泛型的上界

泛型的上界是指泛型类型参数的限制,用于指定该参数必须是某个特定类型或其子类型。在Java中,通过使用 extends 关键字来指定上界。上界限制了可以传递给泛型类型参数的类型范围。

public class Box<T extends Number> {
    private T value;

    public Box(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }

    public static void main(String[] args) {
        // 使用泛型的上界,创建一个存储整数的盒子
        Box<Integer> intBox = new Box<>(42);

        // 使用泛型的上界,创建一个存储双精度浮点数的盒子
        Box<Double> doubleBox = new Box<>(3.14);

        // 下面的代码会导致编译错误,因为String不是Number的子类型
        // Box<String> stringBox = new Box<>("Hello");
    }
}

一个泛型上界的典型运用

// 求数组中的最大值
class Alg<T extends Comparable<T>> { // 设置上界 Comparable<T>
    public T findMax(T[] array) {
        T max = array[0];
        for (int i = 1; i < array.length; ++i) {
            if (max.compareTo(array[i]) < 0) { // 因为此处要对两个类型进行比较,所以必须是限定继承了Comparable接口
                max = array[i];
            }
        }
        return max;
    }
}

泛型方法

对上面的 Alg 类,我们发现 findMax() 方法的调用不需要依赖对象,所以可以设置为静态方法。

但是静态方法因为不直接访问类的实例,也就无法获取类中的泛型信息,无法使用泛型类型参数 T

要解决这个问题,可以把静态方法设置为泛型方法,将泛型参数放在方法的返回类型之前:

class Alg {
    public static <T extends Comparable<T>> T findMax(T[] array) {
        T max = array[0];
        for (int i = 1; i < array.length; ++i) {
            if (max.compareTo(array[i]) < 0) {
                max = array[i];
            }
        }
        return max;
    }
}

通配符

? 通配符: 表示未知类型,可以用在方法参数、方法返回类型、变量等地方。例如,在一个方法中接受一个未知类型的集合:

public static void printList(List<?> list) {
    for (Object element : list) {
        System.out.print(element + " ");
    }
    System.out.println();
}

通配符的上界

通配符是为了处理泛型协变问题而引入的,特别是 ? extends T 这种形式

比如我们有这样一个继承关系:

class Fruit {
    // ...
}

class Apple extends Fruit {
    // ...
}

class Box<T> {
    private T value;

    public Box(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }
}

Apple 是 Fruit 的子类,那么我们可以认为 Box<Apple>Box<Fruit> 的子类,可以写出以下代码:

public class Test {
    private static void fun(Box<Fruit> box) {
        System.out.println(box.getValue());
    }
    public static void main(String[] args) {
        Box<Apple> box = new Box<>(new Apple());
        fun(box); // 将 Box<Apple> 传给 Box<Fruit>,报错
    }
}

解决方式:

public class Test {
    private static void fun(Box<? extends Fruit> box) { // 加入通配符 ? extends Fruit ,正确
        System.out.println(box.getValue());
    }
    public static void main(String[] args) {
        Box<Apple> box = new Box<>(new Apple());
        fun(box);
    }
}

但是 fun 方法里是不能对 box 里的元素进行修改,因为 box 里面放的是 Fruit 及其子类,没有一个类型可以让 Fruit 及其子类 都能接收。

通配符的下界

使用 ? super T 通配符时,表示可以接受类型为 TT 的超类型的对象。这主要用于对泛型集合进行写入操作,允许向集合中添加 T 类型及其子类型的元素。

<? super 下界>

<? super Integer> // 表示可以传入的实参的类型是 Integer 或者 Integer 的父类

例子:

class Food {

}

class Fruit extends Food {
    // ...
}

class Apple extends Fruit {
    // ...
}

class Box<T> {
    private T value;

    public Box(T value) {
        this.value = value;
    }

    public void setValue(T newValue) {
        this.value = newValue;
    }

    public T getValue() {
        return value;
    }
}

public class Test {
    static void fun(Box<? super Fruit> box) { 
        // box 内可以存放 Fruit 及其子类的对象
        box.setValue(new Fruit()); // 行
//        box.setValue(new Food()); // 不行
        box.setValue(new Apple()); // 行
    }
    public static void main(String[] args) {
        Box<Food> box = new Box<>(new Food());
        // 类型参数 Food 是 Fruit 的超类,可以传参
        fun(box);
    }
}

因为 Box 里的元素是 Fruit 类型及其父类类型,那么这些类型一定可以接收 Fruit 的子类对象。而如果你读数据,读出来的是 Fruit 类型或其父类类型,你没有一个很好的类型去接收,如果使用向下转型又不安全。所以通配符的下界适合写数据,不适合读数据。

包装类

装箱:基本数据类型->包装类类型

int a = 10;
Integer b = a; // 自动装箱
Integer c = Integer.valueOf(a); // 手动装箱

拆箱:包装类类型->基本数据类型

Integer a = 20; // 自动装箱
int b = a;  // 自动拆箱
double d1 = a; // 自动拆箱

double d2 = a.doubleValue(); // 手动拆箱

特殊案例:

public static void main(String[] args) {
    Integer a = 100;
    Integer b = 100;
    Integer c = 200;
    Integer d = 200;
    System.out.println(a == b);
    System.out.println(c == d);
}
/* 输出:
true
false
*/

为什么值都是 100 就相等,值都是 200 的却不相等了呢?

装箱底层其实就是调用的 valueOf(),查看 valueOf() 源码:

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high) // low 为 -128,high 为 127
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

可以看到,当 i 在 -128 ~ 127 的时候就直接返回 cache 里面的对象了,如果超过了这个范围就会 new 一个新的对象。这就是导致上述问题的原因。

所以涉及引用类型比较大小,都应该使用对应的比较方法,而不是 == 号,包装类也不例外。

ArrayList

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
  • ArrayList 实现了 RandomAccess, Cloneable, java.io.Serializable 接口,表明它是可随机访问的,可以 clone 的,支持序列化的。
  • ArrayList 不是线程安全的
  • ArrayList 底层是一段连续的空间,并且可以动态扩容, 是一个动态类型的顺序表

构造

方法解释
ArrayList()构造一个初始容量为10的空列表
ArrayList(int initialCapacity)构造一个初始容量为 initialCapacity 的空列表
ArrayList(Collection<? extends E> c)利用其他 Collection 构造 ArrayList

问题1:调用无参构造后,ArrayList 的容量是多少?

答案:0

img

elementDataArrayList 底层维护的用来存储元素的数组,在调用无参构造后,只是把 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 这个空数组的引用赋给了它,所以容量还是 0

在第一次调用 add 时:

img

结论:第一次调用 add 的时候,底层数组 elementData 的容量才变成了10,如果只是调用无参构造方法,容量是0.


问题2:扩容是几倍扩

img

查看扩容方法 grow ,其中 int newCapacity = oldCapacity + (oldCapacity >> 1); 可以得知的 1.5 倍扩容


使用指定容量构造

img

如果指定的容量 > 0,就直接给你开对应容量的数组;如果 = 0,就把空数组传过来;如果 < 0 ,则抛非法参数异常

常见操作

方法解释
boolean add(E e)尾插,返回true
void add(int index, E element)在指定位置插入,如果index越界会抛异常
boolean addAll(Collection<? extends E> c)尾插一个集合的元素,可能抛空指针异常
boolean addAll(int index, Collection<? extends E> c)指定位置的版本,可能抛越界或空指针异常
E remove(int index)删除指定位置的元素,返回被删除的元素
boolean remove(Object o)删除第一个出现的 o
E get(int index)获取 index 位置的元素
E set(int index, E element)设置 index 下标的元素为 element
void clear()清空
boolean contains(Object o)查看 o 是否在 ArrayList 中
int indexOf(Object o)返回第一个 o 所在位置下标
int lastIndexOf(Object o)返回最后一个 o 所在位置下标
List<E> subList(int fromIndex, int toIndex)截取部分,返回的引用仍指向原顺序表

遍历

for循环,println(arrayList),foreach 都和普通数组一样。

迭代器

Iterator<Integer> it = arrayList.iterator(); // 获取迭代器对象, 也可以用listIterator()
while (it.hasNext()) { // 判断是否有下一个
    System.out.println(it.next()); // 访问,并向后走一步
}
  • iterator()Iterable接口定义的方法,返回的迭代器只能向前遍历,并且不支持在遍历过程中修改集合。
  • listIterator()List接口定义的方法,返回的迭代器可以向前和向后遍历,同时支持在遍历过程中对集合进行修改。
  • 迭代器获取的时候也可以传入下标,来获取指定位置的迭代器

LinkedList

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable

底层由链表实现

  • 支持无参构造和集合元素构造
  • 提供的常用操作方法和 ArrayList 的大同小异

Stack

方法解释
Stack()构造一个空的栈
E push(E item)入栈,并返回 item
E pop()出栈,并返回出栈的元素
E peek()获取栈顶元素
int size()获取栈中有效元素的个数
boolean empty()检测栈是否为空

Queue

Queue 是个接口,所以你并不能 new 它,而是 new 它的子类 LinkedList

Queue<Integer> queue = new LinkedList<>();
方法解释
boolean offer(E e)入队
E poll()出队
E peek()获取队头元素

PriorityQueue

默认创建小堆,容量为11

创建大堆,需要传比较器:

PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(new Comparator<Integer>() {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o2 - o1;
    }
});

如果是自定义类,也可以通过实现 Comparable 接口来指定比较规则

也可以用 lambda 表达式:

PriorityQueue<Integer> priorityQueue = new PriorityQueue<>((o1, o2) -> o2 - o1);

优先级队列的扩容说明:

  • 容量小于 64 时,2倍扩容
  • 容量大于等于64,1.5倍扩容
  • 容量超过 MAX_ARRAY_SIZE,按照 MAX_ARRAY_SIZE 扩容

Map

Map 是接口,K-V模型,使用时需要实例化它的子类 TreeMap 或 HashMap

Map.Entry<K, V>

Map.Entry<K, V> 是 Map 内部实现的用来存放 <key, value> 键值对映射关系的内部类,该内部类中主要提供了 <key, value> 的获取,value 的设置以及 key 的比较方式

方法解释
K getKey()返回 entry 中的 key
V getValue()返回 entry 中的 value
V setValue(V value)将 value 替换为指定的 value

注意:Key 不能设置

Map 常用方法

方法解释
V get(Object key)返回 key 对应的 value
V getOrDefault(Object key, V defaultValue)返回 key 对应的 value,如 key 不存在,返回默认值
V put(K key, V value)设置 key 对应的 value
V remove(Object key)删除 key 对应的映射关系
Set<K> keySet()返回所有 key 的不重复集合
Collection<V> values()返回所有 value 的可重复集合
Set<Map.Entry<K, V>> entrySet()返回所有的 key-value 映射关系
boolean containsKey(Object key)判断是否包含 key
boolean containsValue(Object value)判断是否包含 value

Set

Set 是接口,K 模型,使用时需要实例化它的子类 TreeSet 或 HashSet

常用方法

方法解释
boolean add(E e)添加元素,重复元素不会被添加成功
void clear()清空
boolean contains(Object o)判断 o 是否在集合中
Iterator<E> iterator()返回迭代器
boolean remove(Object o)删除
int size()返回元素个数
boolean isEmpty()判断是否为空
Object[] toArray()将Set转换为数组
boolean containsAll(Collection<?> c)判断集合 c 中的元素是否在Set中全部存在
boolean addAll(Collection<? extends E> c)将集合 c 中的元素添加到 Set 中,可以达到去重的效果

实现 Set 接口的常用类还有 LinkedHashSet,是在 HashSet 的基础上维护了一个双向链表来记录元素的插入次序

TreeSet 底层是 TreeMap 实现,查看源码:

public TreeSet() {
    this(new TreeMap<E,Object>()); // 因为是 K 模型,第二个类型参数给的是Object,是用来占位的
}

下面简单了解一下源码:

默认负载因子是0.75:

img

默认桶的初始大小为16:

img

只是初始化,桶的容量是 0,在第一次 put 的时候才会给哈希桶分配 16 的容量

桶的最大容量:

img

树化的条件1:链表的长度>=8

img

树化的条件2:桶的容量>=64

img

树退化的条件

img

哈希桶

img

计算哈希值的方式:

img

h >>> 16 位将高位移到低位,然后与原来的 h 异或,这样得到的 h ,低 16 位既有高位的信息又有低位的信息。

因为在 Java 的哈希表实现中,会使用二的幂次作为哈希表的大小,并使用位掩码进行索引计算。由于使用了二的幂次,高位的哈希值可能会在索引计算中失去一些信息,导致一些哈希冲突。为了减少这种冲突,采用了一种位传播的策略。

在 Java 中,哈希表的数组长度为2的幂。这是因为在使用二进制表示时,取模运算(%)可以被优化为位运算,即使用掩码进行操作,而不是昂贵的除法运算。这种优化可以提高性能。

例如,如果哈希表的长度为2^n(其中n是非负整数),那么对于任意正整数k,k % (2^n) 可以等效为 k & (2^n - 1)。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/153226.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

±15kV ESD 保护、3V-5.5V 供电、真 RS-232 收发器MS2232/MS2232T

产品简述 MS2232/MS2232T 芯片是集成电荷泵&#xff0c;具有 15kV ESD 保护的 RS-232 收发器&#xff0c;包括两路接收器、两路发送器。 芯片满足 TIA/EIA-232 标准&#xff0c;为异步通信控制器和串口连 接器提供通信接口。 芯片采用 3V-5.5V 供电&#xff0c;电荷泵仅用…

快速生成力扣链表题的链表,实现快速调试

关于力扣链表题需要本地调试创建链表的情况 我们在练习链表题&#xff0c;力扣官方需要会员&#xff0c;我们又不想开会员&#xff0c;想在本地调试给你们提供的代码 声明&#xff1a;本人也是参考的别人的代码&#xff0c;给你们提供不同语言生成链表 参考链接&#xff1a; 参…

最终方案(乱)

为什么要在mos管上并一个快恢复二极管 因为电机成感性&#xff0c;为了在关断期间给它提供一个续流回路

Linux 爱好者线下沙龙:成都场圆满结束 下一场西子湖畔相见 | LLUG·第五站

导读&#xff1a;第四站 LLUG成都场已于10 月 29 日在武侯区菁蓉汇成功举办。LLUG 第五站将于11 月 25 日走进美丽的西子湖畔&#xff0c;在这个冬日&#xff0c;LLUG 与你在杭州线下相见。 10 月 29 日&#xff0c;LLUG 成都场成功在武侯区菁蓉汇举办。 LLUG成都站由 Linux 中…

Redhat8.3上部署Lustre文件系统

Lustre文件系统 Lustre架构是用于集群的存储架构。Lustre架构的核心组件是Lustre文件系统&#xff0c;它在Linux操作系统上得到支持&#xff0c;并提供了一个符合POSIX *标准的UNIX文件系统接口。 Lustre存储架构用于许多不同类型的集群。它以支持世界上许多最大的拥有数万个…

2023数维杯国际赛数学建模竞赛选题建议及D题思路讲解

大家好呀&#xff0c;2023年第九届数维杯国际大学生数学建模挑战赛今天早上开赛啦&#xff0c;在这里先带来初步的选题建议及思路。 目前团队正在写B题和D题完整论文&#xff0c;后续还会持续更新哈&#xff0c;大家三连关注一下防止迷路。 注意&#xff0c;本文只是比较简略…

clip4clip:an empirical study of clip for end to end video clip retrieval

广告深度学习计算&#xff1a;阿里妈妈智能创意服务优化使用CPU/GPU分离的多进程架构&#xff0c;加速阿里妈妈智能创意服务。https://mp.weixin.qq.com/s/_pjhXrUZVzFRtiwG2LhnkwCLIP4Clip: CLIP 再下一城&#xff0c;利用CLIP实现视频检索 - 知乎前言&#xff1a; OpenAI 的论…

ubuntu 20.04安装 Anaconda教程

在安装Anaconda之前需要先安装ros(防止跟conda冲突&#xff0c;先装ros)。提前安装好cuda 和cudnn。 本博客参考&#xff1a;ubuntu20.04配置ros noetic和cuda&#xff0c;cudnn&#xff0c;anaconda&#xff0c;pytorch深度学习的环境 安装完conda后&#xff0c;输入: pyth…

CCRC认证是什么?

什么是CCRC认证&#xff1f; 信息安全服务资质&#xff0c;是信息安全服务机构提供安全服务的一种资格&#xff0c;包括法律地位、资源状况、管理水平、技术能力等方面的要求。 信息安全服务资质&#xff08;CCRC&#xff09;是依据国家法律法规、国家标准、行业标准和技术规范…

快手怎么涨粉最快?10个实用方法让你迅速积累粉丝

先来看实操成果&#xff0c;↑↑需要的同学可看我名字↖↖↖↖↖&#xff0c;或评论888无偿分享 各位知友们&#xff0c;大家好&#xff01;今天我来分享一些在快手涨粉的实用方法&#xff0c;让你迅速积累粉丝。如果你还没有注册快手账号&#xff0c;那么现在就赶紧去下载注册…

基于51单片机步进电机加减速正反转数码管显示( proteus仿真+程序+原理图+设计报告+讲解视频)

基于51单片机步进电机加减速正反转数码管显示( proteus仿真程序原理图设计报告讲解视频&#xff09; &#x1f4d1;1.主要功能&#xff1a;&#x1f4d1;讲解视频&#xff1a;&#x1f4d1;2.仿真&#x1f4d1;3. 程序代码&#x1f4d1;4. 设计报告&#x1f4d1;5. 设计资料内容…

电脑提示d3dcompiler43.dll缺失怎么解决?四种方法帮你轻松搞定!

d3dcompiler_43.dll是一个与DirectX相关的动态链接库&#xff08;DLL&#xff09;文件&#xff0c;它主要用于Windows操作系统上的图形和游戏应用程序。这个文件的主要作用是编译和解析DirectX应用程序中的图形代码。 DirectX是一个强大的图形API&#xff08;应用程序编程接口&…

图像生成colab集合

不过colab会做检测&#xff0c;一般文生图算法是基本很难跑起来的。 https://github.com/camenduruhttps://github.com/camenduru这哥们有很多colab。 1.stable-diffusion-webui https://colab.research.google.com/drive/1Iy-xW9t1-OQWhb0hNxueGij8phCyluOh#scrollTow3KNZ-…

一个反向代理神器 ——Nginx Proxy Manager

前言 上一期留了一个问题&#xff0c;我们怎么样才能把 IP 端口变成域名来访问&#xff1f; 答案是用反向代理。 看过之前几期视频的小伙伴应该知道&#xff0c;之前有宝塔的时候&#xff0c;碰到这个情况&#xff0c;我们会先新建一个站点&#xff0c;然后修改 Nginx 配置…

MyBatis关联映射深度解析

文章目录 关联映射基础一对一关联映射一对多关联映射多对多关联映射 延迟加载如何配置延迟加载 结语 &#x1f388;个人主页&#xff1a;程序员 小侯 &#x1f390;CSDN新晋作者 &#x1f389;欢迎 &#x1f44d;点赞✍评论⭐收藏 ✨收录专栏&#xff1a;MyBatis ✨文章内容&am…

如何在 Linux 上部署 RabbitMQ

如何在 Linux 上部署 RabbitMQ 文章目录 如何在 Linux 上部署 RabbitMQ安装 Erlang从预构建的二进制包安装从源代码编译 Erlang RabbitMQ 的安装使用 RabbitMQ Assistant 连接 RabbitMQ Assistant 是一款优秀的RabbitMQ 可视化管理工具&#xff0c;提供丰富的管理功能。下载地址…

【python】OpenCV—Rectangle, Circle, Selective Search(1.2)

文章目录 1 画框画圈1.1 画矩形框1.2 画圆 / 点1.3 椭圆 2 Selective Search3 Resize 1 画框画圈 1.1 画矩形框 # Copy the image img_rgb_copy img_rgb.copy()# Draw a rectangle cv2.rectangle(img_rgb_copy, pt1 (405, 90), pt2 (740, 510),color (255, 0, 0), thickne…

hash路由模式

hash模式 hash模式是一种把前端路由的路径用井号 # 拼接在浏览器 URL 后面的模式。 一个完整的 URL 包括&#xff1a;协议、域名、端口、虚拟目录、文件名、参数、锚。 https://www.wangyuegyq.top/utils/index.html?name123&phone123#home协议&#xff1a;https域名&am…

关于400G光模块的常见问题解答

最近在后台收到了很多用户咨询关于400G光模块的信息&#xff0c;那400G光模块作为当下主流的光模块类型&#xff0c;有哪些问题是备受关注的呢&#xff1f;下面来看看小易的详细解答&#xff01; 1、什么是400G QSFP-DD光模块&#xff1f; 答&#xff1a;400G光模块是指传输速…

《011.SpringBoot之餐厅点餐系统》

《011.SpringBoot之餐厅点餐系统》【界面简洁功能简单】 项目简介 需要源码及数据库的私信… [1]本系统涉及到的技术主要如下&#xff1a; 推荐环境配置&#xff1a;DEA jdk1.8 Maven MySQL 前后端分离; 后台&#xff1a;SpringBootMybatisPlus; 前台&#xff1a;Layuivue; …