见微知著: StringUtils.split

作者:明明如月学长, CSDN 博客专家,《性能优化方法论》作者、《解锁大厂思维:剖析《阿里巴巴Java开发手册》》、《再学经典:《EffectiveJava》独家解析》专栏作者。

热门文章推荐

image.png

一、背景

前一段时间,身边有个同事使用 org.apache.commons.lang3.StringUtils#split(java.lang.String, java.lang.String)对字符串进行切割,发现完全和预期不符。
本文将对这个简单的问题进行分析,并思考通过这个问题我们可以学到什么。

二、问题分析

2.1 情景再现

下面是模拟代码

 public static void main(String[] args) {
        String input = "this is a demo, this \",\"is a \"demo";
        String[] split = StringUtils.split(input,"\",\"");
        for(String str: split){
            System.out.println(str);
        }
    }

预期是使用 "," 切割字符串,因此应该被切割成两部分。
但是输出结果为:

this is a demo, this 
is a 
demo

这是怎么回事?

2.2 源码分析

org.apache.commons.lang3.StringUtils#split(java.lang.String, java.lang.String)

 /**
     * <p>Splits the provided text into an array, separators specified.
     * This is an alternative to using StringTokenizer.</p>
     *
     * <p>The separator is not included in the returned String array.
     * Adjacent separators are treated as one separator.
     * For more control over the split use the StrTokenizer class.</p>
     *
     * <p>A {@code null} input String returns {@code null}.
     * A {@code null} separatorChars splits on whitespace.</p>
     *
     * <pre>
     * StringUtils.split(null, *)         = null
     * StringUtils.split("", *)           = []
     * StringUtils.split("abc def", null) = ["abc", "def"]
     * StringUtils.split("abc def", " ")  = ["abc", "def"]
     * StringUtils.split("abc  def", " ") = ["abc", "def"]
     * StringUtils.split("ab:cd:ef", ":") = ["ab", "cd", "ef"]
     * </pre>
     *
     * @param str  the String to parse, may be null
     * @param separatorChars  the characters used as the delimiters,
     *  {@code null} splits on whitespace
     * @return an array of parsed Strings, {@code null} if null String input
     */
    public static String[] split(final String str, final String separatorChars) {
        return splitWorker(str, separatorChars, -1, false);
    }

进入源码发现和最初现象的差不多,第一个参数是字符串,第二个是分隔符。
关键函数上的示例,都是单个分隔符,并没有多分隔符的例子。

再观察一下参数名称,第二个参数名称为 separatorChars 即为分割的字符(复数)!!
因此怀疑,这里的 "," 会被分视作三个分割字符,分别为 ","
继续往底层分析,发现果然如此: org.apache.commons.lang3.StringUtils#splitWorker(java.lang.String, java.lang.String, int, boolean)

 /**
     * Performs the logic for the {@code split} and
     * {@code splitPreserveAllTokens} methods that return a maximum array
     * length.
     *
     * @param str  the String to parse, may be {@code null}
     * @param separatorChars the separate character
     * @param max  the maximum number of elements to include in the
     *  array. A zero or negative value implies no limit.
     * @param preserveAllTokens if {@code true}, adjacent separators are
     * treated as empty token separators; if {@code false}, adjacent
     * separators are treated as one separator.
     * @return an array of parsed Strings, {@code null} if null String input
     */
    private static String[] splitWorker(final String str, final String separatorChars, final int max, final boolean preserveAllTokens) {
        // Performance tuned for 2.0 (JDK1.4)
        // Direct code is quicker than StringTokenizer.
        // Also, StringTokenizer uses isSpace() not isWhitespace()

        if (str == null) {
            return null;
        }
        final int len = str.length();
        if (len == 0) {
            return ArrayUtils.EMPTY_STRING_ARRAY;
        }
        final List<String> list = new ArrayList<>();
        int sizePlus1 = 1;
        int i = 0;
        int start = 0;
        boolean match = false;
        boolean lastMatch = false;
        if (separatorChars == null) {
            // 省略
        } else if (separatorChars.length() == 1) {
            // 省略
        } else {
            // standard case
            while (i < len) {
                if (separatorChars.indexOf(str.charAt(i)) >= 0) {
                    if (match || preserveAllTokens) {
                        lastMatch = true;
                        if (sizePlus1++ == max) {
                            i = len;
                            lastMatch = false;
                        }
                        list.add(str.substring(start, i));
                        match = false;
                    }
                    start = ++i;
                    continue;
                }
                lastMatch = false;
                match = true;
                i++;
            }
        }
        if (match || preserveAllTokens && lastMatch) {
            list.add(str.substring(start, i));
        }
        return list.toArray(ArrayUtils.EMPTY_STRING_ARRAY);
    }


而且通过调试发现的确走到这里。
image.png

2.3、解决办法

解决办法很简单,使用 String 的 split 方法:

   public static void main(String[] args) {
        String input = "this is a demo, this \",\"is a \"demo";
        String[] split = input.split("\",\"");
        for(String str: split){
            System.out.println(str);
        }
    }

而且看源码可知,这里的参数是正则表达式。

    /**
     * Splits this string around matches of the given <a
     * href="../util/regex/Pattern.html#sum">regular expression</a>.
     *
     * <p> This method works as if by invoking the two-argument {@link
     * #split(String, int) split} method with the given expression and a limit
     * argument of zero.  Trailing empty strings are therefore not included in
     * the resulting array.
     *
     * <p> The string {@code "boo:and:foo"}, for example, yields the following
     * results with these expressions:
     *
     * <blockquote><table cellpadding=1 cellspacing=0 summary="Split examples showing regex and result">
     * <tr>
     *  <th>Regex</th>
     *  <th>Result</th>
     * </tr>
     * <tr><td align=center>:</td>
     *     <td>{@code { "boo", "and", "foo" }}</td></tr>
     * <tr><td align=center>o</td>
     *     <td>{@code { "b", "", ":and:f" }}</td></tr>
     * </table></blockquote>
     *
     *
     * @param  regex
     *         the delimiting regular expression
     *
     * @return  the array of strings computed by splitting this string
     *          around matches of the given regular expression
     *
     * @throws  PatternSyntaxException
     *          if the regular expression's syntax is invalid
     *
     * @see java.util.regex.Pattern
     *
     * @since 1.4
     * @spec JSR-51
     */
    public String[] split(String regex) {
        return split(regex, 0);
    }


三、启发

3.1 多看源码、多调试

当发现有些行为反直觉,不太对劲时,优先去查看底层源码,如果源码看不出来,可以进行调试。
另外,建议工作之余,在不是很忙的时候,可以主动看一些自己调用类的源码,一方面可以防止误用,另外一方面可以学习优秀源码的设计。

3.2 注释的规范性

在这个例子中大家可以看到,虽然 StringUtils.split 这个函数支持传入多个分割字符,但是并没有真正覆盖到多分割字符构成的字符串的情况,这是一大败笔。
这也给我们编写注释带来一些启发,首先工具类注释中可以给出常见调用示例和结果的对应关系,方便大家使用。但是注释中的示例要覆盖常见的输入和输出,至少不能出现令人误会的情况。

3.3 对面试的作用

如果有一天面试官问题,你对 JDK 或者你工作中用到的工具类库如 commons 和 guava 等,你能不能说一说他们有哪些不好的设计?有哪些 BUG ?
我相信大多数人是懵逼的,平时自己或者帮助身边人查问题的时候,可以适当留意一下这类问题。

3.4 及时请教

遇到问题首先自己排查,如果自己排除不出来可以优先问 AI,如果AI还是解决不了尽早问同事。
工作中遇到很多次,一些并不是很难的问题,因为“当局者迷”导致浪费很多时间,问身边的同事可能瞬间解决。

创作不易,如果本文对你有帮助,欢迎点赞、收藏加关注,你的支持和鼓励,是我创作的最大动力。

在这里插入图片描述

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

明明如月学长

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值