ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java面试进阶:包装类、时间日期与正则表达式全解析

Java面试进阶:包装类、时间日期与正则表达式全解析 1. 进阶路上的三个老熟人为什么面试总爱围着它们打转1.1 为什么这三块内容总是成组出现如果你参与过 Java 技术面试或者自己准备过一轮招聘冲刺应该会发现一个很有意思的现象时间日期处理、包装类、正则表达式这三样东西总是以各种组合出现在面试题里。它们看起来一点关系都没有细想又都指向同一个核心就是基础功。我见过不少人能讲十分钟 Spring Cloud 的原理却在Integer a 100; Integer b 100; a b这个问题上卡住。也有能把 JVM 调优参数背得滚瓜烂熟的候选人让他从一行日志里提取时间戳和耗时时反而绕了很大一圈。这背后反映出一个事实很多人的日常开发被框架包得太严了真正需要手写这些核心 API 的场景不多一旦面试官往底层细节追问很容易露馅。时间日期、包装类、正则表达式恰好都是 Java 语言层面的内容不依赖 Spring、不依赖中间件是每一段业务代码下面都在运行的底层机制。把它们搞明白了你的代码不一定马上变高级但排查问题的速度、写工具类的能力会明显上一个大台阶。1.2 什么样的人最需要这份进阶清单如果你是刚工作一两年的 Java 开发者正在准备跳槽面试这篇文章可以直接当作复习线索。如果你已经在项目里写过一段时间业务代码但总觉得有些 Bug 说不清道不明那更值得读一遍因为这三个主题是无数线上故障的源头。网上搜java 面试题“java 八股文”“20个常用正则表达式”这类热词的人很多说明大家不是不想学是不知道该从哪学起。这篇博文我就是按自己的实践节奏来写的先讲包装类为什么容易出问题再讲时间日期新 API 的正确姿势然后讲正则表达式在不同场景下的正确用法最后用一个完整的日志统计小工具把三者串起来。你会发现进阶不需要背多少冷门知识真正有用的是把这几个高频主题背后的原理弄通。2. 包装类的水远比想象中深从装拆箱到缓存池2.1 基本类型为什么还要“包一层”集合、泛型和nullJava 是一门面向对象语言但 int、double、boolean 这些基本类型并不继承 Object也没有对象方法。集合框架在设计时只接受 Object 类型所以想往 List 里塞一个整数早期必须手动new Integer(100)。后来泛型普及ListInteger成了标准写法编译器的自动装箱让代码看起来像是直接存了 int实际底层仍然会转成 Integer。这就是包装类存在的第一个理由让基本类型能进入面向对象体系。第二个理由更重要就是为了表达 null。基本类型只有默认值int 的默认值是 0boolean 的默认值是 false。可很多业务场景里0 和 null 的含义完全不同。用户没有填写年龄和用户年龄就是 0 岁能是一回事吗数据库字段没值是一种业务状态数值为 0是另一种业务状态。如果直接拿基本类型去接收可空字段很容易产生脏数据。现在主流 ORM 框架的实体类都推荐用包装类映射可空列就是为了保留“信息缺失”这个语义。所以你在设计 DTO、实体类时可空字段用包装类明知非空的字段才用基本类型这是一个很实用的判断标准。2.2 自动装箱拆箱的隐藏成本一个压测才暴露的问题Java 5 引入自动装箱后int 和 Integer 之间的转换看起来消失了。写代码确实爽但开销并没有消失。Integer n 100编译后是Integer.valueOf(100)int m n编译后是n.intValue()。这些调用本身不慢慢的是它们在循环里反复触发时产生的对象创建和回收。我之前优化过一个统计接口里面有一段非常普通的求和逻辑Long sum 0L; for (long i 0; i 10_000_000L; i) { sum i; }这段代码肉眼看上去没有问题。sum i在编译器眼里其实是sum Long.valueOf(sum.longValue() i)。也就是说每循环一次就要拆箱一次、相加一次、再装箱一次产生一个新的 Long 对象。一千万次循环就是一千万个垃圾对象GC 压力直线上升。后来我把 Long 换成基本类型 long同样逻辑的接口耗时直接降了一截。这类问题在单元测试里几乎看不到因为单次调用量太小。但一上压测就非常明显。我每次做 Code Review 时看到循环里有包装类参与运算都会提醒同事先评估数据量。不是包装类不能用而是要清楚它的创建成本。2.3 Integer缓存池同为100一个true一个falseJava 面试里有一个非常经典的代码结果题Integer a 100; Integer b 100; System.out.println(a b); // true Integer c 200; Integer d 200; System.out.println(c d); // false第一次看到这个输出的人多半会愣住为什么都是 Integer一个返回 true 一个返回 false翻一下Integer.valueOf的源码就能明白。Integer 内部维护了一个缓存数组默认缓存了 -128 到 127 之间的所有 Integer 实例。valueOf方法遇到这个范围内的数字时直接返回缓存里的同一个对象超出范围才 new 一个新对象。所以 a 和 b 指向同一个缓存对象比较地址自然为 true。c 和 d 是两个不同对象就只能返回 false。这个缓存范围还可以通过 JVM 参数调整上限但我不建议任何人依赖这个特性来写业务逻辑。因为它要求所有调用方共享同一套 JVM 配置可读性也差读代码的人很难一眼看出你的意图。包装类的比较安全做法永远只有一条用equals或Objects.equals不要用。在包装类上只能比较对象引用你心里期待的是数值比较两者经常对不上。省一行代码埋一颗雷不值得。2.4 包装类比较的安全守则equals还是说到 equals包装类的 equals 还有一个经常被忽略的细节它对类型是敏感的。Integer.valueOf(100).equals(Long.valueOf(100L))会返回 false因为两个对象类型不同。但如果你把两者都拆成基本类型再比100 100L又是 true。这两种结果并存很容易让人在混合数值比较时犯迷糊。我这个年在实际项目里给自己定了三条铁律包装类判等一律用equals或Objects.equals禁止用包装类参与算术运算时先判 null再拆成基本类型混合类型数值比较前统一转成同一个基本类型再比较。举一个常见反例你有 Double price 和 Long count要计算price / count。如果不判空就直接做除法一旦 count 为 null拆箱瞬间就是NullPointerException。这种 Bug 在测试环境很难复现因为测试数据常常是精心构造的但到了生产环境一条异常数据就能把接口打挂。防御性编程在这些地方不是洁癖是底线。2.5 统计计数场景下我为什么不推荐用 Integer 做计数器做日志统计、排行榜、计数器这类功能时很多人会直接写MapString, Integer然后通过 get、put 不停自增。前面已经讲过Integer 是不可变的自增永远是通过创建新对象完成的。频繁自增不仅产生大量临时对象set 新值时还会重复装箱。更要命的是并发问题。两个线程同时读到 map 里的同一个旧值各自加一后写回最终结果只加了一次。这个问题的根源在于get和put不是原子操作。所以我通常建议单个线程统计可以用MapString, int[]数组元素自增不会产生新对象多线程高并发统计用LongAdder或自定义计数器类配合ConcurrentHashMap.compute原子更新如果业务上确实需要包装类那就显式装箱不要依赖隐式转换。在第5节的实战例子里我会用自定义 Counter 的方式演示一个更稳的写法。面试时如果你能主动讲清楚“为什么不直接用 Integer 计数”会给面试官留下很深的印象。3. 时间与日期处理java.time 这套新 API值得你彻底换掉旧日历3.1 旧 API 的坑Date 可变、Calendar 从 0 开始、SimpleDateFormat 线程不安全我刚开始写 Java 时时间处理只能用java.util.Date、java.util.Calendar和java.text.SimpleDateFormat。这三个类设计得有多别扭只有踩过坑的人才有感觉。首先 Date 看起来是日期底层其实就是一个 long 类型的毫秒数。它提供setTime方法意味着对象可以被修改。多线程共享同一个 Date 实例时一个线程改了时间另一个线程读到的就是脏数据。其次 Calendar 的月份从 0 开始编号Calendar.JANUARY的值是 012 月对应 11。新手写月份判断时特别容易差一个月。最坑的是SimpleDateFormat它不是线程安全的。它的format和parse方法内部会修改共享的 Calendar 状态。很多人图方便把它定义成static字段高并发解析日期时轻则解析结果错乱重则直接抛异常。这些问题我都在项目里真实遇到过排查过程极其痛苦。JDK 8 推出java.time包之后时间处理才真正迎来了一个脱胎换骨的新版本。3.2 核心类速览LocalDate、LocalTime、LocalDateTime、Instant、ZonedDateTimejava.time 包把时间拆成了职责非常清晰的几个模型LocalDate只有年月日适合生日、证件日期这种没有时区概念的场景LocalTime只有时分秒和纳秒适合每天的固定时刻LocalDateTime日期加时间但仍然不携带时区信息Instant时间轴上的瞬间点底层是从 1970 年 1 月 1 日 0 时 0 分 0 秒开始计算的秒和纳秒全球统一ZonedDateTimeLocalDateTime加上ZoneId带完整时区规则Duration和PeriodDuration 计算秒和纳秒级差值Period 计算年月周期差值。这一套 API 最大的优点是不可变。每个方法返回的都是新对象原对象不会被修改。你在多线程环境下共享一个LocalDateTime完全不用担心谁把它改坏了。这个特性彻底解决了老 API 的并发隐患。日常代码里最常见的就是获取当前时间、加减日期、比较先后、格式化输出几个操作。写起来非常直观LocalDate today LocalDate.now(); LocalDate birthday LocalDate.of(1995, 5, 20); long age ChronoUnit.YEARS.between(birthday, today); LocalDateTime meeting LocalDateTime.of(2024, 6, 15, 14, 30); LocalDateTime moved meeting.plusMinutes(45);执行完这些代码之后today、birthday、meeting 的值都不会变moved 是新对象。这种设计让代码的意图非常清晰。3.3 时区与时间戳为什么有的系统会出现“8小时偏差”时区问题在分布式系统里几乎一定会遇到。最常见的八小时偏差往往不是某一个环节写错了而是存储、传输、展示三个阶段对时区的理解不一致。比如数据库里存的是 UTC 时间但 JDBC 连接参数配成了东八区系统在存取时把它当本地时间处理一进一出就偏了。再比如后续用LocalDateTime接收一段带时区偏移的 ISO 字符串解析时丢掉偏移信息展示时又按服务器所在时区解释自然对不上。我现在的规范是对外接口统一用带时区或偏移的字符串比如2024-06-15T10:23:4508:00或2024-06-15T10:23:45Z服务内部用Instant或ZonedDateTime表示绝对时间点只有到展示层才转成LocalDateTime并绑定用户所在时区。转换代码也很直白ZonedDateTime zdt ZonedDateTime.of(2024, 6, 15, 10, 23, 45, 0, ZoneId.of(Asia/Shanghai)); Instant instant zdt.toInstant(); ZonedDateTime back instant.atZone(ZoneId.of(UTC));只要不把时区信息弄丢八小时偏差这种问题基本不会再出现。3.4 格式化与解析DateTimeFormatter 的正确姿势新 API 的格式化统一走DateTimeFormatter同样支持yyyy-MM-dd HH:mm:ss这种模式。相比SimpleDateFormat它有两个明显优势第一是不可变可以在项目里放心定义成static final常量第二是自带一组预置格式比如ISO_LOCAL_DATE、ISO_INSTANT生成标准格式非常方便。我一般会在工具类里定义一个公共常量public static final DateTimeFormatter COMMON_FORMAT DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss); // 格式化 String text LocalDateTime.now().format(COMMON_FORMAT); // 解析 LocalDateTime parsed LocalDateTime.parse(2024-06-15 10:23:45, COMMON_FORMAT);这里有两个常见坑。第一解析时字符串格式必须和模式完全匹配否则会抛DateTimeParseException这是一个运行时异常代码里得做好 try-catch 或先做格式校验。第二模式串里的字母大小写敏感yyyy表示年份YYYY表示以周为基准的周年在每年年末和年初时可能相差一年。跨年逻辑用它很容易翻车。项目里统一公共常量能最大程度避免各处手写模式串带来的混乱。3.5 时间计算与周期加减、比较、差值的完整场景时间计算在业务里用得非常频繁比如计算距离某个日期还有多少天、统计用户年龄、给定时任务生成上一个周期的时间窗口。新 API 的处理方式比 Calendar 直观很多LocalDate start LocalDate.of(2024, 1, 1); LocalDate end LocalDate.of(2024, 6, 1); long days ChronoUnit.DAYS.between(start, end); Period period Period.between(start, end); int years period.getYears(); int months period.getMonths(); int daysRemain period.getDays();Duration 处理秒和纳秒级别的时间差Period 处理年月日级别的时间差两者不能混用。比如Duration.between(LocalDate.now(), LocalDate.now().plusDays(1))会直接报错因为 LocalDate 没有秒维度。这时你得先转成LocalDateTime或Instant或者改用ChronoUnit.DAYS.between。这种细节在写代码时很难一次想到踩过一次就会记住。4. 正则表达式掌握 Pattern 和 Matcher 才是真正的进阶4.1 String.matches 的隐蔽开销为什么要在循环外编译很多 Java 新手做字符串格式校验时习惯直接写str.matches(正则)。如果他们只是偶尔校验一次完全没问题。但String.matches的实现里每次都调用Pattern.compile再执行匹配你在一行里看不到的地方隐含着一次完整的状态机构建过程。如果这行代码出现在循环里、接口的批量校验里性能差距会非常明显。更好的写法是把 Pattern 编译一次并复用private static final Pattern MOBILE_PATTERN Pattern.compile(^1[3-9]\\d{9}$); public boolean isMobile(String mobile) { if (mobile null) { return false; } return MOBILE_PATTERN.matcher(mobile).matches(); }这段代码里正则只编译了一次后续每次匹配都复用同一个 Pattern。代码只多了两行但性能可能相差上百倍。写工具类时把这个习惯养成以后写任何高性能接口都受益。4.2 双重转义Java 里写正则的小心机Java 的正则表达式要经过两轮转义。第一轮是 Java 字符串本身的转义第二轮才是正则表达式的语法解析。所以匹配数字\d在 Java 源码里要写\\d匹配一个字面上的点要写\\.。很多从 Python 或 JavaScript 转过来的人很容易在这里栽跟头。我建议第一步先在草稿纸上把正则表达式写清楚比如\d{4}-\d{2}-\d{2}然后转成 Java 字符串时把每个反斜杠再补一个变成\\d{4}-\\d{2}-\\d{2}。如果你用的是 JDK 15 之后的文本块转义负担会轻不少但传统项目里还是以普通字符串为主这个习惯必须养好。注意如果只写单个反斜杠编译器会直接报“非法转义字符”这其实还算好排查。更隐蔽的是你用了类似于String regex \d;但这种写法在 Java 里是编译不过的问题会在非常早期暴露。真正容易出错的是正则里出现多个反斜杠时少写一个编译能过匹配结果却不符合预期这种才难查。4.3 分组与提取从文本中精准抽取目标字段正则提取功能在日志解析里高频出现。举个例子日志里有一段订单描述String input 订单号ORD123#2024; Pattern pattern Pattern.compile(ORD(\\d)#(\\d)); Matcher matcher pattern.matcher(input); if (matcher.find()) { String orderSeq matcher.group(1); // 123 String year matcher.group(2); // 2024 }find方法会在当前字符串中查找满足模式内容的子串matches则要求整个字符串完全匹配二者语义不同。提取长文本中夹杂的字段时应该优先用find做整串格式校验时用matches。Java 从 JDK 7 开始支持命名分组可读性更好Pattern.compile(ORD(?seq\\d)#(?year\\d)); matcher.group(seq);命名分组的优势是代码不再依赖括号顺序。后来你往正则里加分组时不用一个个改代码里的序号维护起来轻松不少。4.4 灾难性回溯一个小正则也能拖垮线上服务正则匹配的性能有时候不一定是线性的。Java 的 Pattern 默认使用回溯型引擎某些嵌套量词组合配上特殊输入会让匹配时间成指数级增长这就是常说的灾难性回溯。一个经典例子是(a)$。如果目标字符串是aaaaaaaaaaaaaaaaaaaaaaaaaaaaX引擎为了找到整体匹配会反复尝试不同的分组归属产生极其庞大的计算量。真实线上代码里写这种危险嵌套的场景不太多但我也见过类似的结构比如有人用(.*)*去匹配很长的文本块遇到特定输入时接口直接卡死。规避策略比较明确能用简单字符类解决的问题就不要嵌套量词处理日志或用户输入前先限制字符串长度对线上要用的正则做压测观察不同输入长度下的匹配耗时我比较推荐写正则时保持“最小匹配单元”的思路能用一个字符类解决就不叠加量词。等你有经验后还可以学习原子组和私有量词这些特性来做更极致的优化。4.5 常用正则速查和真实避坑最后汇总几个实际项目里比较常用的正则用 Java 字符串的形式写出来可以直接抄手机号^1[3-9]\\d{9}$邮箱^[A-Za-z0-9._%-][A-Za-z0-9.-]\\.[A-Za-z]{2,}$邮政编码^[1-9]\\d{5}$IP 地址String ipRegex ^(25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]?\\d)){3}$;这几条覆盖了最常见的格式校验场景。实际用的时候还要注意IP 地址这条表达式比较长不适合在高频路径上反复编译还是老规矩先Pattern.compile存起来。5. 实战联动写一个日志解析统计的小工具把三块知识串起来5.1 需求场景一个贴近真实工作的综合任务学知识最怕的是各学各的。我建议你把包装类、时间日期、正则表达式放在同一个任务里练印象会深很多。这里我设计一个日志分析小工具需求很简单但非常接近真实业务输入日志文件每行格式固定节点: 192.168.1.10 | 请求时间: 2024-06-15 10:23:45 | 状态: 200 | 耗时: 45ms 节点: 192.168.1.11 | 请求时间: 2024-06-15 10:25:12 | 状态: 500 | 耗时: 120ms需要统计出两个维度每个小时的请求数量每个小时的平均耗时毫秒。最后按小时排序输出成可读报告。这个任务正好把正则提取、时间 API 聚合、计数器实现这三件事串在一起。5.2 用正则表达式提取结构化字段先写提取逻辑。日志行里有四类信息节点 IP、请求时间、状态码、耗时。直接用正则一次匹配private static final Pattern LOG_PATTERN Pattern.compile( 节点: ([\\d.]) \\| 请求时间: (\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}) \\| 状态: (\\d{3}) \\| 耗时: (\\d)ms );group(1)是 IPgroup(2)是请求时间字符串group(3)是状态码group(4)是耗时。这里有人会问为什么不用split(\\|)直接拆列因为日志的分隔符和字段内容都可能变化正则对结构描述更精确还能一次性提取出多个子字段扩展性更强。在真实日志系统里字段往往是key: value的形式正则结合命名分组几乎成了标准解法。5.3 用时间 API 做小时维度的聚合拿到时间字符串后先解析成LocalDateTime再用withMinute(0).withSecond(0).withNano(0)把它归整到小时。比如 10:23:45 变成 10:00:00。以这个整点时间为 key就可以按小时分组统计DateTimeFormatter formatter DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss); LocalDateTime time LocalDateTime.parse(matcher.group(2), formatter); LocalDateTime hourStart time.withMinute(0).withSecond(0).withNano(0);这里采用时间对象做 key因为LocalDateTime本身实现了Comparable用TreeMap就能保证小时顺序。比直接把字符串截断到小时再拼接要可靠得多。5.4 用自定义计数器替代包装类避免性能陷阱如果日志量只有几百行MapString, Integer完全够用。但要讲进阶和并发安全我建议写一个自定义统计类把计数和耗时累加封装起来private static class HourStat { private long count; private long totalCostMs; void add(long costMs) { count; totalCostMs costMs; } long averageMs() { return count 0 ? 0 : totalCostMs / count; } }这个类内部用基本类型long累加不涉及装箱自增。如果后续日志量大到需要并发处理把add方法加synchronized或者把count和totalCostMs换成LongAdder就能平滑升级。比在整个业务代码里到处改 Integer 优雅太多了。5.5 完整代码示例与运行结果整个工具类完整代码如下注释写了一些关键点import java.io.BufferedReader; import java.io.StringReader; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.Map; import java.util.TreeMap; import java.util.regex.Matcher; import java.util.regex.Pattern; public class LogHourlyReporter { private static final Pattern LOG_PATTERN Pattern.compile( 节点: ([\\d.]) \\| 请求时间: (\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}) \\| 状态: (\\d{3}) \\| 耗时: (\\d)ms ); private static final DateTimeFormatter TIME_FORMATTER DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss); private static final DateTimeFormatter HOUR_FORMATTER DateTimeFormatter.ofPattern(yyyy-MM-dd HH:00); private static class HourStat { private long count; private long totalCostMs; void add(long costMs) { count; totalCostMs costMs; } long averageMs() { return count 0 ? 0 : totalCostMs / count; } } public static void main(String[] args) { String sampleLog 节点: 192.168.1.10 | 请求时间: 2024-06-15 10:23:45 | 状态: 200 | 耗时: 45ms 节点: 192.168.1.11 | 请求时间: 2024-06-15 10:25:12 | 状态: 500 | 耗时: 120ms 节点: 192.168.1.12 | 请求时间: 2024-06-15 11:02:33 | 状态: 200 | 耗时: 30ms 节点: 192.168.1.13 | 请求时间: 2024-06-15 11:14:08 | 状态: 200 | 耗时: 60ms ; MapLocalDateTime, HourStat stats new TreeMap(); try (BufferedReader reader new BufferedReader(new StringReader(sampleLog))) { String line; while ((line reader.readLine()) ! null) { Matcher matcher LOG_PATTERN.matcher(line); if (!matcher.matches()) { System.out.println(未匹配行 line); continue; } LocalDateTime time LocalDateTime.parse(matcher.group(2), TIME_FORMATTER); LocalDateTime hourStart time.withMinute(0).withSecond(0).withNano(0); long costMs Long.parseLong(matcher.group(4)); HourStat stat stats.computeIfAbsent(hourStart, k - new HourStat()); stat.add(costMs); } } catch (Exception e) { e.printStackTrace(); } for (Map.EntryLocalDateTime, HourStat entry : stats.entrySet()) { HourStat stat entry.getValue(); System.out.printf(%s 请求数%d 平均耗时%dms%n, entry.getKey().format(HOUR_FORMATTER), stat.count, stat.averageMs()); } } }输出结果2024-06-15 10:00 请求数2 平均耗时82ms 2024-06-15 11:00 请求数2 平均耗时45ms把这个流程跑通之后你会发现正则负责提取时间 API 负责规整和格式化计数器负责高效聚合三者配合起来非常顺手。如果以后要处理真实文件把StringReader换成FileReader再考虑下大文件逐行读的方式即可。这个工具我在公司内部也做过类似版本几乎可以直接并到定时分析任务里。6. 个人经验进阶不是背 API而是真正理解设计意图6.1 先懂原理再记 API我见过不少朋友每天刷 API 文档刷完就忘效率很低。这些年在团队里带新人的经验让我总结出一个更有效的顺序第一次接触一个 API 时先问它解决了什么问题和旧方案有什么本质区别再打开源码看实现重点看缓存、不可变性、线程安全这些设计意图最后动手写示例用各种边界输入测试一下。比如你看懂 Integer 缓存之后再去理解 Long 的缓存、Boolean 的两个实例就非常简单。你理解了 java.time 的不可变性之后再去看Period和Duration的区别也顺理成章。原理层面的认知一旦建立API 细节就不再是孤立记忆点而是相互连接的知识网络。6.2 我踩过的三个经典 bug你可以直接跳过这三个 Bug 都是我真实遇到过的每次排查都很痛苦但收益也很大。第一个是包装类自增判空问题。当时做导出功能把一个可能为 null 的 Integer 变量直接参与累加前几百条数据平安无事到某一行数据为 null 突然崩溃。后来统一改成先判空再拆箱或者用自定义计数器再没出现过。第二个是 SimpleDateFormat 线程安全。当时把一个 formatter 定义成 static 字段给多个线程共用日常测试没暴露一上高并发导出就出现分钟数错乱。排查了很久才发现是内部状态被并发修改。从那以后我对“无状态不可变工具”有了执念。第三个是正则灾难性回溯。接口响应偶尔很慢翻了日志发现某些长字符串过正则时要卡几十秒。后来把嵌套量词改成字符类加限定次数又限了输入长度问题立刻解决。这三个 Bug 都属于“原理不懂时很难排查原理懂了之后就非常好防”的典型。6.3 一条可落地的行动清单如果你现在不知道从哪里下手下面三件事可以先做起来。第一打开Integer、Long、LocalDate、Pattern这几个类的源码看一遍不用全懂先看构造方法、缓存、equals、hashCode 这些部分。第二给项目里所有时间处理做一次体检把SimpleDateFormat和Date尽量替换成 java.time 系的 API。第三建一个本地小仓库专门收集你见过的正则表达式和对应测试用例越用越熟。这些主题真正的乐趣在于它们不是孤立的。包装类和正则的匹配结果经常出现在同一个统计逻辑里时间 API 又负责把统计结果落到时间窗口上。等你把它们串起来用过一回再回头看面试题会发现很多问题其实都是在考同一个东西你有没有真的理解 Java 底层在帮你做什么。
返回列表