资讯详情

Java提取字符串日期年份:从substring到LocalDate的完整方案

📅 2026/10/16 15:11:17 | 华诺云谱 👁 阅读
Java提取字符串日期年份:从substring到LocalDate的完整方案
上周做报表需求时碰到一个很不起眼的问题数据源从老系统导出来日期字段全是String类型规范统一是2004-07-11这种写法。统计口径要按年份分组所以第一步就变成了一道经典 Java 小题——从 String 类型的2004-07-11提取年份2004并且以Integer返回。需求听起来很简单真落到代码上才发现实现方式至少有四五种每种背后对应不同的格式假设、异常策略和性能取舍。这篇文章就当一次完整复盘把从最朴素的substring到规范的java.time解析方案都梳理一遍也会聊几个实际踩过的坑。1. 拿到“2004-07-11”这种字符串先别急着写 substring1.1 这种字符串是怎么来的很多刚接触的同学会以为日期就是Date或LocalDate类型现实里大量系统为了兼容性把日期直接存成String。常见的来源有三个老系统的数据库字段是varchar存的就是2004-07-11换库换表的时候没人敢动它。外部接口返回的 JSON 里没有日期类型前端拿到的就是字符串。Excel、CSV 文件导入时单元格格式本身就是文本程序读出来必然是String。在这些场景里你没法要求上游“先转好类型再给我”只能自己处理字符串。也正是因为字符串的不可控性提取年份这件事才需要认真想清楚。1.2 动手前先问自己三个问题接到需求第一反应是substring(0, 4)没问题但我会先确认三个问题避免白写输入格式是不是永远是yyyy-MM-dd有没有可能出现null、空字符串、前后空格年份提取失败时是返回null还是抛异常这三个问题直接决定你选哪种写法。如果业务方一口咬定“格式绝对固定”substring最省事如果数据来自不可控的上游那么规范的LocalDate.parse或者一个封装好的工具方法会稳妥得多。很多线上问题都出在“我以为格式固定”上所以开头多问一句比后续写一堆修补代码值。2. 最直接的 substring 方案三行代码但前提是格式必须听话2.1 代码确实简单先看最直白的写法String dateStr 2004-07-11; String yearStr dateStr.substring(0, 4); // 2004 int yearInt Integer.parseInt(yearStr); // 2004 Integer year Integer.valueOf(yearInt); // Integer 对象也可以合并成一行Integer year Integer.valueOf(dateStr.substring(0, 4));为什么能这么写因为2004-07-11这个格式里年份永远占据前 4 个字符而且这 4 个字符都是数字。这是最直接的字符串剪切思路不需要引入任何额外对象代码量最少执行也快。2.2 格式一变substring 就崩给你看substring最大的问题是它根本不知道自己在处理日期它只是“切字符串”所以数据的真实结构一旦发生变化结果就不可控了。我给你列几个真实可能出现的变体输入内容substring(0, 4) 结果后续 parseInt 表现2004-07-112004正常得到 20042004/07/112004碰巧正常但语义上你已经不再校验格式了2004年07月11日2004年抛出NumberFormatExceptionborn:2004-07-11born抛出NumberFormatException2004-7-112004碰巧正常但后续日期部分格式已经被忽略2004-13-992004碰巧正常但明显是非法日期这表格里的后几种情况substring都能“成功”提取出年份但整个日期可能是错的。如果你只是做粗粒度统计要的可能就是这四位数字如果后面还有其他同学接手这段逻辑他看到2004-13-99居然也能返回 2004大概率会以为业务上允许这种脏数据。这个隐形风险比异常本身更麻烦。2.3 Integer.parseInt 和 Integer.valueOf 的区别在这个例子里有个小细节值得单独说一句Integer.parseInt(String)返回的是基础类型int而Integer.valueOf(String)返回的是包装类型Integer。题目要求返回Integer所以直接写Integer.valueOf(dateStr.substring(0, 4))是语法上最贴近需求的。但注意Integer.valueOf(String)内部也会做一次解析如果传入的不是纯数字字符串它一样会抛NumberFormatException。它只负责“字符串到 Integer 的转换”不负责“这个字符串是不是合法年份”。测试的时候可以自己写两行 main 验证一下System.out.println(Integer.valueOf(2004)); // 2004 System.out.println(Integer.valueOf(2004年)); // NumberFormatException第二个输出会直接抛异常这就是把字符串处理和日期语义混在一起的最大代价。3. LocalDate.parse getYear规范做法但也要记住格式限制3.1 为什么我更推荐 java.timeJava 8 引入java.time包之后日期解析这件事就应当优先用它。LocalDate.parse默认使用ISO_LOCAL_DATE格式也就是yyyy-MM-dd正好匹配2004-07-11LocalDate date LocalDate.parse(2004-07-11); int year date.getYear(); // 2004 Integer yearBoxed Integer.valueOf(year);这段代码最大的价值在于它不只完成了“截取前四位”而是真正把整个字符串当作日期去解析。2004-13-11这种非法月份会被直接拦截2005-02-29这种不存在的日期也不会放行。因为底层是完整的日期解析读代码的人一眼就能明白先把字符串转成日期对象再从日期对象里取年份。语义清晰不容易误导后来维护的人。类似的LocalDateTime.parse处理2004-07-11T00:00:00这种带时间的字符串也很方便取年份的逻辑一样。3.2 当“2004-7-11”出现时默认解析会失败LocalDate.parse默认要求月和日都是两位数。拿2004-7-11或2004-07-1去解析会直接抛DateTimeParseException因为默认格式里月份是两位、日期也是两位。这在某些手工录入的场景很常见比如用户填表时写成了2004-7-1。遇到这种需求可以自己定义一个宽松一点的DateTimeFormatterDateTimeFormatter formatter DateTimeFormatter.ofPattern(uuuu-M-d, Locale.ROOT); LocalDate date LocalDate.parse(2004-7-11, formatter); Integer year Integer.valueOf(date.getYear());注意我用的是uuuu。uuuu和yyyy在解析公元年份时基本没有差别但从语言规范角度uuuu是“proleptic year”更贴近底层日期计算的语义社区里越来越多人推荐在日期解析时用uuuu而不是yyyy。新手可以直接记住解析日期写uuuu更保险。3.3 带时间、带格式后缀怎么办实际数据里还经常出现2004-07-11 00:00:00这种带空格带时间的写法。此时LocalDate.parse直接解析会失败有两个方向// 方向一按 LocalDateTime 解析再取日期 LocalDateTime dateTime LocalDateTime.parse(2004-07-11T00:00:00); Integer year Integer.valueOf(dateTime.getYear()); // 方向二自定义格式只取日期部分 DateTimeFormatter formatter DateTimeFormatter.ofPattern(uuuu-MM-dd HH:mm:ss, Locale.ROOT); LocalDateTime dateTime2 LocalDateTime.parse(2004-07-11 00:00:00, formatter); Integer year2 Integer.valueOf(dateTime2.getYear());很多人在这里会纠结“多一个 T 少一个空格”的问题。判断标准很简单先看上游文档确认到底有没有时间部分、分隔符是T还是空格。如果确实存在多种变体可以准备两三个DateTimeFormatter依次尝试而不是写一段脆弱的字符串剪切。4. 其它“能跑”的方案正则、SimpleDateFormat、split4.1 split(-)[0]取巧但同样依赖分隔符还有一个常见的写法是String dateStr 2004-07-11; Integer year Integer.valueOf(dateStr.split(-)[0]);这种方案比substring(0, 4)更进了一步不假设年份固定在前四位而是假设第一段是年份。对于2004-07-11它能正确返回对于2004/07/11它会得到一整段2004/07/11然后解析失败。它的问题在于split是按分隔符切分不是按日期语义切分。如果字符串里年份前有前缀比如born-2004-07-11它取到的就是born。所以这种写法适合你已经确定数据“就是三段以-分隔”且不会变化的场景本质上是substring的变种。4.2 正则表达式校验和提取一起做正则适合“既要判断格式对不对又要提取年份”的场景。比如private static final Pattern YEAR_PATTERN Pattern.compile(^(\\d{4})-(\\d{2})-(\\d{2})$); public static Integer extractYear(String dateStr) { Matcher matcher YEAR_PATTERN.matcher(dateStr); if (matcher.matches()) { return Integer.valueOf(matcher.group(1)); } return null; }这段代码有两个好处第一它要求整个字符串完全匹配yyyy-MM-dd格式不对就返回null不会把2004-13-99当合法数据处理第二捕获组直接提取第一段年份不存在越界问题。缺点是代码量偏大而且正则只校验了结构并没有校验日期的真实合法性比如2004-02-30用上面这个正则也能匹配成功。如果你既想要正则的“格式校验”又想要日期的“业务校验”就必须在正则匹配通过后再调用LocalDate.parse。正则更适合做“预检”不适合做最终裁决。4.3 SimpleDateFormat Calendar老代码里经常见坑也不少在一些老项目里会看到这种做法SimpleDateFormat sdf new SimpleDateFormat(yyyy-MM-dd); Date date sdf.parse(2004-07-11); Calendar calendar Calendar.getInstance(); calendar.setTime(date); Integer year calendar.get(Calendar.YEAR);它的核心思路是先解析成Date再用Calendar取出年份。这个方案能跑但我不推荐在新代码里用。原因有三个SimpleDateFormat不是线程安全的把它定义成static后多线程并发解析会出现诡异结果比如年份错乱、时间漂移。SimpleDateFormat默认是宽松解析模式2004-13-11这种数据可能会被自动“进位”解析成2005-01-11年份直接从 2004 变成 2005。代码里塞进一个Calendar对象结构上明显比LocalDate.getYear()笨重。唯一建议使用它的场景是项目还在用 Java 7 或更早版本没法引入java.time又不想额外加依赖。否则一律优先java.time。4.4 方案横向对比方案代码量能否校验日期合法性线程安全推荐度substringparseInt少不能安全无共享状态格式绝对固定时可用split(-)[0]少不能安全不如 substring 直观正则表达式中只能校验结构安全Pattern 可复用适合做预检SimpleDateFormat中宽松模式有坑不安全老项目维护再用LocalDate.parsegetYear中能安全默认推荐这里说的“线程安全”在substring和split方案里其实是指代码本身不依赖共享可变对象而在SimpleDateFormat方案里如果你把它定义为静态字段风险就会冒出来。DateTimeFormatter相反它是线程安全的定义成static final复用完全没问题。5. 封装一个可以直接抄的工具方法5.1 常规工具方法长这样项目里如果多处都要提取年份不要每次到处写substring或者LocalDate.parse封装一个工具方法是最好的选择。我提供一个可以直接抄的版本import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.util.Locale; public final class ExtractYearUtil { private static final DateTimeFormatter FLEXIBLE_FORMATTER DateTimeFormatter.ofPattern(uuuu-M-d, Locale.ROOT); private ExtractYearUtil() { } public static Integer getYear(String dateStr) { if (dateStr null) { return null; } String text dateStr.trim(); if (text.isEmpty()) { return null; } LocalDate date; try { date LocalDate.parse(text); } catch (DateTimeParseException e) { try { date LocalDate.parse(text, FLEXIBLE_FORMATTER); } catch (DateTimeParseException ex) { return null; } } return Integer.valueOf(date.getYear()); } }逻辑并不复杂先处理空值和前后空格再尝试标准格式2004-07-11失败后尝试2004-7-1这类宽松格式都失败就返回null。两个DateTimeFormatter都没有共享可变状态工具类整体线程安全。5.2 异常策略怎么定上面工具方法选择返回null这是我在统计类需求里的习惯上游有一两条脏数据不应该让整个批量任务崩溃。但如果你的业务要求“解析失败就是接口调用失败必须让调用方感知”那返回null就不合适应该改抛IllegalArgumentException把原始字符串和异常信息一起带出去throw new IllegalArgumentException(无法解析的日期字符串: dateStr, e);没有绝对正确的策略只有适合当前业务的策略。封装时最好把这个决策暴露给调用方比如再加一个getYearOrThrow方法。5.3 简单验证用例不引入测试框架直接写main也能验证public static void main(String[] args) { System.out.println(ExtractYearUtil.getYear(2004-07-11)); // 2004 System.out.println(ExtractYearUtil.getYear(2004-7-11)); // 2004 System.out.println(ExtractYearUtil.getYear( 2004-07-11 )); // 2004 System.out.println(ExtractYearUtil.getYear()); // null System.out.println(ExtractYearUtil.getYear(null)); // null System.out.println(ExtractYearUtil.getYear(2004-13-11)); // null System.out.println(ExtractYearUtil.getYear(2005-02-29)); // null }最后一个用例是关键2005-02-29不是合法日期因为 2005 不是闰年。工具方法返回null而不是 2005这正是java.time方案相对substring方案的核心优势。5.4 为什么我看到有人用 Integer 而不是 int题目明确要求返回Integer但很多项目代码里根本不需要包装类型。这里有个容易混淆的点int year date.getYear()返回的是基本类型Integer.valueOf(date.getYear())返回的是包装类型。如果你的业务逻辑里没有集合泛型、没有数据库映射等强制要求用int更轻量只有当你要把年份放进ListInteger、MapString, Integer或者框架要求返回值非空时才需要Integer。还有一点要提醒如果getYear()设计成在解析失败时返回null那返回类型必须是Integer因为int装不下null。这两者不是一个“心情好选哪个都行”的问题而是由空值策略决定的。6. 实际使用中的隐藏坑前导零、性能、时区、维护6.1 年份可能不是四位数我遇到过从旧系统导出的数据年份只有两位比如04-07-11。用substring(0, 4)会取到04-0这种怪东西直接炸用LocalDate.parse默认格式也会因为年份不足四位而出问题。这类数据要先确认业务口径两位年份04到底代表 2004 还是 1904如果你能确认它是 2004可以自行补零String dateStr 04-07-11; String normalized 20 dateStr; // 简单拼接 LocalDate date LocalDate.parse(normalized); Integer year Integer.valueOf(date.getYear()); // 2004补零逻辑看起来粗暴但至少它把“数据不规范”这件事暴露在代码里了。等将来数据源修复了这里可以整体删掉不会留下模糊不清的年份偏移。6.2 解析性能不是所有场景都应该上 LocalDate.parseLocalDate.parse看起来很美但它内部要做完整的格式化校验和字段解析在高频循环里确实比单纯的substring parseInt慢。我自己本地跑过简单对比两者差距能到数倍甚至更多但这个结论要看环境不要拿它当金科玉律。我的取舍标准很简单数据量小、格式乱、希望校验严格用LocalDate.parse或工具方法。数据量极大、格式绝对固定、且已经确认上游稳定用substring(0, 4) Integer.parseInt。中等规模且上游有变数写工具方法先substring快速路径再兜底LocalDate.parse把你的性能追求和健壮性要求组合起来。多数业务场景里真正的瓶颈在网络和数据库解析省出来的那点时间可以忽略不计。只有在纯内存批量处理海量数据时性能优化才有意义。6.3 时区问题字符串本身和“年份”的歧义有人觉得字符串2004-07-11已经固定了时区有什么关系关系在于这个字符串是怎么生成的。如果字符串来自一个 UTC 时间戳比如2004-07-11T16:00:00Z在某个东八区系统里显示已经是2004-07-12这时按字符串提取年份拿到的是2004但如果你在代码里把它转成Instant再按其他时区取年份就可能变成2005或2004取决于时区转换规则。所以处理带时间部分的日期字符串时必须明确这个字符串是哪个时区的本地时间如果上游文档说“所有时间都是服务器本地时间”那解析时用ZoneId.systemDefault()即可如果上游说“这是 UTC 时间”你就得先按OffsetDateTime.parse解析再转换到目标时区再取年份。最怕的是不明确时区约定代码里随便写一个ZoneId最终年份可能和业务预期差一年。6.4 我的最终建议如果是面试场景给出substring方案可以体现基础功底但紧接着展示LocalDate方案并说明两者的边界差异才是更完整的回答。如果是生产代码我会按这个顺序决策先确认上游格式和脏数据比例。确认是否存在null、空串、前后空格、单月单日、带时间等变体。日常首选LocalDate.parse getYear必要时配一个宽松DateTimeFormatter。性能敏感且格式铁板钉钉再用substring但一定要在代码注释里写清“不要乱动依赖固定宽度”。所有解析集中到一个工具方法里避免团队里每个人都写一套自己的提取逻辑。个人最深的体会是这种小需求如果不把边界说清楚看起来三行代码的事后面一定会变成若干个if和异常补丁。把格式假设、空值策略、失败策略一次定下来反而省心。下次你再遇到类似需求希望能直接照这个思路落地少走几步弯路。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑