很多 Java 开发者写了三五年代码,用的依然是 JDK 8 之前的思维方式。语法都会,但一遇到泛型擦除、Stream 只能消费一次、volatile 到底保不保证原子性…… 就卡住了。本文不谈基础语法,只讲那些“你以为你懂,实际一用就踩坑”的核心进阶特性。掌握了这些,才算真正吃透 Java。

一、泛型:不只是 那么简单
泛型擦除到底是个什么坑?多数人只会在类或方法上写个 ,却不知道编译一过,泛型信息就没了——运行时反射根本拿不到具体的类型参数。这就是第一个陷阱。
陷阱1:运行时无法获取泛型类型
List list = new ArrayList<>();
// ❌ 运行时 list.getClass() 返回 ArrayList,不是 List
// 泛型信息在编译后被擦除,反射拿不到
陷阱2:泛型不能创建实例
想直接用 new T()?编译直接报错。正确姿势是传一个 Class 对象进来,通过反射去构造。
// ❌ 编译报错
T obj = new T();
// ✅ 正确做法:传 Class 对象
public T createInstance(Class clazz) throws Exception {
return clazz.getDeclaredConstructor().newInstance();
}
陷阱3:通配符 ? extends vs ? super
这是 PECS 原则的核心——Producer Extends, Consumer Super。一句话总结:只读用 extends,只写用 super。记不住?看个例子就明白了:
| 场景 | 用什么 | 记忆口诀 |
|---|---|---|
| 只读取(消费) | ? extends T | Producer Extends |
| 只写入(生产) | ? super T | Consumer Super |
// 只能读,不能加元素
List extends Number> nums = new ArrayList();
nums.add(1); // ❌ 编译报错
// 只能加,读出来是 Object
List super Integer> ints = new ArrayList();
Object o = ints.get(0); // ✅ 只能当 Object 用
二、Stream API:别只会 filter().collect()
Stream 真正的威力在于流水线处理和短路优化。你想想,一行代码搞定按部门分组、求每个部门的最高薪资——传统写法得循环多少遍?
// 按部门分组,求每个部门最高薪资
Map> result = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDept,
Collectors.maxBy(Comparator.comparing(Employee::getSalary))
));
但别高兴太早,Stream 有个巨坑:只能消费一次。你数完元素还想再遍历?报错:stream has already been operated upon or closed。
Stream stream = list.stream();
stream.count(); // 第一次消费
stream.forEach(System.out::println); // ❌ 报错:stream 已关闭
// ✅ 重新创建,或用 collect 缓存
List cached = stream.collect(Collectors.toList());
还有个性能陷阱:不要以为 forEach 就一定比普通 for 循环快。小数据量下 Stream 的装箱拆箱开销可能更大。Stream 的优势体现在大数据量加并行流上:
list.parallelStream() // 自动多核并行
.filter(...)
.map(...)
.collect(...);
但并行流不是银弹——线程切换有成本,I/O 密集型任务才值得并行,CPU 密集型反而可能更慢。
三、Lambda 与函数式接口:不只是简化代码
Lambda 的本质是把行为当参数传递,这是全栈开发中事件驱动、回调处理的基础。但很多人只会在 forEach 里写个 item -> {},却不知道如何自定义一个函数式接口——尤其是要处理受检异常的场景。
@FunctionalInterface
public interface Try {
R apply(T t) throws Exception;
}
// 使用:带异常的 Lambda
Try parse = s -> Integer.parseInt(s);
try {
Integer result = parse.apply("123");
} catch (Exception e) {
// 统一处理
}
有了这个模式,你再也不用在每个 Lambda 里写 try-catch 了。
四、并发基础:synchronized 之外你还得知道这些
并发编程绝不只有 synchronized 一把锁。
1. volatile 不是锁,是可见性保证
它确保一个线程修改了变量,其他线程立刻可见。但注意,像 flag++ 这种复合操作,volatile 保不住原子性。
volatile boolean flag = false;
// 写线程修改 flag,读线程立刻可见
// 但 flag++ 这种复合操作,volatile 保不住
2. ConcurrentHashMap 为什么比 Hashtable 强?
| 对比项 | Hashtable | ConcurrentHashMap |
|---|---|---|
| 锁粒度 | 整张表一把锁 | JDK8 后分段锁(CAS + synchronized) |
| 允许 null | ❌ | ❌ key 和 value 都不允许 null |
| 性能 | 高并发下几乎不可用 | 高并发场景首选 |
3. 线程池别手动 new Thread
每来一个任务就 new Thread?OOM 在向你招手。正确的做法是用 ThreadPoolExecutor,核心参数一定要背下来:
// ❌ 每来一个任务新建线程,OOM 预定
ExecutorService pool = Executors.newFixedThreadPool(10);
// ✅ 核心参数背下来
ThreadPoolExecutor pool = new ThreadPoolExecutor(
5, // corePoolSize
20, // maximumPoolSize
60L, TimeUnit.SECONDS, // keepAliveTime
new LinkedBlockingQueue<>(100), // 队列
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
五、异常处理:别再 catch (Exception e) 一把梭
最大的坏习惯就是捕获异常后什么都不做,或者直接 e.printStackTrace()。正确做法:精确捕获 + 早抛出 + 别吞异常。
// ❌ 最差写法:吞掉异常,问题排查无从下手
try {
doSomething();
} catch (Exception e) {
// 什么都不做
}
// ✅ 正确写法:要么处理,要么包装后抛出
try {
doSomething();
} catch (IOException e) {
throw new BusinessException("文件读取失败: " + e.getMessage(), e);
}
在全栈开发中,异常层层上抛到统一处理层,是后端架构的基本功。别让异常烂在某个角落里。
写在最后
Java 的核心语法不复杂,复杂的是那些“看着简单、用着踩坑”的细节。泛型擦除、Stream 消费、volatile 可见性、线程池参数——这些才是区分“会写 Java”和“懂 Java”的分水岭。能把这几块吃透,你的代码质量会上一个台阶。
