Skip to content

JavaSE 商业场景训练营

JavaSE 不是语法清单。真正做后端系统时,JavaSE 会出现在每一个细节里:用集合去重和映射,用泛型保证类型安全,用反射支撑框架,用 IO/NIO 处理文件和网络,用线程池控制并发,用 JVM 工具排查 OOM、GC 和线程堆积。

一句话主线:

JavaSE 训练的目标是把“语法点”变成“项目能力”:知道什么时候用、为什么这么设计、底层怎么工作、不理解会出什么生产事故。

训练目标

学完本页,你要能做到:

  1. 用 Java 对医疗数据采集记录做去重、分组、映射和校验。
  2. 解释 equals/hashCodeHashMapArrayList、泛型擦除在项目里的后果。
  3. 写出反射读取注解并做字段脱敏的 Demo,理解框架为什么离不开反射。
  4. 解释大文件导入为什么不能一次性读入内存,BIO、NIO、Buffer、Channel 解决什么问题。
  5. 设计采集任务线程池,知道参数、队列、拒绝策略、下游容量和监控指标。
  6. 讲清 JDK7 和 JDK8 在集合、接口、Lambda、Stream、日期时间、并发上的关键差异。
  7. 用线程栈、GC 日志、heap dump、对象直方图排查 JavaSE 层生产问题。

商业场景一:采集记录去重和映射

医疗数据采集常见需求:同一个患者、同一次就诊、同一份报告不能重复入库。最自然的做法是定义业务 Key,再用 HashSetHashMap 去重。

mermaid
flowchart TD
    A["读取采集记录"] --> B["构造业务 Key"]
    B --> C["放入 HashSet"]
    C --> D{"是否已存在"}
    D -- "存在" --> E["跳过重复记录"]
    D -- "不存在" --> F["写入待入库列表"]
    F --> G["批量入库"]

Demo:

java
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;

public class CollectDeduplicateDemo {

    static class VisitKey {
        private final String patientId;
        private final String visitNo;
        private final String reportNo;

        VisitKey(String patientId, String visitNo, String reportNo) {
            this.patientId = patientId;
            this.visitNo = visitNo;
            this.reportNo = reportNo;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof VisitKey)) return false;
            VisitKey that = (VisitKey) o;
            return Objects.equals(patientId, that.patientId)
                    && Objects.equals(visitNo, that.visitNo)
                    && Objects.equals(reportNo, that.reportNo);
        }

        @Override
        public int hashCode() {
            return Objects.hash(patientId, visitNo, reportNo);
        }
    }

    static class CollectRecord {
        String patientId;
        String visitNo;
        String reportNo;

        VisitKey key() {
            return new VisitKey(patientId, visitNo, reportNo);
        }
    }

    public static int countNewRecords(List<CollectRecord> records) {
        Set<VisitKey> seen = new HashSet<>();
        int newCount = 0;
        for (CollectRecord record : records) {
            if (seen.add(record.key())) {
                newCount++;
            }
        }
        return newCount;
    }
}

为什么必须同时重写 equalshashCode

mermaid
flowchart TD
    A["HashSet.add"] --> B["调用 hashCode 定位桶"]
    B --> C["桶中已有节点"]
    C --> D["调用 equals 判断是否相同业务 Key"]
    D --> E{"equals 是否相等"}
    E -- "相等" --> F["认为重复"]
    E -- "不相等" --> G["加入集合"]

如果只重写 equals 不重写 hashCode,两个业务上相同的 Key 可能落到不同桶里,HashSet 根本不会在同一个桶里比较它们,去重就会失败。

商业场景二:泛型让接口返回更安全

没有泛型时,集合里什么都能放,取出来要强转,错误可能到运行时才暴露。

错误写法:

java
List records = new ArrayList();
records.add("DATASET_001");
records.add(100);

String code = (String) records.get(1); // 运行期 ClassCastException

泛型写法:

java
List<String> assetCodes = new ArrayList<>();
assetCodes.add("DATASET_001");
// assetCodes.add(100); // 编译期直接报错

泛型的核心价值是把类型错误提前到编译期。它不是运行期给每个对象都保存完整类型参数,Java 泛型主要通过类型擦除实现。

mermaid
flowchart TD
    A["源码 List<String>"] --> B["编译器检查只能放 String"]
    B --> C["擦除为原始 List"]
    C --> D["必要位置插入强转"]
    D --> E["运行期执行"]

为什么框架还能知道一些泛型信息?因为字段、方法签名、父类泛型等元信息可能保存在 class 文件的 Signature 属性中,框架可以通过反射读取。但普通对象本身通常不知道 List<String> 里的 String

商业场景三:用反射和注解做字段脱敏

医疗数据里患者姓名、手机号、身份证号都属于敏感信息。可以用注解标记字段,再通过反射统一脱敏。

java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface Sensitive {
}

class PatientView {
    @Sensitive
    private String patientName;

    @Sensitive
    private String idCard;

    private String deptName;

    PatientView(String patientName, String idCard, String deptName) {
        this.patientName = patientName;
        this.idCard = idCard;
        this.deptName = deptName;
    }
}

public class DesensitizeDemo {
    public static void desensitize(Object target) throws IllegalAccessException {
        Class<?> clazz = target.getClass();
        for (Field field : clazz.getDeclaredFields()) {
            if (!field.isAnnotationPresent(Sensitive.class)) {
                continue;
            }
            field.setAccessible(true);
            Object value = field.get(target);
            if (value instanceof String) {
                field.set(target, mask((String) value));
            }
        }
    }

    private static String mask(String value) {
        if (value == null || value.length() <= 1) {
            return "*";
        }
        return value.charAt(0) + "***";
    }
}

反射流程:

mermaid
flowchart TD
    A["拿到对象"] --> B["target.getClass 获取 Class"]
    B --> C["getDeclaredFields 获取字段"]
    C --> D{"字段是否有 @Sensitive"}
    D -- "有" --> E["setAccessible 打开访问"]
    E --> F["读取字段值"]
    F --> G["脱敏后写回"]
    D -- "没有" --> H["跳过"]

反射的风险是运行期才发现方法名、字段名、权限问题,所以框架会缓存元信息,并在启动阶段尽量提前校验。

商业场景四:大文件导入为什么不能一次性读

数据采集平台经常导入 CSV、Excel、日志文件。初学者常写:

java
byte[] bytes = Files.readAllBytes(path);

小文件可以,大文件会把内容一次性放进堆里,容易 OOM。更安全的方式是缓冲分块读取。

java
try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
    String line;
    int batchSize = 0;
    while ((line = reader.readLine()) != null) {
        batchSize++;
        if (batchSize >= 1000) {
            // 批量入库并清空批次
            batchSize = 0;
        }
    }
}

IO 排查流程:

mermaid
flowchart TD
    A["文件导入慢或 OOM"] --> B{"是否一次性读入内存"}
    B -- "是" --> C["改成流式读取和分批处理"]
    B -- "否" --> D{"是否卡在磁盘或网络 IO"}
    D -- "是" --> E["看线程栈、磁盘、网络、下游接口"]
    D -- "否" --> F["检查解析逻辑、批量入库和锁竞争"]

BIO 是一个线程阻塞等待一个连接或文件操作,模型简单但线程容易被阻塞。NIO 用 Buffer、Channel、Selector 支持多路复用,更适合大量连接场景;但普通文件批处理不一定非要 NIO,先把流式读取、编码、缓冲和批量处理做好更重要。

商业场景五:采集任务线程池设计

采集任务通常要调用外部接口、解析数据、写数据库、发 MQ。不能无限 new Thread,否则线程数失控、数据库连接被打满、下游接口被压垮。

mermaid
flowchart TD
    A["提交采集任务"] --> B{"核心线程是否未满"}
    B -- "是" --> C["创建核心线程执行"]
    B -- "否" --> D{"队列是否未满"}
    D -- "是" --> E["任务进入有界队列"]
    D -- "否" --> F{"最大线程是否未满"}
    F -- "是" --> G["创建非核心线程"]
    F -- "否" --> H["执行拒绝策略"]

Demo:

java
ThreadPoolExecutor executor = new ThreadPoolExecutor(
        8,
        16,
        60,
        TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(1000),
        runnable -> {
            Thread thread = new Thread(runnable);
            thread.setName("collect-worker-" + thread.getId());
            return thread;
        },
        new ThreadPoolExecutor.CallerRunsPolicy()
);

参数不是套公式,要结合下游容量:

参数怎么想
核心线程数平时稳定处理能力
最大线程数突发时允许的上限
队列容量可接受的等待量,必须有界
拒绝策略系统过载时如何保护自己
线程名方便 jstack 和日志排查

如果数据库连接池只有 20,线程池开 200 并不会让系统更快,只会让大量线程等待数据库连接,甚至拖垮数据库。

商业场景六:JDK7 和 JDK8 怎么影响写法

企业项目里 JDK7、JDK8 仍然常见。学习 JavaSE 不能只看 JDK21。

能力JDK7 写法JDK8 变化
资源关闭try-finally 或 try-with-resources延续 try-with-resources
集合遍历for 循环、增强 forLambda、Stream
接口只能抽象方法和常量default/static 方法
日期时间DateCalendarjava.time
异步编排FutureCompletableFuture
HashMap数组 + 链表数组 + 链表 + 红黑树
ConcurrentHashMapSegment 分段锁CAS + synchronized 桶锁

JDK8 Stream 示例:

java
Map<String, Long> countByDept = records.stream()
        .collect(Collectors.groupingBy(
                CollectRecord::getDeptCode,
                Collectors.counting()
        ));

Stream 不一定比 for 快,它的价值是表达数据处理流水线。复杂业务、需要提前退出、需要清晰异常处理时,普通 for 循环可能更易读。

生产排查闭环

mermaid
flowchart TD
    A["Java 服务异常"] --> B{"表现是什么"}
    B -- "OOM" --> C["看 GC 日志、dump、对象直方图"]
    B -- "CPU 高" --> D["top 定位线程,jstack 看热点"]
    B -- "接口慢" --> E["拆线程池、DB、HTTP、锁和 GC"]
    B -- "数据错" --> F["查集合去重、并发写、equals/hashCode"]
    B -- "任务堆积" --> G["看线程池 active、queue、拒绝、下游耗时"]
    C --> H["定位根因并补监控"]
    D --> H
    E --> H
    F --> H
    G --> H

常见根因:

现象JavaSE 根因
去重失败equals/hashCode 写错,Key 可变
堆 OOM大集合无限增长、大文件一次性读入、缓存不清理
线程数暴涨无限创建线程、线程池最大线程过大
CPU 高死循环、频繁 GC、锁竞争、自旋
请求串用户ThreadLocal 在线程池中未清理
反射报错字段名变更、访问权限、模块限制

面试标准回答

JavaSE 为什么是后端地基?

JavaSE 提供了类型系统、面向对象、集合、泛型、异常、反射、IO/NIO、并发和 JVM 能力。Spring、MyBatis、Netty、MQ 客户端、数据库驱动都建立在这些能力上。学 JavaSE 不能只会语法,还要知道集合为什么这么设计、线程池为什么不能乱配、反射为什么支撑框架、OOM 和线程堆积怎么排查。

JDK7 和 JDK8 最重要的差异是什么?

JDK8 引入 Lambda、Stream、Optional、接口默认方法、java.time、CompletableFuture 等现代写法;集合和并发实现也有重要变化,比如 HashMap 冲突严重时会树化,ConcurrentHashMap 从 JDK7 的 Segment 分段锁演进为 JDK8 的 CAS + 桶级 synchronized。企业项目中 JDK7/8 都要懂,因为很多老系统仍运行在 JDK8,面试也常问它们的区别。

JavaSE 线上问题怎么排查?

先按表现分类:OOM 看 GC 日志、heap dump、对象直方图和引用链;CPU 高看线程 CPU 和 jstack;接口慢拆线程池、数据库、HTTP、锁和 GC;数据错看集合、并发写、equals/hashCode;任务堆积看线程池 active、queue、拒绝次数和下游耗时。不要只看业务异常,要结合 JVM、线程和依赖指标。

关联知识点

知识点继续学习
JavaSE 主线JavaSE 从零到生产级掌握
Java 基础Java 基础
集合框架集合框架
HashMapHashMap 全过程原理
泛型泛型全过程原理
反射反射全过程原理
IO/NIOIO 与 NIO
线程池线程池
JVM 排查JVM 排查工具
面试JavaSE 面试知识点