本技能面向已经掌握 Java 语言的开发者,提供生产级 Java 21+(当前 LTS 为 25)的惯用写法与决策规则,重点不是语法教学,而是代码评审中反复出现的陷阱,例如对包装类型使用 ==、Collectors.toMap 遇到重复键、自调用上的 @Transactional、被钉住的虚拟线程,以及测试任务跑了零个 JUnit 5 测试却报告成功的场景。
构建工具:应使用仓库自带的包装器(./gradlew、./mvnw),它固定了构建工具版本;并保持现有构建工具不变,从 Maven 迁移到 Gradle 属于另行请求的变更。文中给出 Gradle 与 Maven 的对应命令,包括完整门禁(./gradlew check / ./mvnw -q verify)、编译主代码与测试、单元测试、单个测试、依赖树、依赖来源查询以及格式化。Gradle 的 check 会运行测试以及插件挂接的所有校验任务;未变更输入的测试任务会被跳过为 UP-TO-DATE,可用 --rerun 强制重跑。Maven 中 Surefire 在 test 阶段运行单元测试,Failsafe 运行 *IT.java;需在 pluginManagement 中固定 maven-surefire-plugin 3.x,因为旧版 super-POM 绑定的 2.12.4 不运行 JUnit 5 测试却报告成功。版本管理上 Gradle 使用版本目录并配合 platform(...) 导入 BOM,Maven 在 dependencyManagement 中用 import 作用域导入 BOM;两者的冲突解决策略不同,Gradle 取请求的最高版本,Maven 取依赖树中最近的声明,可能静默降级传递依赖,可用 maven-enforcer-plugin 的 dependencyConvergence 规则将其变为构建错误。
静态分析:包括 javac -Xlint:all -Werror、Error Prone(编译期缺陷模式)、NullAway(配合 JSpecify 的 @NullMarked/@Nullable)、SpotBugs(字节码分析)、Checkstyle 或 PMD(风格规则),以及用 Spotless 运行 google-java-format 或 palantir-java-format 进行格式化;文中给出了一段 build.gradle.kts 示例,其中包含工具链、JUnit BOM、Error Prone 依赖和 useJUnitPlatform() 配置。
语言特性:用 record 建模数据、用 sealed interface 表示封闭备选集合,switch 可检查穷尽性(Java 21);record 的紧凑构造器可用于校验,嵌套在 sealed 接口中的 record 无需 permits 子句,Jackson 2.12+ 通过规范构造器反序列化 record。record 是浅不可变的,可变组件需在紧凑构造器中复制(List.copyOf、Map.copyOf),数组组件保留引用相等,需覆写 equals/hashCode。sealed 类型上的 switch 不应保留 default 分支,以便新增子类型时编译失败。instanceof 模式可一步完成绑定与收窄,Java 22 的未命名模式 _ 可忽略不用的组件。固定集合且无每实例数据时用枚举,EnumMap/EnumSet 比哈希集合更快;var 仅用于右侧已表明类型的局部变量。
错误与空值:对调用方可处理的情况使用受检异常,对缺陷和违反前置条件使用非受检异常;受检异常不能穿过 lambda,可在流中包装为 UncheckedIOException 并在边界解包。包装异常时必须传入 cause,否则丢失原始堆栈。try-with-resources 按相反顺序关闭资源,close() 抛出的异常作为 suppressed 附加。InterruptedException 需在返回或重抛非受检异常前用 Thread.currentThread().interrupt() 恢复标志。Optional 只应作为返回类型,避免用于字段、参数和集合;orElse 总会求值,orElseGet 是惰性的,优先用 orElseThrow() 而非 get()。在公共边界用 Objects.requireNonNull 校验,配合 JSpecify @NullMarked 与 NullAway 在编译期检查空值,返回空集合而不是 null。
流与集合:Collectors.toMap 不带合并函数时遇到重复键抛 IllegalStateException、遇到空值抛 NullPointerException;Stream.toList() 返回不可修改但允许 null 的列表,Collectors.toList() 不承诺可变性,Collectors.toUnmodifiableList() 拒绝 null。List.of、Set.of、Map.of 不可变且拒绝 null,Set.of/Map.of 在构造时对重复项抛异常,迭代顺序未指定且在不同 JVM 运行间变化,凡顺序可观察处应排序或使用 LinkedHashMap/TreeMap。Arrays.asList 尺寸固定且写回底层数组。Java 21 的顺序集合提供 getFirst()、getLast()、reversed()。流是惰性且单次使用的,peek/map 中的副作用可能不执行;parallelStream() 使用 JVM 级公共 ForkJoinPool,其中阻塞 IO 会拖垮其他并行流,只应在实测后用在大输入量的 CPU 密集型工作。Stream gatherers(24 中定稿)支持窗口与有界并发映射。
并发:虚拟线程(21)用于高并发阻塞 IO,每个任务创建一个且绝不池化,执行器无大小限制,需用 Semaphore 限制对下游服务的调用。JDK 24 之前 synchronized 中的阻塞会钉住载体线程,JEP 491(JDK 24)已移除这一点,但本地帧仍会钉住;在 21 上可用 ReentrantLock 保护阻塞调用并关注 jdk.VirtualThreadPinned JFR 事件。ThreadLocal 在虚拟线程上可用但每线程一份副本;ScopedValue(25 定稿)以不可变方式向下传递请求上下文。CPU 密集型工作应放在按核心数设置的平台线程池或 ForkJoinPool。CompletableFuture.supplyAsync 不传执行器时运行在公共池,join() 抛非受检的 CompletionException,get() 抛受检的 ExecutionException,链末应挂 exceptionally/handle。StructuredTaskScope 在 25 中仍是预览 API。共享状态可用 ConcurrentHashMap.merge/compute 做原子读改写、LongAdder 做竞争计数、volatile 做单写者标志,不可变 record 可无锁跨线程。Spring Boot 3.2+ 可设置 spring.threads.virtual.enabled=true 用虚拟线程处理请求。
包与模块:标准目录为 src/main/java、src/main/resources、src/test/java、src/test/resources,目录路径与包名一致。实现类默认包私有,仅 API 标记 public;同包测试可直接访问包私有成员。JPMS 中导出 API 包、为 API 暴露的类型使用 requires transitive、为反射框架需读取的包使用 opens,缺少 opens 会在运行时以 InaccessibleObjectException 出现;不发布库的应用通常留在类路径上。
Spring Boot:可用 @ConfigurationProperties 配合 @Validated 绑定 record 属性,并通过 @ConfigurationPropertiesScan 或 @EnableConfigurationProperties 注册;单构造器无需 @Autowired。基于代理的注解(@Transactional、@Async、@Cacheable)在同 bean 自调用时无效;默认只对非受检异常和 Error 回滚,受检异常会提交,需用 rollbackFor 指定。应设置 spring.jpa.open-in-view=false,避免懒加载 N+1 查询和整请求占用连接。JPA 实体是带无参构造器的类,record 不能作为实体;基于 id 或业务键实现 equals/hashCode,避免在实体上使用 Lombok @Data。Actuator 暴露 health 和 info,其余端点应置于认证之后。Boot 3+ 使用 jakarta.* 命名空间。
测试:JUnit Jupiter 提供 @Test、@ParameterizedTest(配合 @ValueSource/@CsvSource/@MethodSource)、@TempDir、@Nested、@BeforeEach,测试类和方法可为包私有;AssertJ 的失败信息优于 assertEquals;Mockito 配合 MockitoExtension 使用严格桩,未使用的桩会使测试失败;集成测试可用 Testcontainers 搭配生产数据库引擎,H2 兼容模式在 SQL 方言和锁行为上存在差异;Spring 切片测试加载窄上下文,@SpringBootTest 加载全部上下文,不同上下文配置会各自启动新上下文,是 Spring 测试变慢的常见原因,@MockitoBean(Spring Framework 6.2)取代已弃用的 @MockBean;异步结果用 Awaitility 而非 Thread.sleep;修改构建后应检查报告中的测试数量,防范零测试陷阱。
Loom 测试运行器适配器:Java 使用 gradle 或 maven,loom project detect 会按包打印选择;包含 pom.xml、build.gradle 或 settings.gradle 的目录会被识别为对应类型。