Java 与 JVM 列表全部文章
Java 与 JVM
技术笔记›Java 与 JVM第 6 / 6 篇

Java 核心笔记:String、泛型、异常、反射、日期与 JDK 8+ 常用特性

一份查漏补缺性质的 Java 基础笔记:String 常量池与不可变性、泛型擦除与通配符、异常体系与最佳实践、反射注解的代价、JDK 8 时间 API、equals/hashCode 契约、Stream 与 Optional 的正确用法。

更多

基础不牢,写出来的代码能跑但到处是坑。这篇不是教程,是我自己反复回去查的那部分内容,按主题整理。

一、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); }

三、异常:体系与最佳实践

Throwable Error(JVM 级,不捕获) Exception 受检(必须处理) RuntimeException(非受检,Bug 或不可恢复) Error:OutOfMemoryError / StackOverflowError,catch 了也没用

实践准则(这些都是被线上教训换来的):

  1. 不要 catch 了什么都不做。空 catch 是最差的写法,至少要 log.error + 打上下文(参数、ID)。
  2. 不要 catch Throwable 或 Exception 一把兜,会把 Error 和本该抛出去的业务异常一起吞掉。
  3. 异常信息要带上下文:throw new BizException("清册生成失败, 机构=" + orgId + ", 所属期=" + period),定位时能省半小时。
  4. finally 里不要 return,会覆盖 try 里的返回值,也会吞掉异常。
  5. try-with-resources 代替 finally 关闭,且 JDK 7+ 的 addSuppressed 能保留关闭时的异常。
  6. 不要用异常做流程控制:异常的构造要填栈(fillInStackTrace),成本远高于 if。
  7. 自定义业务异常继承 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+):给接口加默认方法可以平滑升级,但要小心多继承冲突。
— 感谢读到这里 —发现错误,或想补充?告诉我 →
TURN KNOWLEDGE INTO SOMETHING REAL

有想法,也想把它做出来?

从技术方案到上线交付,聊聊你的下一个项目。

聊聊需求

一起聊聊

搜索文章

输入关键词,按标题和摘要即时筛选。

浏览分类文章归档

阅读设置

字号
行宽
专注模式

专注模式会隐藏页头与侧栏,只留正文。设置保存在当前浏览器。

稍后阅读

保存在当前浏览器,留给下一次安静阅读。

总浏览 -- 次 · 总访客 -- 人 · 今日浏览 -- 次 · 今日访客 -- 人