ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java基础进阶:集合框架、排序算法与JVM排查实战

Java基础进阶:集合框架、排序算法与JVM排查实战 如果你已经能熟练写出循环、数组、if-else但一看到 ArrayList 和 HashMap 就发怵面试时被问“为什么 HashMap 线程不安全”心里没底那这篇 Java 总结进阶之路基础二刚好是给你准备的。它不是大而全的教程而是我把带新人、写项目、刷面试题时经常遇到的知识点过滤了一遍挑出最有用的那部分讲清楚集合框架的底层逻辑、排序算法的手写与工具类使用、面向对象设计细节、工程化里的自动建表以及环境配置和 JVM 报错排查。适合学完 Java 基础语法、正准备进入项目开发或面试复习的同学参考也适合想回头补基础的老工程师翻一翻。1. 集合框架进阶路上绕不过去的核心1.1 先理清集合的“族谱”很多初学者会把集合当成“一堆能存东西的类”用的时候背 API换个场景就不知道该用哪个。我建议你先记住一条主线Java 集合框架分成两大体系一个是 Collection单个元素的集合一个是 Map键值对的集合。Collection 下面又分 List、Set、Queue 三类Map 则独立成体系常见实现有 HashMap、TreeMap、LinkedHashMap、ConcurrentHashMap。可以把 List 理解成“有序书架”每一本书都有固定位置可以重复放同一本书Set 是“门禁名单”每个人只能出现一次重复的进不来Map 是“通讯录”一个姓名对应一个电话号码不能有两个相同的姓名。这套接口设计的意义在于调用方只需要面向接口编程底层实现可以随意切换比如方法参数写ListString外面传ArrayList或LinkedList都行后续替换实现不影响主流程。实际面试里我最常被问到的一个问题是“ArrayList 和 LinkedList 怎么选”。很多人张口就说“查询多用 ArrayList增删多用 LinkedList”这其实是个粗糙的结论。真实情况是如果数据量不大两者差别几乎可以忽略如果数据量很大且主要在中间插入删除LinkedList 确实有优势但它的每个节点要额外存前后指针内存开销大而且随机访问是 O(n)。大部分业务场景老老实实用 ArrayList 就够了局部性好、缓存友好遍历速度还快。1.2 底层原理ArrayList、LinkedList、HashMap 到底怎么工作ArrayList 底层就是一个 Object 数组默认容量是 10当元素个数超过容量时会扩容为新数组大小约为原来的 1.5 倍然后把旧数据复制过去。这让它“看起来像自动扩容的数组”但如果你知道会存很多数据最好在构造时就指定初始容量比如new ArrayList(1024)能省掉中间多次扩容复制的开销。LinkedList 底层是双向链表由 Node 节点组成每个节点持有 prev 和 next 引用所以它有addFirst、addLast、removeFirst这类双端操作适合实现队列或栈。但要记住链表结构遍历需要挨个找用索引取中间元素会从头或尾部开始搜索性能是硬伤。HashMap 是另一个重点。JDK 8 以后底层是“数组 链表 红黑树”的结构。插入一个键值对时先根据 key 的 hashCode 计算出数组下标如果这个下标上已经有元素就用 equals 比较key 不相等就挂在链表后面。当链表长度超过 8并且数组容量达到 64链表会转成红黑树把最坏情况下的查找时间从 O(n) 降到 O(log n)。默认加载因子是 0.75翻译成人话就是数组用了 75% 的时候触发扩容容量翻倍。这个 0.75 是时间开销和空间开销的折中调低了浪费空间调高了容易出碰撞。我说一个最常见的坑HashMap 的 key 如果是自定义对象必须重写hashCode()和equals()而且两者要保持一致。如果你只重写 equals、不重写 hashCode两个“逻辑相等”的对象会 hash 到不同下标HashMap 里就会出现重复 key查的时候还查不到。这是新手很容易踩的雷。1.3 集合使用中的三个高频坑第一个坑是在遍历集合的同时删除元素。用增强 for 循环遍历 ArrayList 时直接remove会抛出ConcurrentModificationException因为迭代器检测到集合结构被修改了。正确做法是用Iterator的remove()或者用 JDK 8 的removeIf更优雅。第二个坑是Arrays.asList返回的 List 不能随便增删。很多人以为Arrays.asList(a, b)会得到一个普通 ArrayList其实它返回的是 Arrays 内部的一个私有 ArrayList 实现底层还是固定长度的数组调用 add、remove 会直接报UnsupportedOperationException。想变成真正的可增删列表要外面再包一层new ArrayList(Arrays.asList(...))。第三个坑是集合嵌套时的“引用共享”。比如你用MapString, ListInteger存数据给多个 key 绑定了同一个 List 对象后来往 List 里加内容会发现所有 key 的值都变了。这不是集合框架的 bug而是因为存的是引用。我在写分组统计功能时就被这个坑过一次后来统一改成每次new ArrayList()再也不敢偷懒。2. 排序与算法从手写冒泡到理解 sort2.1 冒泡排序手写与优化算法这个事我不建议背代码而是理解过程。冒泡排序的核心思想很简单每一轮从头开始两两比较相邻元素大的往后挪这样每一轮结束最大的元素就像气泡一样“浮”到最后面。举个实际例子数组[5, 1, 4, 2, 8]第一轮会比较 5 和 1交换得到[1, 5, 4, 2, 8]然后 5 和 4 交换得到[1, 4, 5, 2, 8]接着 5 和 2 交换最后 5 和 8 不用换第一轮结束最大值 8 到了末尾。手写代码时大多数人能写出双层循环但很少有人会做优化。最基础的优化是加一个标志位如果某一轮没有发生任何交换说明数组已经有序直接跳出循环。我面试候选人时只要写冒泡能写出这个优化我就知道他真的理解排序过程而不是背了个模板。再进一步还可以记录每一轮最后发生交换的位置这个位置之后的部分已经有序下一轮只遍历到该位置即可这种写法叫“有序区优化”数据量大的时候能省不少时间。public static void bubbleSort(int[] arr) { if (arr null || arr.length 2) { return; } int n arr.length; for (int i 0; i n - 1; i) { boolean swapped false; for (int j 0; j n - 1 - i; j) { if (arr[j] arr[j 1]) { int tmp arr[j]; arr[j] arr[j 1]; arr[j 1] tmp; swapped true; } } if (!swapped) { break; } } }2.2 Arrays.sort 与 Comparable、Comparator 背后的逻辑业务开发中很少需要你自己写排序算法因为Arrays.sort和Collections.sort已经封装得很好了。Java 的Arrays.sort会根据目标类型和数组大小选择不同的排序算法基本类型数组用双轴快排对象数组用 TimSort一种稳定的归并排序优化版本。为什么这么设计因为快排虽然平均性能好但是不稳定的对基本类型来说稳定性没有意义而对象排序往往需要保持相同字段元素的原有相对顺序TimSort 刚好是稳定的。既然对象排序要稳定你就得让对象“可比较”。两个方案让类实现Comparable接口重写compareTo方法或者单独定义一个实现了Comparator的比较器类。前者适合类本身有天然排序规则比如按 id 排序后者适合多种排序维度随时切换比如商品既要能按价格排、又要能按销量排。ListProduct products new ArrayList(); products.sort(Comparator.comparing(Product::getPrice) .reversed() .thenComparing(Product::getSales));按价格从高到低排序价格一样再按销量排。写代码时注意一点比较器返回的应该是负数、零、正数而不是单纯返回一个布尔值很多人写成return o1.getPrice() - o2.getPrice();如果 price 是 int 类型极端情况下会出现整数溢出建议用Integer.compare或 Comparator 的静态方法。2.3 蓝桥杯与面试常见的算法模板如果目标是蓝桥杯这类算法竞赛或者面试手撕代码我建议有选择性地准备几个模板。排序方面除了冒泡至少要掌握快速排序和归并排序查找方面二分查找是必须背熟的基础字符串和数组操作方面要熟悉双指针、滑动窗口。比赛和面试强调的其实不是“你会不会用 Arrays.sort”而是“你能不能在没有工具类的限制下把算法实现出来”。还有一个在 Java 里很基础但经常被人忽略的操作数组拼接。Java 不像 JavaScript 有个concat方法想拼两个数组得自己写循环或借助 System.arraycopy也可以用 Stream 合并String[] a {a, b}; String[] b {c}; String[] c Stream.concat(Arrays.stream(a), Arrays.stream(b)) .toArray(String[]::new);至于“分组”这种常见需求Java 里也有现成工具Collectors.groupingBy可以按照某个字段把集合分组比如把订单列表按照状态分组返回一个MapInteger, ListOrder。很多新手不知道有这个用法还在手写双层循环写出来又长又容易出错。3. 面向对象与代码设计从会用语法到写出好代码3.1 封装、继承、多态的进阶理解基础语法阶段你把封装理解成“private 私有化字段、提供 getter/setter”这个没错但进阶以后要换个角度封装不是“藏数据”而是“隐藏变化”。一个好的封装外部只需要关心公开方法做了什么不需要关心内部字段怎么组织的。继承也容易被误用。我见过不少同事喜欢为了复用几个方法就搞继承结果子类和父类强耦合改一处崩一片。经验法则只有当“子类 is-a 父类”的语义成立时才用继承比如Dog extends Animal如果只是想复用实现优先考虑组合把另一个类的对象作为字段持有。多态的本质是“同一操作在不同对象上有不同行为”它依赖继承或接口实现再加上方法重写。运行时 JVM 会找到对象真实类型的方法来执行这就是动态绑定。3.2 this、super 与对象初始化的细节热词里有一条“java pojo this”说明不少人被 this 搞晕过。this 在 Java 里表示“当前对象引用”有几种典型用法区分成员变量和方法参数同名的情况在构造方法里调用同类其他构造方法比如this(name, 0)这种链式调用返回当前对象实现链式编程。children 们容易出错的是在 static 方法里用 thisstatic 方法属于类不依赖于具体对象编译期就会报错。super 则用于访问父类成员或调用父类构造方法。需要特别注意的是对象初始化顺序先执行父类静态代码块再执行子类静态代码块然后执行父类实例代码块和构造方法最后才执行子类实例代码块和构造方法。也就是说子类构造开始之前父类已经初始化完成了。很多人在这里栽跟头是因为在父类构造方法里调用了可重写方法结果子类对象还没初始化完成就提前进入了子类方法容易拿到 null 字段。3.3 接口与抽象类该怎么选这是面试和设计里绕不开的选择题。你可以按这张表来对照维度抽象类接口本质is-a 关系公共代码复用can-do 能力行为规范字段可以有实例字段JDK 8 后可以有常量但基本不建议放构造方法可以有不能有方法实现可以有抽象方法也可以有非抽象方法JDK 8 可以有 default 方法JDK 9 可以有 private 方法多继承单继承一个类可以实现多个接口我的个人经验是如果多个类之间有公共的字段和逻辑同时它们确实属于同一类事物抽象类是不错的选择如果只是想要求它们“必须具备某种能力”接口更灵活。很多框架设计都是“接口 抽象模板类”的组合拳先定义能力再提供骨架实现比如 Spring 里的InitializingBean接口配合各种 Abstract 模板类。理解这两个概念看框架源码时就不会那么吃力。4. 工程化实战MyBatis Plus 自动建表与 Spring Boot 集成4.1 为什么需要按实体类生成建表 SQL项目开发里数据库表结构频繁变更是常有的事。如果只靠 DBA 手写一大堆 DDL再让后端开发手动维护一份实体类两边很容易不同步。字段名改了一个字母数据库没改代码一跑就报“列不存在”。MyBatis Plus 是国产的 MyBatis 增强框架它根据实体类上的注解就能知道对象对应哪张表、哪个字段是主键、哪些字段要自动填充所以很多人希望能“根据实体类直接生成建表 SQL”目的就是让表结构和代码始终保持一致。这个需求背后真正的核心是“约定优于配置”。你在实体类上写TableName(user)、TableId(type IdType.AUTO)、TableField(create_time)框架就能把类字段和表字段一一对应。既然已经有这么全的映射信息为什么不顺手生成 DDL 呢理解了这一点你就知道所谓自动建表并不是什么黑魔法本质还是读取注解和字段类型拼出一段 SQL。4.2 手写一个基于实体类的建表 SQL 生成器需要说明一点MyBatis Plus 官方并没有把“自动建表”做成内置功能常见的做法是自己写一个小工具或者引入 Flyway、Liquibase 这类迁移工具。这里我分享一个轻量级思路用反射扫描实体类读取TableName、TableId、TableField注解把 Java 类型映射成数据库类型最后拼成CREATE TABLE IF NOT EXISTS语句。public String generateCreateTableSql(Class? entityClass) { TableName tableName entityClass.getAnnotation(TableName.class); StringBuilder sb new StringBuilder(); sb.append(CREATE TABLE IF NOT EXISTS ) .append(tableName.value()) .append( (\n); for (Field field : entityClass.getDeclaredFields()) { TableField tableField field.getAnnotation(TableField.class); String columnName tableField null ? field.getName() : tableField.value(); String columnType javaTypeToDbType(field.getType()); sb.append( ).append(columnName).append( ).append(columnType).append(,\n); } // ... 处理主键、去掉最后一个逗号、拼上分号 }javaTypeToDbType就是做类型映射的方法比如String映射成VARCHAR(255)Integer映射成INTLocalDateTime映射成DATETIME。核心逻辑不难但有三个细节要注意主键字段要单独处理因为可能要加PRIMARY KEY和自增属性字段注释最好从TableField(comment ...)或 Swagger 注解里取生成的表才可维护字符串类型要处理长度不能一刀切都是默认长度。说实话如果你在团队里维护的表不多这个生成器很好用。但一旦表数量超过几十张还是建议用 Flyway 管理数据库迁移脚本可追踪、可回滚、更规范。4.3 Spring Boot MyBatis 多商户项目中的实战经验热词里有“spring boot mybatis 的开源多商户跨境商城”说明现在是多商户系统很常见的时代。这类项目里MyBatis Plus 的几个功能很有用一是逻辑删除配置TableLogic删除时变成更新二是乐观锁配合Version字段更新时自动带上 version 条件三是分页插件通过 MyBatis Plus 的PaginationInnerInterceptor实现。多商户系统还有一个绕不开的问题数据隔离。如果一张 order 表存所有商户的订单SQL 里基本上都要带merchant_id条件不然 A 商户就能查到 B 商户的数据。我建议用 MyBatis Plus 的拦截器做“行级权限”在 SQL 解析阶段自动加上merchant_id ?的条件前端登录后从上下文中拿到当前商户 ID。这样就不需要每个业务代码手写这个条件也不会漏写。支付、库存这类场景“数据一致性”是必须考虑的。最简单的是利用数据库事务保证本地一致性但涉及跨库操作时就要考虑分布式事务方案。我的建议是能用本地事务解决的绝不上分布式必须跨服务时优先考虑最终一致性方案比如本地消息表加消息队列重试别一上来就搞强一致成本太高。5. 环境配置与 JVM 排查启动失败与 OOM 怎么处理5.1 多 JDK 共存与 JAVA_HOME 切换很多人的电脑上不止一个 JDK项目 A 要求 JDK 8项目 B 用 JDK 17于是便出现了“Java 环境变量使用多个 JDK”的困惑。Windows 上环境变量里配置的JAVA_HOME只有一个切换版本最简单的方法是建几个不同的环境变量名比如JAVA_HOME_8和JAVA_HOME_17需要切到哪个版本时把JAVA_HOME的值改过去。Linux 服务器上更推荐用update-alternatives来管理多个 JDK它本质是软链接管理工具。另外老服务器上安装 JDK 时要留意一个问题32 位系统装不了 64 位 JDK下载前先用uname -m确认架构。还有一个经常被忽略的点改完/etc/profile或~/.bashrc里的 JAVA_HOME 后要记得source一下或者重新登录终端否则环境变量不会生效。很多“Java 启动失败”其实根本不是代码问题就是环境变量没刷新。5.2 IDEA 编译时的 OutOfMemoryError热词里有个很典型的求助“IDEA 编译时进程堆大小调整为 8000还是报错java.lang.OutOfMemoryError”。这里有个常见误区你以为调大堆内存就能解决但 IDE 的编译进程和运行程序不是同一个 JVM。IDEA 自身的堆内存配置在安装目录的idea64.exe.vmoptions文件里调整的是 IDE 主进程而实际编译时如果用 Maven 构建Maven 编译器插件跑在单独的 JVM 里内存由MAVEN_OPTS或jvmArguments控制。所以排查步骤应该是先看异常信息是Java heap space还是Metaspace。Java heap space才是堆不够Metaspace是元空间不够要调MaxMetaspaceSize。如果编译报错发生在执行 Maven compiler 插件期间就去改 Maven 的 JVM 参数比如MAVEN_OPTS-Xmx2g。如果项目模块特别多IDEA 会启用多个编译进程单独调一个内存可能不够考虑减少并行编译模块数。我遇到过有人把-Xmx调到 8000 兆结果本机内存一共就 8GIDE 都快卡死了还在报错。调参数之前先jvisualvm看一下到底哪个进程在报错别盲调。5.3 数据库连接与数据权限问题老项目里连接 SQL Server 2008 的场景依然不少。JDBC 连接串要写成jdbc:sqlserver://localhost:1433;DatabaseNametest驱动要用sqljdbc4或mssql-jdbc。2008 版本比较老连接时容易报“TLS 协议不匹配”之类的问题除了升级驱动还要确认 JDK 版本是否兼容。这类老数据库适配问题我的经验是先看数据库版本和驱动版本对应表再看 JDK 版本最后才怀疑代码。行级权限前面提过拦截器方案这里补充一句如果只是报表类查询也可以在 SQL 层用视图把商户条件固定进去但页面查询还是要业务代码主动传参。数据一致性方面如果只是单表单条记录更新用乐观锁就可以如果是复杂业务链路优先用事务边界划分清楚把不必要的事务范围缩小避免长事务锁表。这些都是我写商城项目时一点点踩出来的经验。6. 面试知识点与学习路线打好基础二这张牌6.1 面试常问的 Java 基础题清单准备面试时基础二阶段应该有意识地整理一份自己的“八股文”清单但不要死背要能讲出原理。我整理了高频考点知识点核心问题一句话解答方向StringString 为什么不可变内部 char 数组被 final 修饰且不提供修改方法异常体系checked 和 unchecked 区别编译器强制检查与运行时抛出泛型泛型擦除是什么运行时泛型类型被擦除用 Object 代替反射为什么反射性能低需要解析类元数据跳过编译期优化动态代理JDK 代理和 CGLIB 区别代理类基于接口还是继承Cleaner和 finalize 区别Cleaner 用幻象引用释放资源但不保证及时执行HashMap为什么线程不安全并发扩容可能死循环数据覆盖数据一致性事务和锁的关系隔离级别、乐观锁、悲观锁、最终一致Cleaner 这个概念可能有人不熟它是 JDK 9 引入的用来替代 finalize 的对象清理机制底层依赖 PhantomReference 和一个守护线程。它适合清理堆外内存、DirectByteBuffer 这类资源但不应该被当成业务逻辑的一部分因为清理时机完全由 GC 决定你永远不知道它什么时候跑。AES 解密这类安全编码知识也常被问到。实际工作里敏感字段加密一般用 AES 对称加密因为性能好密钥管理是关键不要让密钥硬编码在代码里可以放到配置中心或环境变量中。加密和解密要指定同一套字符集和填充模式比如AES/CBC/PKCS5Padding否则两端各用各的参数解密必定失败。6.2 Java 与 Python 怎么选学习路线怎么走很多人在学基础时纠结 Java 还是 Python。我的看法是看你想做哪条路的开发。Java 的生态庞大且稳定企业级后端、电商、金融、大数据都大量使用岗位需求一直很稳Python 在数据分析、人工智能、脚本自动化方面占优势但纯后端岗位的深度和广度相对不如 Java。两者语法差异不算大掌握一种语言后转另一种一般两到三周就能上手核心是编程思维。Java 的学习路线我建议分四段走第一段是基础语法和面向对象也就是你现在所在的“基础二”阶段目标是能独立写一个小作业第二段是集合、IO、多线程、网络编程配合刷一遍蓝桥杯或力扣里的入门题第三段入 Spring Boot、MyBatis 等框架做一个完整的 Web 项目比如后台管理系统、商城第四段再看 JVM、并发源码、分布式理论这时候你已经具备“看源码”的能力了。每一段都要配合项目输出只刷视频不动手过两个月就全忘了。6.3 从基础二到工程实战的最后一公里最后分享一个我从带新人中总结出来的观点基础二学完的标志不是你能默写 HashMap 的扩容公式而是你遇到一个问题时知道自己“不知道什么”。比如看到ConcurrentModificationException你能联想到迭代器和 fail-fast看到编译 OOM你能快速判断是哪个 JVM 进程内存不够看到建表需求你能想到注解、反射、DDL 生成这些工具。这种能力比背多少 API 都值钱。我自己写代码有个小习惯临时变量喜欢用tmp循环变量用i、j这不是什么高深的规范但能让代码一眼看出是临时数据。命名规范不是面试考点却是真实工作里代码可读性的基础。至于那些“Java 最新网站更新入口”之类的东西我建议大家少看乱七八糟的网站直接去官方文档和 GitHub 开源项目质量有保证。实际开发中还有一个看起来不起眼但特别影响体验的事情工程化配置和调试。很多项目不是逻辑难写而是环境配置、依赖版本、启动参数这些“旁边的事”把人卡死。我自己的做法是每到一个新项目先把启动流程在本地完整跑通再开始改业务代码。这是最笨但最有效的方法能帮你避开后面 80% 的 JVM 和数据库问题。
返回列表