文档章节

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

GotaX
 GotaX
发布于 2017/02/28 00:05
字数 976
阅读 47
收藏 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
浦东
程序员
私信 提问
Java 7 运行环境出现新漏洞,Mac系统面临巨大威胁

几位安全专家现在警告大家Java 7运行环境再次出现漏洞,这对于Mac用户来说非常危险。攻击者可以利用该漏洞通过各种系统的各种浏览器对安装Java的机器进行攻击。这次漏洞非常危险,当用户访问...

oschina
2012/08/28
2.8K
30
sqoop1 导出与hue oozie踩坑

可能是不同版本不同吧,按网友的最终改为: export --connect jdbc:mysql://172.16.5.100:3306/dwtest --username testuser --password ** --table che100kv --export-dir /user/hive/wareho......

hblt-j
09/18
0
0
九浅一深ThreadLocal

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

潘复哲
07/27
0
0
Oracle 为 OS X 发布 Java 开发工具

2010年末的时候,苹果宣布终止对OS X的Java支持,当时乔布斯表示苹果的Java版本总是比Sun/Oracle发布落后,导致苹果比其他所有平台的Java版本都要老,所以继续支持没 有意义了。之后过了几周...

红薯
2012/04/27
2.1K
9
SpringMVC跨域配置以及权限控制拦截跨域请求时的问题解决

背景 最近公司开始推行前后端分离的架构,于是不可避免的引入了跨域的问题,跨域的概念可以参考大佬的博客,这里就不再赘述了。 作为Java最流行框架之一的Spring其实已经帮我们写好了很多代码...

Iceberg_XTY
09/29
0
0

没有更多内容

加载失败,请刷新页面

加载更多

我的Linux系统九阴真经

在今天,互联网的迅猛发展,科技技术也日新月异,各种编程技术也如雨后春笋一样,冒出尖来了。各种创业公司也百花齐放百家争鸣,特别是针对服务行业,新型互联网服务行业,共享经济等概念的公...

问题终结者
2分钟前
0
0
Java 使用 gson 对 json 根据 key 键进行排序

引入Google的gson jar <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>2.8.0</version>......

yh32
4分钟前
0
0
Vue.use源码解析

什么是Vue插件 关于什么是Vue插件大家可以去看官网的解释 ,总得来说就是提供一个全局注册/调用的能力。 怎么用 我们以Weex为例。 首先有一个toast.js const Toast = {}Toast.install = (Vu...

peakedness丶
11分钟前
0
0
mybatis学习(2)

http://www.mybatis.org/spring/zh/factorybean.html 参考mybatis官网 Mybatis集成Spring: 使用Spring的IOC,将sqlSession(存在事物),交给Spring管理。 1.依赖jar包 <dependency> <g......

杨健-YJ
21分钟前
1
0
ES的性能优化

我们在很多场景下会用到ES帮助我们解决搜索问题,但是很多人了解只是停留在表面,如何深入的使用ES,并做针对性的性能优化呢? 批量提交 当大量的写任务时,可以采用批量提交的方案,但是需要...

春哥大魔王的博客
22分钟前
1
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部