发布时间:2026/8/26 21:46:02
Java Set集合详解:HashSet、TreeSet、LinkedHashSet核心原理与实战选型 1. 从“集合”到“Set”为什么Java需要它如果你写过一段时间的Java尤其是在处理数据去重、成员关系判断这类场景时肯定会遇到一个绕不开的接口java.util.Set。面试八股文里它和List、Map的对比是必考题实际项目中从简单的用户ID去重到复杂的业务规则过滤Set的身影无处不在。但你真的了解它吗为什么有了List还需要SetHashSet、TreeSet、LinkedHashSet这几个兄弟到底有什么区别又该怎么选今天我们就抛开那些枯燥的API文档从一个一线开发者的视角把JavaSet里里外外、从原理到实战彻底讲透。简单来说Set是一个不包含重复元素的集合。这是它最核心、最本质的特征也是它和List最根本的区别。List关心顺序和索引允许重复Set只关心“存在与否”拒绝重复。这个看似简单的特性背后是数学中“集合”概念的编程实现为解决数据唯一性问题提供了优雅且高效的方案。无论是处理从数据库查出来的一堆可能有重复的记录还是判断一个用户是否在某个特权名单里Set都是你的首选工具。接下来我们会深入它的三种最常见实现HashSet、TreeSet和LinkedHashSet。它们都实现了“不重复”但实现方式、性能特性和适用场景天差地别。选择不当轻则性能低下重则出现诡异的业务逻辑错误。我们不仅会看它们怎么用更要深挖它们为什么这么设计以及在实际编码中那些教科书上不会告诉你的“坑”和技巧。2. HashSet速度之王与哈希的魔法当你需要一个快速查找、快速去重的集合并且不关心元素的顺序时HashSet几乎总是第一选择。它的名字就揭示了其核心哈希表Hash Table。2.1 底层原理HashMap的“马甲”很多新手会惊讶地发现HashSet的底层实现其实是一个HashMap。打开JDK源码你会看到类似这样的结构简化版public class HashSetE { private transient HashMapE,Object map; // 虚拟值用于填充HashMap的value private static final Object PRESENT new Object(); public boolean add(E e) { return map.put(e, PRESENT) null; } }看到了吗HashSet把自己要存储的元素E e作为HashMap的键Key而HashMap的值Value则用一个固定的、毫无意义的PRESENT对象来占位。HashMap的键本身就是不允许重复的这个特性被HashSet完美地“借用”了过来。当你调用set.add(“apple”)时底层实际执行的是map.put(“apple”, PRESENT)。如果put方法返回null说明这个键之前不存在添加成功如果返回了旧值也就是PRESENT说明键已存在添加失败。这种设计的精妙之处在于极高的代码复用。HashMap经过千锤百炼其哈希算法、冲突解决、扩容机制都非常成熟高效。HashSet站在巨人的肩膀上无需自己再造轮子就获得了同样卓越的性能。2.2 性能特征与使用要点HashSet的添加add、删除remove、包含判断contains操作在理想情况下时间复杂度都是O(1)即常数时间。这是它被称为“速度之王”的原因。但是这个“理想情况”有前提良好的哈希函数你存入HashSet的对象必须正确重写了hashCode()和equals()方法。这是老生常谈但也是踩坑最多的地方。hashCode()用于确定对象在哈希表中的桶bucket位置。equals()用于在哈希冲突时即多个对象的hashCode值相同进一步比较它们是否真的相等。黄金法则如果两个对象根据equals()比较是相等的那么它们的hashCode()必须相等。反之hashCode()相等的两个对象equals()不一定相等这就是哈希冲突。实战踩坑如果你把一个自定义的Student对象放入HashSet但只重写了equals()比如按学号判断相等没重写hashCode()。那么即使两个学号相同的Student对象也可能被HashSet认为是不同的从而都添加成功违反了Set的唯一性规则。IDE如IntelliJ IDEA, Eclipse都可以一键生成这两个方法务必使用。合理的初始容量与负载因子HashSet有构造函数HashSet(int initialCapacity, float loadFactor)。initialCapacity哈希表桶数组的初始大小。默认是16。如果你预先知道要存入大约1000个元素设置一个更大的初始容量如1024可以避免多次扩容提升性能。loadFactor负载因子默认0.75。当哈希表中的元素数量超过容量 * loadFactor时哈希表会进行扩容通常是翻倍并重新哈希所有元素。负载因子越小哈希冲突的概率越低查询越快但空间浪费越多。0.75是时间和空间的一个经验平衡点通常无需修改除非有极特殊的性能调优需求。无序性这是HashSet的一个重要特性。它不保证元素的迭代顺序也不保证顺序随时间保持不变。今天的迭代顺序是[A, B, C]明天扩容后可能就是[C, A, B]。任何依赖HashSet顺序的业务逻辑都是错误的。2.3 典型应用场景快速去重从List中去除重复项。ListString uniqueList new ArrayList(new HashSet(originalList))这是一行经典代码。成员关系快速判断比如判断一个用户ID是否在黑名单中。if (blacklistSet.contains(userId)) { ... }contains操作是O(1)的远比用List的containsO(n)高效。集合运算求两个集合的交集、并集、差集。Set接口提供了addAll(并集)、retainAll(交集)、removeAll(差集)等方法底层基于哈希效率很高。注意HashSet不是线程安全的。如果多个线程同时修改一个HashSet可能会导致数据损坏。在多线程环境下可以使用Collections.synchronizedSet(new HashSet())包装或者直接使用ConcurrentHashMap的KeySet视图通过ConcurrentHashMap.newKeySet()创建后者性能更好。3. TreeSet有序的代价与比较的艺术当你需要一个元素自动排序的Set时TreeSet就该登场了。它实现了SortedSet和NavigableSet接口意味着元素总是处于排序状态并且你可以方便地进行范围查询如“找出小于某个值的所有元素”。3.1 底层原理红黑树的力量TreeSet的底层是一颗红黑树Red-Black Tree。红黑树是一种自平衡的二叉查找树。它通过在插入和删除时进行特定的旋转和变色操作来保证树大致平衡从而确保最坏情况下的查找、插入、删除时间复杂度也能保持在O(log n)。和HashSet类似TreeSet也穿了个“马甲”它的内部使用了一个TreeMap。public class TreeSetE { private transient NavigableMapE,Object m; // 实际上就是TreeMap private static final Object PRESENT new Object(); // ... 方法实现类似HashSet但操作的是TreeMap }元素作为TreeMap的键存储排序的规则就由这个TreeMap来维护。3.2 排序规则Comparable与ComparatorTreeSet如何知道怎么给元素排序有两种方式自然排序让元素类实现Comparable接口并重写compareTo方法。例如String、Integer等包装类都实现了这个接口。创建TreeSet时无需额外参数。SetString sortedSet new TreeSet(); sortedSet.add(orange); sortedSet.add(apple); sortedSet.add(banana); System.out.println(sortedSet); // 输出[apple, banana, orange] (按字典序)定制排序在创建TreeSet时传入一个Comparator比较器对象。这种方式更灵活尤其适用于以下情况元素类没有实现Comparable接口。你想覆盖元素类原有的自然排序规则。你想根据对象的某个特定属性进行排序。// 按字符串长度排序 SetString lengthSortedSet new TreeSet(Comparator.comparingInt(String::length)); lengthSortedSet.add(fig); lengthSortedSet.add(apple); lengthSortedSet.add(banana); System.out.println(lengthSortedSet); // 输出[fig, apple, banana] (长度3,5,6) // 注意长度相同时“apple”和“fig”因为内容不同Comparator认为它们不相等所以都能加入。 // 但如果Comparator只比较长度那么长度相同的元素会被视为“相等”从而无法加入Set。这里有一个巨大的坑TreeSet判断元素是否“重复”的依据不是equals()和hashCode()而是比较器Comparator或Comparable的compare/compareTo方法返回0如果比较器认为两个元素相等返回0TreeSet就认为它们是同一个元素后一个无法加入。这可能导致令人困惑的行为。实战案例假设我们有一个Person类有name和age属性。我们想按年龄排序放入TreeSet。class Person { String name; int age; // 构造器、getter/setter省略 } SetPerson personSet new TreeSet(Comparator.comparingInt(p - p.age)); personSet.add(new Person(Alice, 25)); personSet.add(new Person(Bob, 25)); // 这个能加进去吗答案是不能。因为比较器只比较ageAlice和Bob的age都是25compare方法返回0TreeSet认为Bob和Alice是“相同”的元素拒绝加入。即使他们的name不同equals方法返回false也没用。在TreeSet的世界里比较器是唯一的“相等”裁判官。3.3 性能与场景权衡TreeSet的增、删、查操作都是O(log n)。这比HashSet的O(1)慢但比在List中线性查找快得多。它的优势在于“有序”和“范围查询”。需要元素始终有序的场景例如维护一个实时更新的排行榜每次添加新成绩后都能自动排序。需要范围查找的场景NavigableSet接口提供了subSet,headSet,tailSet,ceiling,floor,higher,lower等方法可以高效地找到某个边界附近的元素。TreeSetInteger scores new TreeSet(); scores.addAll(Arrays.asList(85, 92, 78, 95, 88)); // 找出所有大于等于90分的成绩 SetInteger highScores scores.tailSet(90); // [92, 95] // 找出刚好低于90分的成绩 Integer justBelow90 scores.lower(90); // 88注意和HashSet一样TreeSet也不是线程安全的。它的迭代器是“快速失败”的如果在迭代过程中集合被修改除了通过迭代器自身的remove方法会抛出ConcurrentModificationException。4. LinkedHashSet兼顾速度与插入顺序LinkedHashSet是HashSet的一个子类。它继承了HashSet的快速查找特性基于哈希表同时通过维护一个贯穿所有元素的双向链表保证了元素的迭代顺序就是它们被插入的顺序。这是一种“预知性”的顺序不像TreeSet那样是排序顺序。4.1 原理哈希表链表你可以把LinkedHashSet理解为在HashSet的每个“条目”上额外增加了前驱和后继指针将这些条目串成一条链表。当元素被插入时它既被放入哈希表也被追加到链表的末尾。这个设计是空间换时间的典型它需要额外的内存来存储链表指针。因为底层是HashSet所以LinkedHashSet的查找、添加、删除性能依然是接近O(1)的虽然常数时间会比纯HashSet略高一点因为要维护链表。但由于链表的存在它的迭代速度比HashSet要快因为迭代链表是顺序访问而迭代HashSet需要遍历整个桶数组其中很多桶是空的。4.2 应用场景需要预测的迭代顺序LinkedHashSet的用武之地非常明确当你既需要Set的去重特性又需要元素按照插入的先后顺序进行遍历时。实现LRU缓存虽然LinkedHashSet本身不直接支持LRU但它的一个构造函数LinkedHashSet(int initialCapacity, float loadFactor, boolean accessOrder)当accessOrder为true时它会按访问顺序排序最近访问的会被移到链表末尾。LinkedHashMap正是利用这个特性实现了LRU缓存而LinkedHashSet可以基于LinkedHashMap来实现类似的缓存键集合。记录用户操作序列并去重例如记录用户最近浏览过的10个不同的商品ID要求既不能重复又要按浏览时间排序。用LinkedHashSet再合适不过。当超过10个时可以移除链表头部的元素最早浏览的。需要稳定顺序的配置项集合有些配置项需要去重但读取时希望保持它们在配置文件中的出现顺序LinkedHashSet可以很好地保持这种“原貌”。与List去重后的比较你可能会想我先用ArrayList按顺序存再去重不也一样但List的contains操作是O(n)的去重过程效率很低。LinkedHashSet在插入时就能保证唯一性和顺序效率更高。5. 三大Set实现的核心对比与选型指南光知道每个是什么还不够在实际项目中如何选择才是体现功力的地方。下面这个表格从多个维度对它们进行了对比特性HashSetTreeSetLinkedHashSet底层数据结构哈希表 (基于HashMap)红黑树 (基于TreeMap)哈希表 双向链表元素顺序不保证任何顺序自动排序(自然或定制)按插入顺序迭代时间复杂度添加、删除、查找:O(1)(平均)添加、删除、查找:O(log n)添加、删除、查找:O(1)(平均常数略高于HashSet)判断重复的依据hashCode()与equals()Comparator/Comparable的compare方法 (返回0)hashCode()与equals()是否允许null元素允许一个null不允许(除非Comparator能处理)允许一个null线程安全否否否内存开销较低中等 (树节点结构)较高 (哈希表链表指针)典型应用场景通用去重、快速成员检查、集合运算需要自动排序、范围查询的场景需要保持插入顺序的去重场景、简单LRU缓存选型决策流问是否需要元素有序否- 首选HashSet。性能最好。是- 进入第2步。问需要什么样的顺序需要自动排序按大小、字典序等- 选择TreeSet。需要保持插入时的先后顺序- 选择LinkedHashSet。一些进阶考量数据量数据量非常小比如几十个时三者性能差异不大可以优先考虑功能需求。数据量大时HashSet和LinkedHashSet的O(1)优势会非常明显。哈希成本如果元素的hashCode()方法计算非常耗时比如要对一个大对象进行深度哈希HashSet的性能优势可能会被削弱。此时如果数据量巨大且需要排序TreeSet的O(log n)可能相对更稳定。内存敏感在内存极其受限的环境如某些嵌入式设备HashSet可能是更经济的选择。6. 实战避坑与性能调优经验谈理论懂了但在实际编码中还是有很多细节需要注意。这里分享几个我踩过或见别人踩过的坑。6.1 可变对象作为Set元素的灾难这是一个经典陷阱。假设你把一个可变对象比如一个HashMap或自定义对象放入了HashSet之后又修改了这个对象的内容。SetPoint pointSet new HashSet(); Point p new Point(1, 2); pointSet.add(p); System.out.println(pointSet.contains(p)); // true // 修改p的内部状态 p.x 10; // 再次检查 System.out.println(pointSet.contains(p)); // **可能返回false** // 尝试删除 System.out.println(pointSet.remove(p)); // **可能返回false删除失败**发生了什么当你修改p的坐标后它的hashCode()值很可能发生了变化如果hashCode方法用到了x和y字段。现在当HashSet调用contains(p)时它根据p新的哈希值去查找对应的桶。但当初p是根据旧的哈希值被放在另一个桶里的。在新的桶里找不到p所以返回false。结果就是这个元素“丢失”在了Set中——你既找不到它也删不掉它因为remove也是基于哈希查找但它实际上还占据着内存。这会导致内存泄漏和逻辑错误。解决方案最佳实践尽量让作为Set或Map的键的对象是不可变的。例如使用String、Integer或record类型Java 14。如果对象必须是可变的那么在修改其影响hashCode和equals的字段后必须将其从Set中移除修改后再重新添加。6.2 初始化容量设置一个容易被忽略的性能优化点如果你能预估Set最终会包含多少元素在构造时指定一个合适的初始容量可以避免或减少扩容操作这对HashSet和LinkedHashSet尤其有效。反面教材// 默认初始容量16负载因子0.75 SetString set new HashSet(); for (int i 0; i 1000000; i) { set.add(element- i); } // 这个Set在添加过程中会经历多次扩容和重哈希消耗大量时间。优化方案// 预估最终有100万个元素负载因子0.75 // 所需容量 元素数量 / 负载因子 1,000,000 / 0.75 ≈ 1,333,334 // 取一个2的幂哈希表容量通常是2的幂比如 2^21 2,097,152 int estimatedSize 1000000; int initialCapacity (int) (estimatedSize / 0.75f) 1; // 或者更简单地直接给一个足够大的值 SetString optimizedSet new HashSet(2_000_000);对于TreeSet虽然没有“容量”概念但如果你使用带有Comparator的构造器并且比较操作非常昂贵也应考虑其性能影响。6.3 遍历与修改的并发问题即使在单线程环境下遍历时修改集合也可能出问题。SetString set new HashSet(Arrays.asList(A, B, C)); for (String s : set) { if (s.equals(B)) { set.remove(s); // 这里会抛出 ConcurrentModificationException! } }HashSet、TreeSet、LinkedHashSet的迭代器都是“快速失败”的。它们内部有一个modCount变量记录修改次数。迭代器在创建时会记录当前的modCount在每次迭代如调用next()时会检查当前的modCount是否与记录的一致如果不一致就认为集合在迭代过程中被其他方式修改了立即抛出ConcurrentModificationException。安全删除元素的方法使用迭代器的remove方法IteratorString iterator set.iterator(); while (iterator.hasNext()) { String s iterator.next(); if (s.equals(B)) { iterator.remove(); // 正确通过迭代器自身的方法删除 } }使用Java 8的removeIf方法set.removeIf(s - s.equals(B));先收集要删除的元素遍历完再删适用于复杂逻辑ListString toRemove new ArrayList(); for (String s : set) { if (someComplexCondition(s)) { toRemove.add(s); } } set.removeAll(toRemove);6.4 关于Null元素处理的差异HashSet和LinkedHashSet允许放入一个null元素。TreeSet在大多数情况下不允许null。因为排序时需要比较而null无法与其它对象进行比较调用compareTo或compare会抛出NullPointerException。除非你提供了一个能够处理null值的Comparator。在实际编码中除非业务逻辑明确需要null否则应尽量避免向Set中添加null因为它会使逻辑判断变得复杂contains(null)。7. 超越基础NavigableSet与并发安全Set7.1 NavigableSet更强大的有序集合TreeSet实现了NavigableSet接口这个接口提供了比SortedSet更丰富的导航方法。前面我们已经看到了tailSet、lower等例子。这里再系统看一下范围视图subSet(E from, boolean fromInclusive, E to, boolean toInclusive)可以精确控制是否包含边界。端点访问first(),last(),pollFirst(),pollLast()后两个会移除元素。最接近匹配ceiling(E e)返回大于等于e的最小元素。floor(E e)返回小于等于e的最大元素。higher(E e)返回严格大于e的最小元素。lower(E e)返回严格小于e的最大元素。这些方法在需要“寻找附近值”的场景下非常高效比如在日程表中查找下一个最近的会议时间。7.2 线程安全的Set选择标准的HashSet、TreeSet、LinkedHashSet都不是线程安全的。在多线程环境下有几种选择Collections.synchronizedSet()SetString syncSet Collections.synchronizedSet(new HashSet());这是最简单的包装方式。它返回的集合所有方法都加了synchronized锁是线程安全的但并发性能较差因为锁的粒度是整个集合。CopyOnWriteArraySet 位于java.util.concurrent包。它的底层是一个CopyOnWriteArrayList。顾名思义它在“写”时添加、删除会复制整个底层数组因此“写”操作开销巨大但“读”操作迭代、包含判断非常快且不需要加锁。适用于读多写少且集合规模不大的场景。它的迭代器不会抛出ConcurrentModificationException因为迭代的是创建迭代器时的一个快照。基于ConcurrentHashMap的Set 这是JDK推荐的高性能并发Set实现。SetString concurrentSet ConcurrentHashMap.newKeySet();或者ConcurrentHashMapString, Boolean map new ConcurrentHashMap(); SetString keySet map.keySet(); // 这是一个线程安全的视图它的并发性能非常好因为ConcurrentHashMap使用了分段锁或CAS等更细粒度的并发控制。这是目前高并发场景下的首选。ConcurrentSkipListSet 同样位于java.util.concurrent包。它是TreeSet的并发版本底层基于跳表实现。它实现了NavigableSet提供了并发环境下的有序集合操作。当需要线程安全且有序的Set时就选它。选择哪一个取决于你的具体场景读写比例、是否需要有序、对性能的要求等。Java的Set家族虽然核心概念简单但深入下去从哈希冲突到红黑树平衡从迭代器快速失败到并发控制处处都是学问。理解HashSet、TreeSet、LinkedHashSet三者的本质差异和适用场景是写出高效、正确代码的基础。记住HashSet求快TreeSet求序LinkedHashSet求插入顺序。在复杂的业务中根据数据特性和访问模式做出合适的选择往往比盲目追求某种数据结构更重要。最后时刻警惕那些可变对象作为元素、遍历时修改集合的陷阱这些才是真正让程序在线上“翻车”的隐形杀手。

相关新闻

2026/8/26 21:46:02

链表算法精讲:Hot100经典题解与面试技巧

1. 链表专题深度解析作为一名经历过无数次算法面试的老兵,我深知链表问题在技术面试中的分量。今天要分享的这个"hot100-链表III"专题,正是剑指Offer、LeetCode等主流题库中最经典的链表问题集合。这些题目不仅频繁出现在大厂面试中&#xff0…

2026/8/26 21:46:02

AI漫剧制作全流程:Seedance2.0与即梦从分镜到成片

想要入门 AI 漫剧,第一步不是学工具操作,而是把“生成一段能看的视频”和“稳定产出一部能更新的剧集”这两件事分开。前者只需要会点按按钮,后者需要一套从文案、分镜、提示词到后期剪辑的完整工作流。Seedance2.0 和即梦的组合,…

2026/8/26 21:46:02

笔记本电脑开机原理与故障排查:从EC芯片到BIOS/UEFI的完整解析

1. 从按下电源键到屏幕点亮:一次完整的开机旅程当你按下笔记本电脑的电源键,屏幕亮起,系统开始加载,这个过程在用户看来可能只是一两秒的等待,但在机器内部,却是一场精密、有序、环环相扣的“交响乐”。很多…

2026/8/26 22:41:07

K3、GLM5.2 Coding Plan抢不到?正规替代路径与工程实践指南

想用 K3,GLM5.2 的 coding plan,结果打开页面不是排队就是名额已满,按钮灰着点不了。最近半个月,身边至少有五六个做开发的朋友问过我同一个问题:K3、GLM5.2 热度明显上来了,为什么 coding plan 这么难抢&a…

2026/8/26 22:41:07

基于飞书CLI与OpenClaw构建自动化工作流:从手动操作到智能调度

1. 从“手动点点点”到“一键触发”:为什么我们需要自动化工作流 如果你每天的工作,有超过30%的时间是在重复点击、复制粘贴、切换应用、填写表单,那么你很可能已经掉进了“低效陷阱”。我过去就是这样,尤其是在处理飞书上的各种…

2026/8/26 22:41:07

CentOS7图形化安装Oracle19c实战指南:Xvfb+OUI避坑全解析

1. 为什么在 CentOS7 上坚持用图形化方式装 Oracle19c?这真不是“炫技”你搜“centos7如何安装oracle19c”,前二十条结果里,九成是命令行静默安装(silent install)教程——贴一堆参数、改一堆响应文件、跑几条 shell 命…

2026/8/26 22:41:07

Java多进程编程实战:从ProcessBuilder到进程池的完整指南

1. 项目概述:为什么Java开发者需要关注多进程?在Java开发者的日常里,多线程(java.lang.Thread)和线程池(java.util.concurrent.ExecutorService)几乎是处理并发任务的标配。我们习惯了在同一个J…

2026/8/26 22:36:07

InpaintOnly+LaMa:ControlNet中的结构级图像修复方案

1. 这不是“换个背景”那么简单:InpaintOnly LaMa 在 ControlNet 里的真实定位ControlNet 插件在 Stable Diffusion WebUI 生态里,已经从“锦上添花”变成了“刚需基建”。但很多人装完 ControlNet,只用 Canny、OpenPose 或 Depth&#xff0…

2026/8/26 9:13:28

[光学原理与应用-521]:对光的错误理解与纠偏

首先光是一种能量的载体和形态,宏观上观察到的光是由无数个微观的光量子组成的,每个光子在产生的瞬间,其在真空的空间中以确定不变的速度沿着一个初始的方向一直向前,在微观层面,每个光量子的运动轨迹是以波函数所展现…

2026/8/25 11:48:27

SIP通话转接原理与REFER方法实战解析

1. 通话转接不是“挂断再拨号”,而是SIP会话的动态重定向你有没有遇到过这样的场景:客服坐席A正在和客户通电话,突然需要把这通对话无缝转给专家坐席B,客户完全感知不到中间的断连——既没听到忙音,也没被要求重新拨号…

2026/8/25 16:56:43

Kolla-ansible单节点OpenStack部署实战:从环境准备到排坑指南

1. 为什么选择Kolla-ansible来部署单节点OpenStack?如果你正在寻找一种能把OpenStack从“概念”快速变成“可用的实验环境”的方法,那么Kolla-ansible几乎是当前最主流、最省心的选择。我见过太多人卡在手动编译依赖、配置服务、处理版本冲突的泥潭里&am…

2026/8/26 0:04:32

Python random 模块常用函数详解:从入门到实战

目录 1. 引言2. 准备工作3. 基础随机函数4. 序列相关函数5. 随机种子与复现6. 实战案例7. 注意事项8. 常见问题与排查9. 总结 1. 引言 摘要: 本文系统介绍 Python 标准库 random 模块中最常用的随机数生成函数。内容涵盖基础随机函数(random()、unifor…

2026/8/26 1:19:35

JSON总结

JSON概念 JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式,主要用于跟服务器进行交换数据。它基于ECMAScript的一个子集。 JSON采用完全独立于语言的文本格式,但是也使用了类似于C语言家族的习惯(包括C、C、C#、Java、JavaScr…

2026/8/26 1:19:35

保存连接sse 是什么原理,为什么不会一直请求

“保持连接”用的是 SSE(Server-Sent Events),本质是一个没有马上结束的 HTTP 请求。 过程是: 拷贝机发送一次请求: GET /api/code-sync/events服务器返回: Content-Type: text/event-stream但不关闭响应&…

2026/8/26 19:34:06

实测才敢推 AI论文网站 2026最新测评与推荐

2026年真正好用的AI论文网站,核心看生成的论文质量、低AI味、格式正确、学术适配四大指标。综合实测,千笔AI、ThouPen、豆包、DeepSeek、Grammarly 是当前最值得推荐的梯队,覆盖从免费到付费、从中文到英文、从文科到理工的全场景需求。一、综…

2026/8/26 19:17:08

2026必备!AI论文网站测评:最新推荐与深度对比

2026年真正好用的AI论文网站,核心看生成的论文质量、低AI味、格式正确、学术适配四大指标。综合实测,千笔AI、ThouPen、豆包、DeepSeek、Grammarly 是当前最值得推荐的梯队,覆盖从免费到付费、从中文到英文、从文科到理工的全场景需求。 一、…

2026/8/26 19:34:05

摆脱论文困扰!盘点2026年全网爆红的的AI论文写作工具

一天写完毕业论文在2026年已不再是天方夜谭。2026年最炸裂、实测能大幅提速的AI论文写作工具,覆盖选题构思、文献整理、内容生成、格式排版等核心场景,真正帮你高效搞定论文难题。 一、全流程王者:一站式搞定论文全链路(一天定稿首…