头图

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

最近生产环境遇到一个奇怪的数组下标越界报错,如下图代码所示,我们可以肯定的是 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("~")
Javascript02312
PHP02312
Dart02312
Golang02312
Scala10012
Java10012

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


半栈开发者

7.7k 声望
1.7k 粉丝
0 条评论
推荐阅读
MySQL 数据库索引技术原理初探
一本书 500 页的书,如果没有目录,直接去找某个知识点,可能需要找一会儿,但是借助前面的目录,就可以快速找到对应知识点在书的哪一页。这里的目录就是索引。

mylxsw1阅读 1.2k

从零搭建 Node.js 企业级 Web 服务器(十五):总结与展望
总结截止到本章 “从零搭建 Node.js 企业级 Web 服务器” 主题共计 16 章内容就更新完毕了,回顾第零章曾写道:搭建一个 Node.js 企业级 Web 服务器并非难事,只是必须做好几个关键事项这几件必须做好的关键事项就...

乌柏木60阅读 6k评论 16

从零搭建 Node.js 企业级 Web 服务器(一):接口与分层
分层规范从本章起,正式进入企业级 Web 服务器核心内容。通常,一块完整的业务逻辑是由视图层、控制层、服务层、模型层共同定义与实现的,如下图:从上至下,抽象层次逐渐加深。从下至上,业务细节逐渐清晰。视图...

乌柏木39阅读 7.1k评论 6

从零搭建 Node.js 企业级 Web 服务器(二):校验
校验就是对输入条件的约束,避免无效的输入引起异常。Web 系统的用户输入主要为编辑与提交各类表单,一方面校验要做在编辑表单字段与提交的时候,另一方面接收表单的接口也要做足校验行为,通过前后端共同控制输...

乌柏木32阅读 6.1k评论 9

从零搭建 Node.js 企业级 Web 服务器(五):数据库访问
回顾 从零搭建 Node.js 企业级 Web 服务器(一):接口与分层,一块完整的业务逻辑是由视图层、控制层、服务层、模型层共同定义与实现的,控制层与服务层实现了业务处理过程,模型层定义了业务实体并以 对象-关系...

乌柏木32阅读 4.5k评论 9

从零搭建 Node.js 企业级 Web 服务器(十三):断点调试与性能分析
Node.js 官方提供了断点调试机制,出于安全性考虑默认为关闭状态,可以通过 node 参数 --inspect 或 --inspect-brk 开启,配合 IDE 能够非常方便地调试代码,本章就上一章已完成的项目 licg9999/nodejs-server-ex...

乌柏木29阅读 3.8k评论 9

从零搭建 Node.js 企业级 Web 服务器(八):网络安全
计算机网络依据 TCP/IP 协议栈分为了物理层、网络层、传输层、应用层,通常基础设施供应商会解决好前三层的网络安全问题,需要开发者自行解决应用层的网络安全问题,本章将着重表述应用层常见的网络安全问题及处...

乌柏木31阅读 5.7k评论 1

半栈开发者

7.7k 声望
1.7k 粉丝
宣传栏