当前位置: 首页 > news >正文

Java集合框架深度剖析:结构、并发与设计模式全解析


title: 2.集合框架


目录

1.集合框架概述与基本使用

学习并理解 Java 集合框架的整体结构及其各个常见集合类。

2.底层实现及原理

深入研究集合框架背后的底层实现。

3.线程安全与并发场景

了解在多线程环境中,如何保证集合的线程安全。

4.设计模式与源码设计思想

探讨集合框架中的设计模式(如工厂模式、单例模式等)及其在源码中的应用。

1.集合框架概述与基本使用

集合体系结构概览

Java 的集合框架(Collection Framework)是一个强大且统一的 API,用于存储和操作对象数据。它大致可以分为两大体系:

Collection 与 Map 的两大体系

  • Collection 接口是集合层次结构的根,用于表示一组对象(元素),其子接口包括:
    • List:有序集合,允许重复元素。
    • Set:不允许重复元素,无序或有序视实现而定。
    • Queue:支持队列操作的集合,通常用于按一定顺序处理元素(如 FIFO)。
  • Map 接口独立于 Collection,用于存储**键值对(key-value)**映射关系:
    • 键唯一,值可重复。
    • 典型实现包括 HashMapTreeMapLinkedHashMap 等。

Collection 下的 List、Set、Queue 简介

接口特点常见实现类
List有序,可重复ArrayList, LinkedList, Vector
Set无序,不可重复HashSet, LinkedHashSet, TreeSet
Queue先进先出(FIFO)LinkedList, PriorityQueue, ArrayDeque

• Map 接口的独立地位

  • Map 不继承 Collection,因为其语义(键值映射)与传统集合不同。
  • 其主要操作包括:添加 (put)、获取 (get)、删除 (remove)、遍历等。

接口 vs 实现类的关系图解

          Iterable|Collection               Map/    |    \               |List   Set   Queue         SortedMap|       |      |             |ArrayList HashSet LinkedList   TreeMap

核心接口与通用方法Java 集合框架中的所有类都遵循统一的接口规范,使得学习与使用变得更为一致和高效。

• add()、remove()、contains() 等基础方法

  • add(E e):向集合中添加元素。
  • remove(Object o):移除指定对象(第一次出现)。
  • contains(Object o):判断集合中是否包含该对象。
  • isEmpty():判断集合是否为空。
  • size():获取集合大小。

注意: 不同集合对 add() 的行为不同,如 Set 添加重复元素会失败。

equals()、hashCode() 在集合中的作用

  • SetMap 等集合依赖 equals()hashCode() 判断对象是否“相等”。
  • 若自定义对象作为集合元素或 Map 的键,必须重写这两个方法以避免逻辑错误。
@Override
public boolean equals(Object obj) {// 比较字段值
}
@Override
public int hashCode() {// 返回基于字段的 hash
}

泛型支持与类型安全

  • Java 5 引入泛型后,集合类支持类型参数:

    List<String> list = new ArrayList<>();
    
  • 避免类型转换错误(ClassCastException):

    String s = list.get(0); // 不再需要强制转换
    

fail-fast 与 fail-safe 行为差异

  • fail-fast(快速失败)
    • 集合在结构被修改(非通过迭代器)时,遍历会抛出 ConcurrentModificationException
    • 如:ArrayList, HashMap, HashSet
  • fail-safe(安全失败)
    • 基于副本的遍历,不抛异常,但不保证实时性。
    • 如:CopyOnWriteArrayList, ConcurrentHashMap
集合框架中的遍历方式

集合遍历是集合使用中最常见的操作之一,Java 提供了多种方式。

Iterator / enhanced for / forEach

  • Iterator 是最通用的遍历方式:

    Iterator<String> it = list.iterator();
    while (it.hasNext()) {String s = it.next();
    }
    
  • for-each 语法糖:

    for (String s : list) {// 自动调用 Iterator
    }
    
  • Collection.forEach(Consumer)(Java 8+):

    list.forEach(s -> System.out.println(s));
    

ListIterator 的前后遍历

  • List 接口支持 ListIterator,可双向遍历:

    ListIterator<String> it = list.listIterator();
    while (it.hasNext()) {String s = it.next();
    }
    while (it.hasPrevious()) {String s = it.previous();
    }
    
  • 可通过 add()remove() 等在遍历中安全修改集合。

lambda 与 stream 的遍历新姿势(Java 8+)

  • Java 8 引入 Stream API,使集合操作更函数式和声明式:

    list.stream().filter(s -> s.startsWith("A")).map(String::toUpperCase).forEach(System.out::println);
    
  • 并行流(parallelStream)可加速处理大数据量(注意线程安全)。

List 接口与实现类详解

List 是最常用的集合类型,特点是:有序可重复

ArrayList(顺序数组)

  • 底层结构:动态数组(Object[])
  • 扩容机制:初始容量为10,每次扩容为原容量的 1.5 倍
  • 优点
    • 随机访问性能高(O(1)
    • 内存连续,缓存友好
  • 缺点
    • 插入/删除慢(涉及数组复制)
  • 使用场景
    • 读多写少的顺序数据
  • 进阶
    • ensureCapacity() 可提前扩容避免重复复制
    • 可配合 Collections.synchronizedList 实现线程安全

LinkedList(链表实现)

  • 底层结构:双向链表
  • 优点
    • 插入/删除效率高(不需位移)
  • 缺点
    • 随机访问慢(O(n)
  • 支持队列操作:可作为 Deque 使用(支持栈/队列)
  • 使用场景
    • 数据频繁插入/删除

Vector(线程安全的 ArrayList)

  • ArrayList 类似,但所有方法都被 synchronized 修饰
  • 几乎被淘汰,推荐使用 Collections.synchronizedList(new ArrayList<>())
Set 接口与实现类详解

Set 体现的是数学集合语义:无序、不可重复。

HashSet(最常用)

  • 底层结构:基于 HashMap 实现,元素作为键,值为常量 PRESENT
  • 添加逻辑:依赖 hashCode()equals() 保证唯一性
  • JDK 8 优化:链表过长转为红黑树(链表长度 > 8 且桶大小 > 64)
  • 特点
    • 查询/添加/删除时间复杂度为 O(1)(理想情况)
  • 常见误区
    • 忘记重写 equals/hashCode 导致“重复元素”失效

LinkedHashSet

  • 底层结构:继承 HashSet + 双向链表
  • 保持元素插入顺序
  • 适合需要“去重 + 有序”场景

** TreeSet**

  • 底层结构:红黑树(TreeMap 支撑)
  • 自动排序:元素需实现 Comparable 或使用自定义 Comparator
  • 时间复杂度:添加/删除/查询为 O(log n)
  • 适合对数据进行自动排序、范围查询
Map 接口与实现类详解

Map 是 Java 中用于处理键值对映射的核心接口。

HashMap(最常用)

  • 底层结构
    • JDK 7:数组 + 链表
    • JDK 8:数组 + 链表 + 红黑树
  • 容量/负载因子
    • 初始容量默认 16,负载因子 0.75
  • 扩容机制
    • 超过阈值(容量 × 负载因子)后,容量翻倍
  • 冲突处理
    • 同 hash 值元素存储在链表或红黑树中(树化条件如上)
  • 非线程安全

进阶

  • 键的要求:必须实现 hashCode()equals()
  • JDK 8 之后性能显著提升(链表转红黑树)
  • Hash 冲突多时性能可能降为 O(n),树化避免此问题

LinkedHashMap

  • 底层结构:继承 HashMap,同时维护插入顺序的双向链表
  • 保持元素插入顺序(也可以按访问顺序)
  • LRU 实现方式
    • 构造时指定 accessOrder=true,并重写 removeEldestEntry()

TreeMap

  • 基于红黑树,键需排序(ComparableComparator

  • 适合范围查找、自动排序,如:

    map.subMap(10, 20); // 获取 key 在 10~20 之间的子映射
    

Hashtable(线程安全但已淘汰)

  • 所有方法都加了 synchronized,效率低
  • 推荐使用 ConcurrentHashMap
Queue 接口与实现类详解

Queue 表现先进先出(FIFO)行为,广泛用于任务调度、缓存等。

LinkedList 实现 Queue

  • 实现了 QueueDeque
  • 支持头尾插入/删除(栈/队列双模式)

PriorityQueue

  • 基于堆(最小堆),元素自动排序
  • 默认使用 Comparable,也可传入 Comparator
  • 常用于任务调度、优先队列

ArrayDeque

  • 双端队列,高效替代 StackLinkedList
  • 无容量限制,头尾操作 O(1)
  • LinkedList 更轻量级,无链表指针开销
对比汇总
接口实现类底层结构有序性线程安全特点
ListArrayList数组有序×快速随机访问
ListLinkedList双向链表有序×快速增删
SetHashSetHash 表无序×快速查重
SetTreeSet红黑树排序×自动排序
MapHashMapHash 表无序×快速 key-value 映射
MapTreeMap红黑树排序×按 key 排序
QueuePriorityQueue排序×优先处理
QueueArrayDeque循环数组有序×双端操作

2.底层实现及原理

因为Java集合并没有太多理解困难的抽象知识,更多是在使用中的选择和应用,对于实现原理和源码分析已经有很多成熟的文章和视频,所以我不再重复赘述。

Java集合常见面试题总结(上) | JavaGuide

CarpenterLee/JCFInternals: 深入理解Java集合框架

google/guava: Google core libraries for Java

3.线程安全与并发场景

1.线程不安全集合的问题

在多线程并发环境中,如果使用 Java 中默认的集合类(如 ArrayListHashMapLinkedList 等),很容易出现数据不一致程序异常甚至死循环等问题。这是因为这些集合类本身没有线程安全保障,多个线程同时读写时会引发竞态条件。

示例一:ArrayList 的并发问题

java复制编辑List<Integer> list = new ArrayList<>();Runnable task = () -> {for (int i = 0; i < 1000; i++) {list.add(i);}
};Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();System.out.println("Size: " + list.size());

预期结果:2000
实际可能结果:小于 2000,甚至抛出 ConcurrentModificationExceptionArrayIndexOutOfBoundsException

原因:多个线程同时调用 add(),在扩容或修改内部数组时发生冲突,导致数据错乱或覆盖。


示例二:HashMap 的并发死循环(JDK 1.7)

java复制编辑Map<Integer, Integer> map = new HashMap<>();Runnable task = () -> {for (int i = 0; i < 10000; i++) {map.put(i, i);}
};Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();

在 JDK 1.7 中,如果在多线程环境下频繁 put,可能会触发 rehash 时链表形成循环,导致 CPU 100% 卡死在遍历中(虽然 JDK 1.8 改进为红黑树后几率降低,但仍不安全)。

因此,在并发环境中使用这些集合类需要格外小心,不能直接使用,应使用线程安全版本或并发集合类替代。

2.线程安全的集合类

在多线程环境中,普通集合类(如 ArrayListHashMap)由于缺乏内部同步机制,无法保障并发操作下的数据一致性,容易引发线程安全问题。为了解决这一问题,Java 提供了多种线程安全的集合实现方案,分别适用于不同的使用场景。

1. 使用 Collections.synchronizedXXX 包装

Java 的 Collections 工具类提供了若干静态方法,可以将原本非线程安全的集合包装成线程安全的版本,例如:

java复制编辑List<String> syncList = Collections.synchronizedList(new ArrayList<>());
Map<String, String> syncMap = Collections.synchronizedMap(new HashMap<>());

这些包装方法的底层实现通过 synchronized 关键字为集合的每个操作加锁,从而保证线程安全。但需要注意:

  • 对集合的迭代仍需手动同步;
  • 在高并发环境下性能较差,因为锁粒度粗,每次访问都串行化处理。

这种方式简单直接,适合轻量级的并发需求场景,但并不推荐用于复杂或高频操作的系统中。

2. Concurrent 包下的高性能集合类

为了提供更优的并发性能,Java 5 引入了 java.util.concurrent 包,内置了多种基于分段锁或无锁机制的线程安全集合,它们在企业开发中广泛应用:

  • ConcurrentHashMap
    HashMap 的线程安全替代品,JDK 7 使用分段锁(Segment),JDK 8 开始改为使用 CAS + synchronized 实现,具有更高的并发性能。适合高读写并发场景。
  • CopyOnWriteArrayList
    基于写时复制(Copy-On-Write)机制,每次修改(如添加、删除)时都会复制整个底层数组,写操作代价高,但读操作无需加锁,非常适合读多写少的场景。
  • ConcurrentLinkedQueue
    是一个基于非阻塞算法的无界线程安全队列,采用 CAS 操作保证并发安全,适用于高吞吐量的消息队列模型中。
  • ConcurrentSkipListMap / ConcurrentSkipListSet
    基于跳表结构的线程安全有序集合,支持并发访问的同时保持元素有序,适用于需要排序或范围查询的场景。

这些集合的设计核心是降低锁粒度,避免全局锁,通过细粒度锁或乐观并发控制机制(如 CAS)来提升并发能力。

3. 并发集合的使用建议

  • 读多写少:优先考虑 CopyOnWriteArrayList
  • 高并发读写:使用 ConcurrentHashMap
  • 高性能队列:采用 ConcurrentLinkedQueue
  • 有序并发集合:考虑 ConcurrentSkipListMapConcurrentSkipListSet
3.Concurrent 包下的并发集合原理

为了提升集合在并发场景下的性能和可扩展性,Java 的 java.util.concurrent 包引入了多种并发集合,其设计核心在于降低锁粒度分离读写操作使用无锁算法,相比传统的同步集合性能更优。

以下是几种常见并发集合的底层原理简介:


1. ConcurrentHashMap

ConcurrentHashMap 是最常用的线程安全 Map 实现:

  • JDK 7:使用分段锁(Segment)
    • 将整个 HashMap 拆分成多个 Segment,每个 Segment 维护一个独立的哈希表。
    • 不同 Segment 之间互不干扰,提升并发访问性能。
    • 缺点是默认只支持 16 个 Segment,扩展性有限。
  • JDK 8:使用 CAS + synchronized
    • 取消了 Segment,使用数组 + 链表/红黑树的结构。
    • 写操作使用 synchronized 锁定单个桶,读操作通过 volatile 保证可见性,避免加锁。
    • 扩容时也采用分布式迁移,减少阻塞。

优点: 高性能、高可扩展性、适合读写并发场景。
注意: 并不支持 null 键或 null 值。


2. CopyOnWriteArrayList

适合读多写少的场景,其核心思想是写时复制

  • 每次修改操作(如 add()remove())都会复制一份新数组。
  • 读操作无需加锁,因为读的是旧数组副本,天然线程安全。
  • 写操作通过 ReentrantLock 保证串行化。

优点: 读操作无锁,读性能极高。
缺点: 写操作开销大,不适合频繁修改的场景。


3. ConcurrentLinkedQueue

是一个基于 非阻塞链表 的并发队列,实现了 无锁队列算法

  • 使用 CAS(Compare-And-Swap) 操作保证原子性,避免传统加锁开销。
  • 基于 Michael-Scott 非阻塞队列算法实现,入队和出队操作互不干扰。

优点: 高吞吐,适合生产者-消费者模型、任务调度等场景。
缺点: 在极高并发下可能存在 ABA 问题(JDK 已优化)。


4. ConcurrentSkipListMap / Set

使用跳表(Skip List)实现的有序并发集合:

  • 支持并发访问的同时保持元素的自然顺序或指定顺序
  • 跳表是一种多层级链表,平均查找、插入、删除复杂度为 O(log n)。
  • 内部采用 CAS 保证节点插入删除的原子性,同时使用 ReentrantLock 做局部加锁。

优点: 支持范围查询、有序遍历,是线程安全的 TreeMap 替代品。

Java 并发集合的底层实现都体现了对传统加锁方式的优化,通过合理的算法设计和内存模型控制,使得集合在多线程环境下既安全又高效。掌握它们的原理,对于设计线程安全的数据结构和排查并发 Bug 都非常重要。

4.使用建议与场景

在多线程环境下,选择正确的集合类至关重要。使用不当会导致数据竞争、性能瓶颈甚至严重的线程安全问题。以下是一些实用建议和典型场景,帮助开发者做出合理选择。


1. 明确读写比重

  • 读多写少:
    • 使用 CopyOnWriteArrayListCopyOnWriteArraySet
    • 避免加锁带来的读阻塞,适合缓存、配置列表等。
  • 读写并发:
    • 使用 ConcurrentHashMapConcurrentSkipListMap 等。
    • 它们对读写进行了优化,适合共享数据频繁变动的业务逻辑。
  • 写密集场景:
    • 避免使用写时复制类(如 CopyOnWrite 系列)。
    • 可以使用 ConcurrentLinkedQueueBlockingQueue 等支持高并发写入的结构。

2. 是否需要顺序保证

  • 若需要 有序遍历范围查询,推荐使用:
    • ConcurrentSkipListMap(有序 Map)
    • ConcurrentSkipListSet(有序 Set)
  • 若对顺序不敏感:
    • ConcurrentHashMap 更加高效,占用更少内存。

3. 是否涉及阻塞行为

  • 若集合用于生产者-消费者模型,考虑使用:
    • LinkedBlockingQueue
    • ArrayBlockingQueue
    • DelayQueuePriorityBlockingQueue
  • 这些阻塞队列内置 take()put() 方法,可以自然地控制线程等待和唤醒,避免手动加锁。

4. 不要误用线程不安全集合

在并发场景中避免使用:

  • ArrayList
  • HashMap
  • LinkedList
  • HashSet

这些集合在并发读写中没有任何线程安全机制,容易导致数据不一致、死循环(如旧版 HashMap 扩容)或程序崩溃。


5. 警惕性能陷阱

  • Collections.synchronizedXXX() 虽然提供了线程安全包装,但性能差,锁粒度粗,容易成为瓶颈。
  • Hashtable 是遗留类,已被 ConcurrentHashMap 取代,避免使用。
场景描述推荐集合
读多写少CopyOnWriteArrayList
高并发读写ConcurrentHashMap
有序并发访问ConcurrentSkipListMap / Set
阻塞队列 / 消费者模型LinkedBlockingQueue, ArrayBlockingQueue
简单线程安全包装(低性能)Collections.synchronizedList 等

4.设计模式与源码设计思想

这部分还会更新扩充

Java集合框架是基于许多经典设计模式和原则来实现的,它通过不同的集合类和接口,为开发者提供了高效、灵活的数据存储解决方案。在深入分析Java集合框架时,我们可以发现其中有大量设计模式的应用以及源码设计思想的体现。


一、集合框架中的常见设计模式
  1. 工厂模式(Factory Pattern)

    • 概述:工厂模式用于创建对象的接口,但不暴露对象创建的具体实现。
    • 在集合框架中的应用Collections 类和 ListSetMap 等接口的实现都遵循了工厂模式。例如,Collections 类提供了静态工厂方法(如 singletonList()emptyList() 等)来创建不可变集合对象。
    • 源码示例
    public static <T> List<T> singletonList(T o) {return new SingletonList<>(o);
    }
    

    这里的 singletonList 就是工厂方法,通过封装集合的创建逻辑,提供特定类型的集合实例。

  2. 单例模式(Singleton Pattern)

    • 概述:确保某个类只有一个实例,并提供全局访问点。
    • 在集合框架中的应用EnumSet 是一个典型的单例模式应用。因为 EnumSet 只能处理枚举类型,所以它通过内部静态方法确保了集合的唯一性和单例模式。
    • 源码示例
    public static <E extends Enum<E>> EnumSet<E> of(E first, E... rest) {return EnumSet.allOf(first.getDeclaringClass()).clone();
    }
    

    EnumSet 确保了枚举集合的单一性,不会创建多余的实例。

  3. 策略模式(Strategy Pattern)

    • 概述:通过封装算法,允许在运行时切换算法。
    • 在集合框架中的应用:在 TreeSetTreeMap 中,元素的比较方式由 Comparator 接口策略化。Comparator 可以在运行时被替换,从而改变元素的排序方式。
    • 源码示例
    public TreeSet(Comparator<? super E> comparator) {this.comparator = comparator;
    }
    

    TreeSetTreeMap 允许用户通过传入不同的 Comparator 来决定集合的排序策略。

  4. 装饰者模式(Decorator Pattern)

    • 概述:允许动态地给对象添加额外的功能。
    • 在集合框架中的应用ListSetMap 等集合接口有多个装饰类,如 Collections.unmodifiableList(),它封装了一个集合对象,为集合提供只读功能。
    • 源码示例
    public static <T> List<T> unmodifiableList(List<? extends T> list) {return new UnmodifiableList<>(list);
    }
    

    UnmodifiableList 是对原集合的装饰,使得外部无法修改该集合。

  5. 代理模式(Proxy Pattern)

    • 概述:通过代理对象控制对原对象的访问。
    • 在集合框架中的应用CopyOnWriteArrayList 类使用了代理模式,它的行为被“复制”到一个新的副本中,以避免直接修改原集合时的并发问题。
    • 源码示例
    public boolean add(E e) {final ReentrantLock lock = this.lock;lock.lock();try {Object[] elements = getArray();int length = elements.length;// Create a new array for every modification.Object[] newElements = Arrays.copyOf(elements, length + 1);newElements[length] = e;setArray(newElements);return true;} finally {lock.unlock();}
    }
    

    这个设计通过复制原数组来避免在并发修改集合时出现问题,从而使得集合在多线程环境下保持线程安全。


二、Java集合中的源码设计思想
  1. 高内聚低耦合
    • 概述:集合类的设计遵循了高内聚低耦合的原则,意味着每个集合类仅负责实现集合的特定功能,而与其他类或组件的耦合度尽量保持在最低。
    • 体现:通过接口和抽象类,Java集合框架使得不同类型的集合可以灵活替换,并且实现了面向接口编程。例如,ListSetMap 都是集合接口,具体的实现类如 ArrayListHashSetHashMap 等都有各自独立的实现。
  2. 接口与抽象类的运用
    • 概述:集合框架的设计采用了大量的接口和抽象类来解耦集合的实现与使用。
    • 体现:通过 CollectionListSetMap 等接口,集合框架能够灵活地支持不同类型的集合。在实现细节方面,诸如 AbstractListAbstractSet 提供了基础的功能实现,让具体的实现类可以继承并加以拓展。
  3. 通用性与灵活性
    • 概述:Java集合框架强调通用性,几乎所有集合类都遵循了一种标准的接口规范,确保了不同集合类之间的通用操作。
    • 体现:通过一组统一的接口和方法,所有集合类(如 add()remove()size())都遵循相同的约定,这使得开发者可以更轻松地切换不同类型的集合(如 ArrayListLinkedList)而不改变代码逻辑。
  4. 延迟初始化与懒加载
    • 概述:在某些集合类中,采用延迟初始化和懒加载的策略来优化性能。
    • 体现:例如,在 HashMap 中,内部数组并不是在初始化时就创建,而是在需要时才进行扩容,确保内存的有效利用。Lazysynchronized 在某些线程安全的集合类(如 CopyOnWriteArrayList)中发挥作用,减少不必要的计算和开销。

Java集合框架不仅仅提供了多种高效的数据结构和操作方法,它背后也体现了许多经典的设计模式和良好的源码设计思想。这些模式和思想的应用使得集合框架在不同的场景下都能提供灵活、高效、可扩展的解决方案。深入理解这些设计模式和源码实现,能够帮助开发者在使用集合框架时更加得心应手,同时也能够在设计自己的系统时,借鉴这些模式来提升代码的可维护性、可扩展性和性能。

相关文章:

  • Qt C++图书管理系统
  • 轴承与螺母表面缺陷数据集
  • PostgreSQL跨数据库表字段值复制实战经验分
  • DAY8字典的简单介绍
  • 30.第二阶段x64游戏实战-认识网络数据包发送流程
  • 深入了解linux系统—— 文件系统
  • 即插即用性能提升技巧:YOLOv8集成OREPA卷积的5个关键步骤(附精度/速度对比)
  • Java大厂面试:JVM调优、高并发订单处理与大数据服务场景解析
  • 【补题】The 2021 ICPC Asia Nanjing Regional Contest Problem J. Xingqiu’s Joke
  • 使用 Navicat 17 for PostgreSQL 时,请问哪个版本支持 PostgreSQL 的 20150623 版本?还是每个版本都支持?
  • 【Redis】三、在springboot中应用redis
  • 第十周作业
  • 5月21日学习笔记
  • C# 使用 Source Generation 提升 System.Text.Json 性能
  • 错误: gdalbuildvrt 命令未找到————的问题
  • LeetCode 257. 二叉树所有路径的迭代解法:栈的妙用与类型设计深度解析
  • Enhancing Relation Extractionvia Supervised Rationale Verifcation and Feedback
  • Starrocks的CBO基石--统计信息的来源 StatisticAutoCollector
  • Vue 3.0学习目录
  • 制作一款打飞机游戏54:子弹编辑UI
  • 网站切片 做程序/免费站推广网站2022
  • wordpress+资源站模板/百度助手app下载
  • 深圳公司视频制作/大连seo建站
  • 个人网站做交易类的赚钱吗/苏州网站建设费用
  • 市体育局网站 两学一做/推广优化
  • 网站建设及维护机/舆情监控