Java数组深度解析:从内存模型到性能优化与实战避坑指南
1. 从零到一为什么数组是Java程序员的“第一块砖”如果你刚开始学Java或者正在准备面试那么“数组”这个概念你肯定绕不过去。它看起来简单不就是一组相同类型数据的集合吗但在我带过的新人里十个有八个都在数组上栽过跟头。面试官也特别喜欢拿数组做文章从基础的声明、遍历到内存模型、性能对比再到各种“八股文”式的变形题比如“数组和ArrayList有什么区别”、“如何实现数组去重”、“二维数组在内存中怎么存储”。这些问题如果你只停留在“会用”的层面而没有理解其背后的“所以然”很容易就被问懵。数组之所以重要是因为它是几乎所有高级数据结构如ArrayList、HashMap的底层基石之一。理解数组就相当于理解了计算机如何组织和管理连续内存空间的最基本方式。很多新手觉得集合框架更方便就直接跳过了数组的深入学习这其实埋下了隐患。当你遇到需要极致性能的场景或者去排查一些诡异的ArrayIndexOutOfBoundsException数组下标越界异常时对数组内存模型的深刻理解就是你的救命稻草。这篇文章我们就抛开那些枯燥的教科书定义从一个一线开发者的视角重新拆解Java数组。我会结合我这些年踩过的坑和面试中常考的点不仅告诉你怎么用更重点剖析为什么这么用以及在不同场景下该如何选择。我们会从最基础的内存模型讲起一直深入到一些不常用但关键时刻很有用的“骚操作”。目标是让你看完后不仅能轻松应对面试题更能写出更高效、更健壮的代码。2. 内存中的数组不止是“连续存储”那么简单几乎所有教材都会告诉你数组在内存中是连续存储的。这句话没错但太笼统了。连续存储到底带来了什么好处又有什么代价我们得把它掰开揉碎了看。2.1 寻址的魔法为什么数组能 O(1) 访问数组能做到通过下标在常数时间复杂度O(1)内访问任意元素其核心秘密就在于这个“连续存储”和一套简单的地址计算公式。假设我们有一个int[] arr new int[10];。在Java中arr这个变量本身存放在栈内存或某个方法的局部变量表中它存储的值并不是数组内容本身而是数组对象在堆内存中的首地址引用。当我们执行arr[5]时JVM内部会进行如下计算获取数组的起始内存地址假设是baseAddress。获取目标元素的索引这里是5。获取数组中每个元素的大小对于int是4个字节。计算目标元素的内存地址targetAddress baseAddress 5 * 4。这个计算过程是固定且极快的与数组长度无关所以是O(1)。这也是数组相比链表的最大优势。但这也是把双刃剑为了保持“连续”数组在初始化时必须指定固定长度且后续难以改变。注意这里的“连续”是逻辑上的。对于引用类型数组如String[]数组中连续存储的是各个String对象的引用地址这些引用是连续存放的但它们各自指向的String对象在堆中的位置则是散落的。这一点常常被混淆。2.2 多维数组的“套娃”模型对于二维数组int[][] matrix new int[3][4];很多初学者会想象成一个整齐的3行4列的矩形。但在内存中它更像一个“数组的数组”。首先matrix这个引用指向堆中的一个数组对象A这个数组A的长度是3它的每一个元素都是一个int[]类型的引用。这些引用初始时都是null。当我们通过new int[3][4]一次性创建时JVM会紧接着创建3个新的int[]数组对象B1, B2, B3每个长度都是4并把它们的地址分别赋给数组A的第0、1、2个位置。因此访问matrix[1][2]实际上是两次寻址先通过matrix找到数组A取它的第1个元素这是一个引用再通过这个引用找到对应的行数组B2最后取这个行数组的第2个元素。这种非真正的“矩形”存储也允许我们创建不规则数组Jagged Array比如int[][] arr new int[3][]; arr[0] new int[2]; arr[1] new int[5];。2.3 从OutOfMemoryError看数组的内存代价你可能会在日志里看到这样的错误java.lang.OutOfMemoryError: Java heap space。当你尝试创建一个非常大的数组时比如int[] hugeArray new int[Integer.MAX_VALUE - 8];就很容易触发这个错误。为什么是Integer.MAX_VALUE - 8因为数组对象本身有一个小的对象头Object Header用来存储类元数据、数组长度等信息。在HotSpot虚拟机中数组长度的存储字段是int类型所以理论上的最大元素数量是Integer.MAX_VALUE。但虚拟机实现需要一些额外的空间所以实际允许的最大值会稍小一些这个-8就是一个常见的经验值不同JVM可能有差异。这个错误提醒我们使用数组时必须对数据规模有清晰的预估。如果数据量动态变化且可能很大盲目使用大数组是危险的。这时就需要考虑使用ArrayList它内部也是数组但支持动态扩容或者其他基于链表的数据结构。3. 声明、创建与初始化细节里的魔鬼声明和创建数组的语法看起来简单但里面的门道不少写错了编译器可能不会报错但运行时行为会非常诡异。3.1 声明方式的细微差别// 方式一声明并立即初始化推荐清晰 int[] arr1 {1, 2, 3, 4, 5}; // 方式二先声明后初始化 int[] arr2; arr2 new int[]{1, 2, 3}; // 注意这里不能省略 new int[] // 方式三声明时指定长度元素为默认值 int[] arr3 new int[5]; // arr3 [0, 0, 0, 0, 0] String[] strArr new String[3]; // strArr [null, null, null] // 容易混淆的错误写法 int[] arr4; // arr4 {1,2,3}; // 编译错误这种简写语法只能在声明时直接使用。关键点{1,2,3}这种语法糖是编译器在编译期就能确定数组大小和内容的便捷写法所以它不能用于赋值语句的右边。而new int[]{1,2,3}是在运行时创建数组对象所以两者都可以。3.2 默认初始化别被“零值”坑了对于new int[5]JVM会自动将所有元素初始化为其类型的默认值int/short/byte/long-0float/double-0.0char-\u0000(空字符)boolean-false引用类型如String,Object -null最后一点是新手常踩的坑。比如你定义了一个Person[] people new Person[10];然后直接调用people[0].getName()就会抛出NullPointerException。你必须先为每个数组位置new一个Person对象。3.3 动态初始化与静态初始化的选择静态初始化int[] arr {1,2,3};适用于数据已知且固定的场景代码简洁。动态初始化int[] arr new int[n];适用于数据大小在运行时才能确定的场景比如从文件读取、网络接收。在实际项目中更常见的是动态初始化。例如从数据库查询一批用户ID数量不确定你可以先查询总数count然后Long[] userIds new Long[count];再循环填充。4. 遍历、拷贝与填充高效操作必备技巧数组创建好了接下来就是操作它。遍历是最基本的但怎么遍历也有讲究。4.1 三种遍历方式与性能迷思int[] arr new int[1000000]; // 方式1传统for循环 long start System.currentTimeMillis(); for (int i 0; i arr.length; i) { int value arr[i]; // 操作value } long time1 System.currentTimeMillis() - start; // 方式2增强for循环 (foreach) start System.currentTimeMillis(); for (int value : arr) { // 操作value } long time2 System.currentTimeMillis() - start; // 方式3使用Java 8 Stream API (通常最慢因为涉及装箱、流水线开销) start System.currentTimeMillis(); Arrays.stream(arr).forEach(value - { // 操作value }); long time3 System.currentTimeMillis() - start;很多人会纠结哪种方式更快。对于数组在纯遍历场景下传统for循环和增强for循环的性能在JIT编译器的优化下差异微乎其微可以忽略。增强for循环语法更简洁不易出错不会下标越界是首选。但如果你需要在循环中用到下标比如同时处理两个数组的对应位置那就必须用传统for循环。Stream API的抽象层次更高功能强大过滤、映射、归约但它的开销也最大在极端性能敏感的遍历场景下不推荐。它胜在表达清晰适合复杂的集合处理逻辑。4.2 深拷贝与浅拷贝引用类型数组的“陷阱”这是面试高频题也是实际开发中Bug的重灾区。class Person { String name; Person(String name) { this.name name; } } Person[] original {new Person(Alice), new Person(Bob)}; // 浅拷贝 - 错误示范 Person[] shallowCopy original.clone(); // 或者 Arrays.copyOf(original, original.length) shallowCopy[0].name Carol; System.out.println(original[0].name); // 输出 Carol原始数据被改了。 // 深拷贝 - 需要手动复制对象 Person[] deepCopy new Person[original.length]; for (int i 0; i original.length; i) { deepCopy[i] new Person(original[i].name); // 创建新对象复制内容 } deepCopy[0].name David; System.out.println(original[0].name); // 输出 Carol原始数据未变。clone()和Arrays.copyOf()对于数组来说创建了一个新的数组对象但是新数组里的每个元素只是简单复制了原数组对应位置的引用。对于基本类型数组这没问题因为值被拷贝了。但对于引用类型数组这导致两个数组的元素指向同一个对象修改一个会影响到另一个这就是“浅拷贝”。要实现“深拷贝”你必须遍历数组为每个元素创建新的对象并复制其状态。如果对象内部还有引用其他对象那拷贝会变得非常复杂可能需要序列化/反序列化如使用Jackson、Gson或专门的拷贝工具如Apache Commons Lang的SerializationUtils.clone()但要求类实现Serializable。4.3Arrays工具类的妙用java.util.Arrays是一个宝藏类封装了大量静态方法。快速填充Arrays.fill(arr, 1)将整个数组填充为1。比写循环快而且代码意图更明确。快速排序Arrays.sort(arr)。对于对象数组需要对象实现Comparable接口或传入Comparator。它使用的是经过高度优化的双轴快速排序Dual-Pivot Quicksort或归并排序比自己手写排序可靠得多。二分查找int index Arrays.binarySearch(arr, key);。前提是数组必须已排序如果返回负数表示没找到其值为-(插入点) - 1这个特性可以用来计算插入位置。比较与哈希Arrays.equals(arr1, arr2)和Arrays.hashCode(arr)。它们会深度比较数组内容而不是比较引用非常实用。流式处理Arrays.stream(arr)这是连接数组和Stream API的桥梁。5. 数组的典型“坑”与实战应对策略理论懂了不代表实战不踩坑。下面这些场景都是我或同事真实遇到过的。5.1ArrayIndexOutOfBoundsException下标越界的N种死法这是最常见的运行时异常之一。除了明显的arr[arr.length]这种错误还有一些隐蔽的场景循环条件写错for(int i0; iarr.length; i)应该是i arr.length。动态计算的下标从其他方法获取的下标或者通过复杂计算得到的下标没有做有效性校验。并发修改在多线程环境下一个线程在遍历数组基于length另一个线程修改了数组引用比如指向了一个更小的新数组也可能导致越界。虽然数组本身长度不可变但数组引用可变。防御性编程建议在访问数组元素前尤其是下标来自外部输入或复杂计算时务必检查if (index 0 index arr.length) { ... }。5.2 数组作为方法参数与返回值的秘密数组是引用类型。这意味着当你把数组作为参数传给一个方法时传递的是引用的副本但这个副本和原引用指向同一个数组对象。void modifyArray(int[] array) { array[0] 100; // 这会修改调用方原始的数组内容 array new int[]{7,8,9}; // 这只改变了方法内部局部引用array的指向不影响外部 } public static void main(String[] args) { int[] myArr {1,2,3}; modifyArray(myArr); System.out.println(Arrays.toString(myArr)); // 输出 [100, 2, 3] }这个特性可以用来在方法内部修改数组内容非常方便。作为返回值时也是返回引用需要注意避免返回内部可变数组的引用导致封装性被破坏除非这是你故意的。5.3 多维数组遍历的性能陷阱遍历一个M x N的二维数组哪种循环顺序更快int[][] matrix new int[10000][10000]; // 顺序一外层行内层列 CPU缓存友好 long start System.currentTimeMillis(); for (int i 0; i matrix.length; i) { for (int j 0; j matrix[i].length; j) { matrix[i][j] i j; } } // 顺序二外层列内层行 CPU缓存不友好跳跃访问 for (int j 0; j matrix[0].length; j) { for (int i 0; i matrix.length; i) { matrix[i][j] i j; } }在现代CPU架构下由于存在多级缓存Cache顺序一的访问模式逐行访问是“空间局部性”友好的当前访问的内存地址附近的数据很可能已经被预加载到缓存中所以速度远快于顺序二逐列访问每次访问都跳很远。在性能关键代码中如图像处理、矩阵运算这一点至关重要。6. 数组 vs. 集合框架 (ArrayList)如何选择这是面试必问题也是设计代码时需要做的首要决策之一。特性数组ArrayList(基于数组实现)长度固定创建后不可变。动态自动扩容。类型安全编译时检查对于泛型数组有缺陷。运行时泛型擦除但编译时检查良好。性能读/写 O(1)内存开销极小仅对象头数据。读/写 O(1)但涉及扩容时是O(n)。有额外的对象开销。功能功能简单仅有基本操作。功能丰富提供添加、删除、查找、迭代器等大量方法。多线程本身非线程安全但长度不变在某些只读场景下安全。非线程安全。可用Collections.synchronizedList包装或使用CopyOnWriteArrayList。适用场景1. 数据长度明确且不变。2. 极致性能要求如底层算法、数值计算。3. 表示多维数据如矩阵。1. 数据长度未知或频繁变化。2. 需要丰富的集合操作。3. 大多数业务代码场景。选择心法当你百分之百确定元素数量不会变且对性能有苛刻要求用数组。例如存储一周七天的名称存储RGB颜色通道值。其他所有情况优先考虑ArrayList。它的便利性和安全性远胜于那一点点微乎其微的性能损失。现代JVM对ArrayList的优化已经非常好。不要过早优化。除非性能分析工具如Arthas明确告诉你数组操作是瓶颈否则就用ArrayList。7. 进阶话题当数组遇到泛型、反射与JVM调优7.1 泛型数组的“禁区”与变通Java不允许直接创建泛型数组如new ListString[10];是编译错误。这是因为Java的泛型是通过类型擦除实现的运行时ListString和ListInteger都是List而数组却需要在运行时知道其确切的元素类型以进行类型检查这就产生了矛盾。变通方法是使用原始类型数组然后强制转换但这失去了部分类型安全需要开发者自己保证。// 不推荐有警告 ListString[] listArray (ListString[]) new List[10]; // 推荐使用集合的集合 ListListString listOfLists new ArrayList();7.2 通过反射操作数组java.lang.reflect.Array类提供了动态创建和操作数组的能力这在框架开发中很有用。// 动态创建一个String数组长度为5 Object strArray Array.newInstance(String.class, 5); // 设置值 Array.set(strArray, 0, Hello); // 获取值 String element (String) Array.get(strArray, 0); // 获取长度 int length Array.getLength(strArray);7.3 JVM参数对大型数组的影响当你需要处理非常大的数组几百MB甚至GB级别时JVM的堆参数设置就很重要。-Xms和-Xmx设置堆的初始大小和最大大小。确保最大值足够容纳你的数组。-XX:UseLargePages启用大内存页可以减少TLB Miss提升大内存访问性能。警惕OutOfMemoryError: Requested array size exceeds VM limit这通常意味着你尝试分配的单个数组大小超过了JVM对单个对象大小的限制约Integer.MAX_VALUE - 8个元素。数组是Java里最基础、最古老的数据结构但它蕴含的计算机原理却非常深刻。从内存连续存储带来的高效随机访问到多维数组的引用“套娃”再到与集合框架的对比选择每一个点都值得深入琢磨。我见过太多程序员因为轻视数组在遇到性能问题或诡异Bug时束手无策。把数组吃透不仅是应付面试更是夯实你Java编程内功的关键一步。下次当你下意识地想用ArrayList时不妨先问自己一句这里的数据规模真的会变吗用数组是不是更合适多这一层思考你的代码质量就会不一样。

相关新闻

飞鸽网站建设如何助力中小企业在2024年实现低成本高效获客与品牌升级的全面解析

飞鸽网站建设如何助力中小企业在2024年实现低成本高效获客与品牌升级的全面解析

今天咱们不聊那些高高在上的互联网黑科技,也不整那些虚头巴脑的融资神话,就聊聊最实在的东西——怎么做一个好网站,以及为什么对于现在的中小企业来说,一家靠谱的飞鸽网站建设团队或者服务,可能是你今年性价比最高的投资。很多老板或者市场负责人在提到“网站建设”这四个…

2026/10/9 21:03:26 阅读更多 →
AI工具提升毕业设计效率:文献到代码全流程优化

AI工具提升毕业设计效率:文献到代码全流程优化

1. 毕业设计效率革命:AI工具全景解析2023年堪称AI技术落地元年,各大语言模型和智能工具如雨后春笋般涌现。作为刚带完本届毕业设计的导师,我发现那些善用AI工具的学生普遍能提前2-3周完成论文初稿,代码质量也比往届有明显提升。本…

2026/10/10 14:09:34 阅读更多 →
计算机视觉197个SOTA模型全解析:从AlexNet到Stable Diffusion的架构演进与实战指南

计算机视觉197个SOTA模型全解析:从AlexNet到Stable Diffusion的架构演进与实战指南

1. 项目概述:一份计算机视觉从业者的“藏宝图”在计算机视觉这个日新月异的领域,无论是刚入门的新手,还是深耕多年的老手,都面临着一个共同的挑战:模型太多了。今天刚读完一篇论文,明天就出了三个变体&…

2026/10/10 15:16:45 阅读更多 →

最新新闻

SpringBoot整合MyBatis实战:从配置到原理,常见问题全解析

SpringBoot整合MyBatis实战:从配置到原理,常见问题全解析

1. 项目概述与核心价值这块内容的核心就是解决一个很实际的问题:怎么让 SpringBoot 项目和 MyBatis 顺利“牵手”。很多刚开始接触这套组合的朋友,最直观的感受是“配置怎么这么多”“为什么我的 Mapper 接口扫不到”“明明 SQL 没问题但就是报错”。这很…

2026/10/12 3:22:59 阅读更多 →
remocn一次性讲透27种Remotion场景转场:从硬切到Shader擦除

remocn一次性讲透27种Remotion场景转场:从硬切到Shader擦除

【免费下载链接】remocn Production-ready animations, transitions, backgrounds, and scenes for Remotion 项目地址: https://gitcode.com/gh_mirrors/re/remocn 点击查看 免费下载 remocn 是一个面向 Remotion 的复制粘贴式动效组件库,它的 Transit…

2026/10/12 3:22:59 阅读更多 →
interview-go 仓库导读:Golang 面试题知识体系与实战解析指南

interview-go 仓库导读:Golang 面试题知识体系与实战解析指南

文档教程后端 【免费下载链接】interview-go golang面试题集合https://interview.disign.me/ 项目地址: https://gitcode.com/gh_mirrors/in/interview-go 点击查看 免费下载 本文以 interview-go 仓库的导航文档 docs/README.md 为骨架,系统梳理这份「…

2026/10/12 3:22:58 阅读更多 →
LiteLLM实战:统一多模型接入与网关部署的完整指南

LiteLLM实战:统一多模型接入与网关部署的完整指南

近一两年做 AI 应用,最让我头疼的往往不是模型本身的能力,而是“怎么把各个模型顺畅地接进自己的代码”。每家模型服务商的接口风格都不同,有的偏好流式事件,有的用完全不一样的鉴权方式,有的连请求字段和返回结构都有…

2026/10/12 3:22:58 阅读更多 →
Oat UI 数据表格(Table)组件实战:零依赖语义化表格与源码级样式解析

Oat UI 数据表格(Table)组件实战:零依赖语义化表格与源码级样式解析

【免费下载链接】oat Ultra-lightweight, zero dependency, semantic HTML, CSS, JS UI library. ~10KB mingz. 项目地址: https://gitcode.com/gh_mirrors/oat5/oat 点击查看 免费下载 Oat 是一个超轻量、零依赖的语义化 HTML/CSS/JS UI 库(约 10KB mi…

2026/10/12 3:22:58 阅读更多 →
具身智能中的无线技术——无线线束1

具身智能中的无线技术——无线线束1

无线线束是具身智能领域的颠覆性技术&#xff0c;通过用无线电波替代机器人内部的信号线&#xff0c;解决线缆易断、重量大、灵活性受限等问题。其核心在于实现低时延&#xff08;<20μs&#xff09;、高可靠性、抗干扰的无线通信&#xff0c;已在灵巧手、触觉皮肤和电池管理…

2026/10/12 3:21:58 阅读更多 →

日新闻

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

在数码相机、高清显示屏与现代矢量图形技术高度发达的今天&#xff0c;画面可以做到绝对的锐利、平滑与无瑕。然而&#xff0c;当一张秋日手账插画或拍立得照片过于“平整无瑕”时&#xff0c;往往会散发出一种冰冷生硬的“数码塑料感&#xff08;Digital Plasticity&#xff0…

2026/10/12 0:00:59 阅读更多 →
活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

在现代网页与移动端设计中&#xff0c;横排&#xff08;Horizontal Layout&#xff09;早已经成为了绝对的主流。然而&#xff0c;当我们翻开泛黄的线装古籍、宋版木刻诗集&#xff0c;或是欣赏一张茶道雅集的手写便签时&#xff0c;那种**自上而下纵向书写、自右向左逐列铺展&…

2026/10/12 0:00:59 阅读更多 →
周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

每到周日的晚上八点到十点&#xff0c;很多人心里都会悄悄亮起一盏警示灯。 在心理学上&#xff0c;这种现象有一个专门的称谓——“周日夜晚焦虑症&#xff08;Sunday Scaries&#xff09;”。明天又是周一&#xff0c;闹钟又要重新在七点响彻卧房&#xff1b;脑海里仿佛有一个…

2026/10/12 0:00:59 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介&#xff1a;基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码&#xff0c;面向计算机相关专业课程设计与期末大作业学生&#xff0c;以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程&#xff0c;…

2026/10/12 0:16:30 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程&#xff1a;键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化&#xff0c;十个新手有八个栽在"往输入框里填东西"这件事上&#xff1a;要么填不进去&#xff0c;要么填了一半&#xff0c;要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程&#xff1a;阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀&#xff1a;什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面&#xff0c;跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/12 0:16:43 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 10:45:37 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 14:36:53 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 14:36:54 阅读更多 →