V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
mylxsw
V2EX  ›  程序员

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

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

    最近生产环境遇到一个奇怪的数组下标越界报错,如下图代码所示,我们可以肯定的是 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
    mringg
        10
    mringg  
       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   ·   我们的愿景   ·   实用小工具   ·   1024 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 29ms · UTC 19:21 · PVG 03:21 · LAX 12:21 · JFK 15:21
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.