Java 字符串 split 的一个反直觉陷阱 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
请不要在回答技术问题时复制粘贴 AI 生成的内容
mylxsw
V2EX    程序员

Java 字符串 split 的一个反直觉陷阱

  •  
  •   mylxsw
    mylxsw 2022-11-27 15:02:46 +08:00 4254 次点击
    这是一个创建于 1053 天前的主题,其中的信息可能已经有所发展或是发生改变。

    最近生产环境遇到一个奇怪的数组下标越界报错,如下图代码所示,我们可以肯定的是 fieldName 变量不为空(不是空字符串,也不是 null),但是代码执行到读取 names[0] 变量的时候,抛出了一个 数组下标越界java.lang.ArrayIndexOutOfBoundsException) 的异常。

    异常信息如下图所示

    问题很简单,我们对一个字符串执行 split 方法之后,以过往其它编程语言( Go 、PHP 、Javascript 、Dart 等)的使用经验来看,即使字符串为空,即使没有匹配到分隔符,在返回值数组中也会包含一个当前字符串的值。但是这里却抛出了 ArrayIndexOutOfBoundsException,难道 split 方法的返回值可能为空数组?

    最终经过排查发现,在上述代码段中,当 fieldName 的值为 "~" 的时候,我们访问 names[0] 就会抛出 ArrayIndexOutOfBoundsException,为什么会这样呢?

    本文将会持续修正和更新,最新内容请参考我的 GITHUB 上的 程序猿成长计划 项目,欢迎 Star ,更多精彩内容请 follow me

    问题

    在 Java 中,如果执行下面这段代码,直觉上你认为会输出什么?

    String str = "~"; String []arr = str.split("~"); System.out.println(arr.length); 

    如果你有其他编程语言的经验,可能直觉上会觉得这里输出的应该是 2,但是遗憾的是,这里输出的是 0,变量 arr 是个空数组。

    这里不禁怀疑自己之前的记忆是不是有偏差,于是我又使用其它语言来尝试复现这个问题。

    不同语言中 split 的行为

    我总结了一个表格,说明了不用语言不同的行为,这里对比的是执行 split 函数 /方法后返回数组的长度:

    语言\函数 "".split("") "~".split("~") "~~".split("~") "".split("~") "~123".split("~")
    Javascript 0 2 3 1 2
    PHP 0 2 3 1 2
    Dart 0 2 3 1 2
    Golang 0 2 3 1 2
    Scala 1 0 0 1 2
    Java 1 0 0 1 2

    Javascript

    首先是 Javascript ,在浏览器的控制台上直接执行,得到了下面的结果

    "".split("") "~".split("~") "~~".split("~") "".split("~") "~123".split("~") 

    执行结果

    跟我的直觉是一致的,同样的情况,这里返回的是 2

    PHP

    在 PHP 中,我使用了 mb_split 函数,该函数用于对多字节字符串进行分割

    执行结果如下

    执行结果跟我的直觉也是一致的,同样的情况,这里返回的是 2

    Dart

    然后是 Google 的 Dart ,这是一门主要用于使用 Flutter 来开发跨平台应用的编程语言,代码如下

    void main() { print("".split('').length); // 0 print("~".split('~').length); // 2 print("~~".split('~').length); // 3 print("".split('~').length); // 1 print("~123".split('~').length); // 2 } 

    执行结果

    同样,"~".split("~") 也是返回了两个值。

    Golang

    在 Golang 中,执行结果依旧是符合直觉的,返回的是 2

    package main import( "strings" "fmt" ) func main() { printStrs(strings.Split("", "")) // 0 [] printStrs(strings.Split("~", "~")) // 2 ["", "", ] printStrs(strings.Split("~~", "~")) // 3 ["", "", "", ] printStrs(strings.Split("", "~")) // 1 ["", ] printStrs(strings.Split("~123", "~")) // 2 ["", "123", ] } func printStrs(s []string) { fmt.Print(len(s), " [") for _, item := range s { fmt.Printf(`"%s", `, item) } fmt.Print("]\n") } 

    执行结果

    Scala

    然后,我又尝试了 Scala ,发现在 Scala 中, split 的行为有些不一样了。

    "".split(").length "~".split("~").length "~~".split("~").length "".split("~").length "~123".split("~").length 

    代码 "~".split("~") 返回的是 空数组,与在 Java 中我们遇到的问题如出一辙。

    Java

    最后,我又用 Java 执行了同样的代码

    package example; import org.junit.Test; public class ExampleTest { @Test public void testSplit() { printStrings("".split("")); // 1 ["", ] printStrings("~".split("~")); // 0 [] printStrings("~~".split("~")); // 0 [] printStrings("".split("~")); // 1 ["", ] printStrings("~123".split("~")); // 2 ["", "123", ] } private void printStrings(String[] strings) { System.out.print(strings.length + " ["); for (String str : strings) { System.out.printf("\"%s\", ", str); } System.out.println("]"); } } 

    执行结果

    结果与 Scala 是一致的,同时也解释了为什么我们会遇到 ArrayIndexOutOfBoundsException 的问题。

    原因

    翻阅了 Java 的 API 文档,发现原来 Java 中的 split 方法确实跟其它语言是不一样的,这一点我们特别容易忽略

    如果分隔符表达式与字符串不匹配,则返回原始字符串作为数组的唯一值,这也就解释了

    "".split("") // 1 [""] "".split("~") // 1 [""] 

    如果分隔符表单式与字符串的开始字符就已经匹配了,则返回值中第一个元素会被设置为 ""

    "~123".split("~") // 2 ["", "123"] 

    如果 limit 参数为 0,也就是 split(String regex) 方法,则匹配结果末尾的所有空字符串 "" 都会被丢弃,也就解释了下面两段代码

    "~".split("~") // 0 [] "~~".split("~") // 0 [] 

    然后我又翻阅了 Scala 的官方文档,Scala 和 Java 的行为是一致的。

    总结

    在 Java 中使用字符串的 split 方法,一般情况下的行为是和其他编程语言是一致的,但在一些边界条件下,也有一些不一致的地方,这一点是我们应该注意的,这也提醒了我们,不要想当然的认为不同语言,同名函数(方法)的功能是完全一致的,当我们遇到一些奇奇怪怪的问题时,多看官方文档才是硬道理。

    本文将会持续修正和更新,最新内容请参考我的 GITHUB 上的 程序猿成长计划 项目,欢迎 Star ,更多精彩内容请 follow me

    14 条回复    2022-11-29 12:12:58 +08:00
    chendy
        1
    chendy  
       2022-11-27 18:45:32 +08:00   14
    很直觉地拉到最后,果然,推广区见
    wiix
        2
    wiix  
       2022-11-27 18:46:45 +08:00
    @chendy 屏蔽之
    xy90321
        3
    xy90321  
       2022-11-27 21:06:40 +08:00 via iPhone   4
    撇开推广不谈… 鬼的反直觉,原生 Java 程序员还觉得 JS 等的逻辑是异端呢?无非就是过于自信先入为主了,用之前不查文档只能说明是个只有“直觉”没有“敬畏”的码农而已
    blankmiss
        4
    blankmiss  
       2022-11-27 22:17:48 +08:00
    确实 我直接划到评论区
    az467
        5
    az467  
       2022-11-27 23:15:10 +08:00
    虽然但是 为什么整的跟黑盒测试一样
    yuk1no
        6
    yuk1no  
       2022-11-27 23:16:29 +08:00 via iPhone
    RTFM
    zwb9412
        7
    zwb9412  
       2022-11-27 23:19:33 +08:00
    别的语言不清楚,java 的 split 一般不用 string.split(String regex) 而使用 string.split(String regex, int limit)。因为 string.split(String regex) 内部调用的 string.split(String regex, 0) 有特殊逻辑。
    循环判断的更是使用 pattern.split(CharSequence input, int limit)。
    zeni123
        8
    zeni123  
       2022-11-28 03:38:18 +08:00
    不是有源代码吗?
    Necer
        9
    Necer  
       2022-11-28 09:13:16 +08:00
    我直接用了 apache 的 StringUtils
    ming2050
        10
    ming2050  
       2022-11-28 09:19:45 +08:00 via iPhone
    哎,我怎么点进来的
    threeroseing0
        11
    threeroseing0  
       2022-11-28 10:39:26 +08:00
    可以多用工具类,比如 lang3 的 StringUtils
    vincent7245
        12
    vincent7245  
       2022-11-28 11:02:47 +08:00
    1 不是反直觉,是 java 的特性,只是你使用其他语言的习惯让你先入为主了。2 这样推广真的好吗
    moshiyeap100
        13
    moshiyeap100  
       2022-11-28 14:50:26 +08:00
    连续两次 Ctrl 和鼠标左键就能得到答案啦(或者花五分钟阅读 API 文档)。 推广至少也拿点硬干货文章开路吧?
    HikariLan
        14
    HikariLan  
       2022-11-29 12:12:58 +08:00
    当你写出 String []arr 的时候,我就知道我们已经不是一路人了
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     1071 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 30ms UTC 23:12 PVG 07:12 LAX 16:12 JFK 19:12
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86