基础不牢,写出来的代码能跑但到处是坑。这篇不是教程,是我自己反复回去查的那部分内容,按主题整理。
一、String:为什么不可变,以及拼接的代价
String 用 final char[](JDK 9 后是 byte[])存值,类也是 final。不可变带来三件事:线程安全、可缓存 hash、字符串常量池可行。
String a = "abc"; // 常量池
String b = new String("abc"); // 堆上新对象,一定不等
String c = "ab" + "c"; // 编译期优化,等同 "abc"
a == c; // true
a == b; // false
a.equals(b); // true ← 比较内容永远用它
循环里用 += 拼字符串是性能事故:每次都会 new 一个 StringBuilder 再 toString。正确做法:
StringBuilder sb = new StringBuilder(1024); // 能预估就给容量
for (String s : list) sb.append(s).append(',');
| 类 | 线程安全 | 场景 |
|---|---|---|
StringBuilder |
否 | 默认选它,单线程快 |
StringBuffer |
是(synchronized) | 几乎不需要,多线程用 join 或局部变量 |
二、泛型:擦除是万恶之源
Java 泛型是编译期语法糖,运行时类型被擦除(List<String> 和 List<Integer> 运行期都是 List)。由此产生的限制:
- 不能
new T(),不能new T[],不能instanceof List<String>。 - 不能创建泛型数组:
new List<String>[10]编译不过。 - 基本类型不能做泛型参数,用
Integer/Long(这也是为什么有IntStream这类特化流)。 - 重载冲突:
void f(List<String>)和void f(List<Integer>)不能共存(擦除后签名相同)。
PECS 原则(Producer Extends, Consumer Super):
// 只从集合里读(生产者)→ ? extends
void read(List<? extends Number> list) { Number n = list.get(0); }
// 只往集合里写(消费者)→ ? super
void write(List<? super Integer> list) { list.add(1); }
三、异常:体系与最佳实践
实践准则(这些都是被线上教训换来的):
- 不要 catch 了什么都不做。空 catch 是最差的写法,至少要
log.error+ 打上下文(参数、ID)。 - 不要 catch
Throwable或Exception一把兜,会把Error和本该抛出去的业务异常一起吞掉。 - 异常信息要带上下文:
throw new BizException("清册生成失败, 机构=" + orgId + ", 所属期=" + period),定位时能省半小时。 finally里不要return,会覆盖try里的返回值,也会吞掉异常。- try-with-resources 代替 finally 关闭,且 JDK 7+ 的
addSuppressed能保留关闭时的异常。 - 不要用异常做流程控制:异常的构造要填栈(
fillInStackTrace),成本远高于if。 - 自定义业务异常继承
RuntimeException,用全局@ControllerAdvice统一转成 Result,别让受检异常污染方法签名。
四、equals 与 hashCode:必须成对重写
契约:equals 相等的两个对象,hashCode 必须相等;反过来不成立(哈希碰撞允许)。只重写 equals 不重写 hashCode,放进 HashMap/HashSet 就会「存得进去取不出来」。
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Order order = (Order) o;
return Objects.equals(orderNo, order.orderNo); // 用业务唯一键,别用所有字段
}
@Override
public int hashCode() { return Objects.hash(orderNo); }
JDK 16+ 可以直接用 record,编译器自动生成 equals / hashCode / toString,且天然不可变,做 DTO 和 Map 的 key 都很合适。
五、反射与注解:好用但别滥用
反射让框架能做很多事(Spring 的 IOC、MyBatis 的映射全靠它),但代价明确:
| 代价 | 说明 |
|---|---|
| 性能 | 方法调用慢 3–10 倍。热点路径要缓存 Method 对象或 setAccessible(true) |
| 安全 | setAccessible(true) 可以绕过 private,JDK 17 起强封装模块会告警 |
| 可维护性 | 编译期检查失效,重命名方法不会报错,运行时才炸 |
注解的生命周期三选一:SOURCE(编译后丢弃,如 @Override)、CLASS(默认,字节码里有但运行时读不到)、RUNTIME(运行时可通过反射读取,Spring 的注解都是这个)。写自定义注解做切面时,一定要显式写 @Retention(RetentionPolicy.RUNTIME),否则运行时根本读不到。
六、日期时间:只用 java.time
Date 和 SimpleDateFormat 已废弃,理由很硬:SimpleDateFormat 不是线程安全的,static 共享会导致日期错乱甚至抛异常。
// JDK 8+
LocalDate date = LocalDate.now();
LocalDateTime dt = LocalDateTime.now();
String s = dt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
LocalDateTime parsed = LocalDateTime.parse("2026-09-01 12:00:00",
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
// 时区:存 UTC,显示时再转
ZonedDateTime shanghai = dt.atZone(ZoneId.of("Asia/Shanghai"));
// 计算
LocalDate next = date.plusDays(15);
long days = ChronoUnit.DAYS.between(date, next);
DateTimeFormatter 是不可变且线程安全的,可以放心定义成 static final。
一个真实教训:统计口径里「所属期 2026-08」用的是
LocalDate但存库时用了Date,跨时区部署后差了 8 小时,导致月末那天的申报数据被算到上个月。规则:库里存datetime或时间戳,业务语义上明确时区,不要混用。
七、Stream 与 Optional:写对才优雅
// 分组(最常用)
Map<Long, List<Order>> byOrg = orders.stream()
.collect(Collectors.groupingBy(Order::getOrgId));
// 转 Map,注意 key 冲突
Map<String, Order> byNo = orders.stream()
.collect(Collectors.toMap(Order::getOrderNo, o -> o,
(a, b) -> a)); // ★ 不给 merge 函数,key 重复直接抛异常
// 大集合用并行流要谨慎:公共 ForkJoinPool,会互相影响
Stream 的注意点:
toMap一定要给 merge 函数,否则 key 重复直接IllegalStateException。- 不要在流里修改外部集合(副作用),并行流下会数据错乱。
- 小集合(<1000)不要用
parallelStream,线程开销大于收益,且共用ForkJoinPool.commonPool。 - Optional 只用于返回值,不要做方法参数、不要做字段(不可序列化)。
- 别写
Optional.of(x)然后isPresent()判断 —— 那还不如直接判 null。Optional的价值在map/orElseGet/ifPresent的链式写法。
八、零碎但常考
Integer缓存 -128~127:Integer a = 127, b = 127; a == b为 true,128 就 false。比较包装类型一律用equals。BigDecimal比较用compareTo不用equals:equals连精度都比对(1.0≠1.00)。金额构造用new BigDecimal("0.1")字符串,不要用double。switch支持 String 和枚举,JDK 17 支持模式匹配。final的三重语义:修饰类(不可继承)、方法(不可重写)、变量(引用不可变,注意是引用不可变,对象内容可以改)。- 接口默认方法与静态方法(JDK 8+):给接口加默认方法可以平滑升级,但要小心多继承冲突。
一起聊聊