文档章节

Java String 中一不留神就中招的坑

GotaX
 GotaX
发布于 2017/02/28 00:05
字数 976
阅读 27
收藏 2

String 类型应该算是 Java 中最常用的类型之一了, 不过其中有一些函数用不好就容易中招.

我们就以分割字符串的函数举例, 一般我们都会这么写:

String[] arr = "a,b,c".split(",");

非常简洁, 并且没什么问题. 不过一旦上下文变得复杂起来事情恐怕就没这么简单了.

比如说, 现在我们的任务是要对一本 (英文) 书进行分词, 分割符是标点和空格. 一般来说我们可以这么写:

for (String line : lines) {
    line.split("[,.!+@#$%^&*()\\- ]");
}

这种写法虽说看起来 OK, 但其实并非如此. 实际上, 这样写的话一旦遇到调用频率高或是需要分割大文本的情况就会出现内存占用大及运行耗时长的问题. 至于为什么会这样, 我们可以来看一下该函数是如何实现的:

public String[] split(String regex, int limit) {
    /* fastpath if the regex is a
     (1)one-char String and this character is not one of the
        RegEx's meta characters ".$|()[{^?*+\\", or
     (2)two-char String and the first char is the backslash and
        the second is not the ascii digit or ascii letter.
     */
    char ch = 0;
    if (((regex.value.length == 1 &&
         ".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1) ||
         (regex.length() == 2 &&
          regex.charAt(0) == '\\' &&
          (((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 &&
          ((ch-'a')|('z'-ch)) < 0 &&
          ((ch-'A')|('Z'-ch)) < 0)) &&
        (ch < Character.MIN_HIGH_SURROGATE ||
         ch > Character.MAX_LOW_SURROGATE))
    {
        int off = 0;
        int next = 0;
        boolean limited = limit > 0;
        ArrayList<String> list = new ArrayList<>();
        while ((next = indexOf(ch, off)) != -1) {
            if (!limited || list.size() < limit - 1) {
                list.add(substring(off, next));
                off = next + 1;
            } else {    // last one
                //assert (list.size() == limit - 1);
                list.add(substring(off, value.length));
                off = value.length;
                break;
            }
        }
        // If no match was found, return this
        if (off == 0)
            return new String[]{this};

        // Add remaining segment
        if (!limited || list.size() < limit)
            list.add(substring(off, value.length));

        // Construct result
        int resultSize = list.size();
        if (limit == 0) {
            while (resultSize > 0 && list.get(resultSize - 1).length() == 0) {
                resultSize--;
            }
        }
        String[] result = new String[resultSize];
        return list.subList(0, resultSize).toArray(result);
    }
    return Pattern.compile(regex).split(this, limit);
}

也就是说在大部分情况下, split(String regex) 函数实际上是新建了一个 Pattern 对象, 再去调用它的 split(CharSequence input, int limit) 函数, 仅有两种情况例外:

  • 传入的 regex 参数仅有一个字符, 且非正则表达式中的 ".$|()[{^?*+\" 字符
  • 传入的 regex 参数仅有两个字符, 且第一个字符为反斜杠, 第二个字符不能是数字或字母

这样事情就很明朗了, 我们在分词的时候调用了多少次 split 函数就等于新建了多少 Pattern 对象, 自然会慢. 因此只要对原来的实现稍加改动就能解决这个问题:

Pattern pattern = Pattern.compile("[,.!+@#$%^&*()\\- ]");
for (String line : lines) {
    pattern.split(line);
}

下面附上实际测试的一些数据, 对 "The_Thousand_and_One_Nights_.txt" (1.42 MB) 进行分词, 两种调用方式各跑 100 遍, 取平均值后结果如下:

String#splitPattern#split
24.32 ms3.03 ms

从这个结果可以看出比较明显的差异, Pattern 要比 String 的切分快 8 倍左右.

需要注意的是 String 中除了 split 还有一些函数也会在内部生成 Pattern 对象, 包括:

  • matches(String regex)
  • replaceFirst(String regex, String replacement)
  • replaceAll(String regex, String replacement)
  • split(String regex, int limit)

所以使用这些函数的时候就要小心了, 如果是被反复调用的情况, 最好是声明成一个 Pattern 常量, 再去调用对应的函数.

最后附上测试代码:

import com.google.common.base.Stopwatch;
import com.google.common.io.Resources;
import org.junit.BeforeClass;
import org.junit.Test;

import java.io.IOException;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.IntStream;

/**
 * Created by gota on 2017/2/27.
 */
public class StringTest {

  private static final String REGEX = "[,.!+@#$%^&*()\\- ]";

  private static List<String> lines = new LinkedList<>();

  @BeforeClass
  public static void before() throws IOException {
    URL resource = Resources.getResource(StringTest.class, "The_Thousand_and_One_Nights_.txt");
    lines.addAll(Resources.readLines(resource, Charset.defaultCharset()));
  }

  /**
   * 使用 String 的 split 函数分割字符串
   */
  @Test
  public void test() {
    split(lines, line -> line.split(REGEX));
  }

  /**
   * 使用 Pattern 的 split 函数分割字符串
   */
  @Test
  public void test2() {
    Pattern pattern = Pattern.compile(REGEX);
    split(lines, line -> pattern.split(REGEX));
  }

  private void split(List<String> lines,
                     Function<String, String[]> function) {
    IntStream
        .range(0, 100)
        .mapToLong($ -> {
          Stopwatch stopwatch = Stopwatch.createStarted();
          lines.forEach(function::apply);
          return stopwatch.stop().elapsed(TimeUnit.MILLISECONDS);
        })
        .average()
        .ifPresent(ms -> System.out.printf("Use: %s ms\n", ms));
  }
}

© 著作权归作者所有

共有 人打赏支持
GotaX
粉丝 9
博文 5
码字总数 4551
作品 0
浦东
程序员
九浅一深ThreadLocal

ThreadLocal的作用、使用示例 ThreadLocal是线程的本地存储,存储在其内的值只能被当前线程访问到,其他线程获取不到,可以存储任意对象。 经常用来存储当前线程的一些上下文信息,这样不用通...

潘复哲
07/27
0
0
『OGG 02』Win7 配置 Oracle GoldenGate Adapter Java 踩坑指南

上一文章 《Win7 配置OGG(Oracle GoldenGate).docx》定下了 两个目标: 目标1: 给安装的Oracle_11g 创建 两个用户 admin 和 root 。 admin 对应了 ADMIN 结构,创建了一个 TB_ TEST表。 ro...

InkFx
06/30
0
0
java ThreadLocal(应用场景及使用方式及原理)

虽然ThreadLocal与并发问题相关,但是许多程序员仅仅将它作为一种用于“方便传参”的工具,胖哥认为这也许并不是ThreadLocal设计的目的,它本身是为线程安全和某些特定场景的问题而设计的。 ...

onedotdot
2017/10/18
0
0
记一次Java加密加签算法到php的坑

此文为本人原创首发于 http://www.35coder.com/convertencryptioncodesto_php/。 写代码的经历中,总少不了与外部的程序对接,一旦有这样的事,往往周期会很长,很麻烦,因为你要考虑的事会多...

rinson
07/18
0
0
从动态代理,看Class文件结构定义

前言 这篇内容是上一篇动态代理是如何"坑掉了"我4500块钱的补充,进一步分析篇。 建议二者结合食用,醇香绵软,入口即化。 好了,不扯淡了,开始... 正文 2、Class 文件的格式 这里为啥是2开...

MDove
08/13
0
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

高三暑假我是怎么想开去学linux系统的

高三的时候,我有一句口头禅:“老了老了,现在做题越来越迟钝了”。当时整天日夜苦读,体重日益增加,脸色越来越黯淡,我在终于熬过了高考的时候,简直心里面乐得开了花。我终于可以去做自己...

linuxprobe16
22分钟前
0
0
Python 获得命令行参数的方法

需要模块:sys 参数个数:len(sys.argv) 脚本名: sys.argv[0] 参数1: sys.argv[1] 参数2: sys.argv[2]

编程老陆
29分钟前
0
0
链队

队列用链表来表示时,需要用两个变量来记录队列两端的变化:theFront,theBack. 根据链接方向的不同,链队有两种链接方式(其实就是链表的头插入节点和尾插入节点,头删除节点和尾删除节点)。...

Frost729
32分钟前
0
0
IDEA toString() json模板

public java.lang.String toString() {java.lang.StringBuilder builder = new java.lang.StringBuilder();#set ($i = 0)#foreach ($member in $members)#if ($i == 0)builder.appen......

Mtok
42分钟前
0
0
Dubbo内核实现之SPI简单介绍

Dubbo采用微内核+插件体系,使得设计优雅,扩展性强。那所谓的微内核+插件体系是如何实现的呢!即我们定义了服务接口标准,让厂商去实现(如果不了解spi的请谷歌百度下), jdk通过ServiceLo...

明理萝
47分钟前
0
1

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部