字符串切分工具比较

在日常开发中,我们经常需要处理字符串。字符串处理通常有两个主要的场景:一个是字符串的格式化,通常我们通过String.format方法或者构建一个StringBuilder,然后通过append方法来完成;另一个是字符串的切分。在这篇文章中,通过String.split、Apache Commons中的StringUtils.split和Google工具包Gauva中提供的Splitter三个字符串切分工具的测试来体会一下三者在使用方法和性能上的差异。

在进行实际讨论之前,我们先花一些小的篇幅介绍一下Google的Gauva工具包。实际上,Guava几乎可以看成是一个全新的用更“时髦”(更符合现代编程理念)的Apache Commons的替代品。最新的Guava版本提供了集合、并发、基本类型操作、反射、比较、I/O、哈希、网络通讯、字符串处理、数学处理、基于内存的缓存、基于内存的发布/订阅等等一个庞大而全面的数据访问和处理工具库。

好了,下面就让我们看看在字符串切分上不同的工具会给我们带来什么样的惊喜吧。

我们知道,String类本身实际上已经提供了一个split的方法用来实现字符串的切分。比如,如果我们想把字符串“I am from Newtouch”分成I,am,from,Newtouch四个独立的字符串时,通过以下代码就可以简单的实现。
  final String message = "I am from Newtouch";
	String[] items = message.split(" ");

如果仔细观察,我们会发现,在按空格切分字符串时,虽然空格(‘ ’)只是一个字符,但是仍需要传入一个字符串作为切分匹配的参数。其实,split方法的参数不仅仅只是一个简单的字符串,而且还支持正则表达式的所有语义。仍然以上例为例,如果在两个单词之间(如:I和am)存在两个或者三个以上的空格,我们仍然希望切分结果是I,am,from,Newtouch四个独立的字符串的话,只需按照正则表达式的语法稍微修改一下split的参数。代码如下:
  final String message = "I     am     from              Newtouch";
	String[] items = message.split(" +");

通过这个简单的小例子,我们可以体会到正则表达式的威力。但是,在一些对性能要求非常高的场合,当只需要按照单个字符进行切分时,JDK这种支持正则表达式的切分方法无疑又会成为一种“鸡肋”;而另一种情况则正好相反,对于复杂的切分方式,开发人员很多时候更希望采用一种比正则表达式更好用、也更直白、更易于理解和掌握的方式来书写代码。还好,很多工具包都提供了基单个字符的切分方法,如本文重点讨论的Apache Commons中的StringUtils和Google的Guava工具包提供的Splitter。StringUtils和Splitter使用起来都非常简单。如下所示:
  
  //Apache commons-lang StringUtils.split
	final String message = "I am from Newtouch";
	String[] items = StringUtils.split(message,' ');
  


  //Google Guava Splitter
	final String message = "I am from Newtouch";
  Iterator<String> items = Splitter.on(' ').split(message).iterator();
  
那么,我们在处理一个字符串切分问题时,究竟选择哪种切分工具更好呢?要了解这个问题,我们先看看下面这个性能测试。
public class StringSplitPerformanceTest
{
	//测试循环次数
    static int testDataCount = 5000;
    //测试字符串的长度
    static int testDataStringSize = 100;
    //测试数据集
    static List<String> testData = new LinkedList<String>();
    //测试结果记录
    static StopWatch stopWatch = new StopWatch(String.format("Test the performance of splitting a string which size is %d, %d times.",testDataStringSize,testDataCount));
    
    //创建测试数据
    @BeforeClass
    public static void generateTestData() {
        for(int i=0; i < testDataCount; i++){
            String data = RandomStringUtils
              .random(testDataStringSize, "ABCDEFGHIJKLMNOPQRSTUVXYZ,");
            testData.add(data);
        }
    }

    //输出测试结果
    @AfterClass
    public static void printTestSummery(){
        System.out.println(stopWatch.prettyPrint());
    }

    //测试Apache commons-lang的StringUtils.split
    @Test
    public void apacheCommonLangSplit()  throws Exception {
        stopWatch.start("Apache Common Lang Split");
        for(String data: testData){
            String[] elements = StringUtils.split(data, ',');
            for (String element : elements) {
                assertTrue(element.length() <= testDataStringSize);
            }
        }
        stopWatch.stop();
    }
    
    //测试Goolge Guava的Splitter
    @Test
    public void guavaSplitterSplit() throws Exception {
        Splitter spiltter = Splitter.on(',');
        stopWatch.start("Google Guava Splitter");
        for(String data: testData){
            Iterable<String> elements = spiltter.split(data);
            for (String element : elements) {
                assertTrue(element.length() <= testDataStringSize);
            }
        }
        stopWatch.stop();
    }
    
    //测试String自身的split方法
    @Test
    public void jdkStringSplit() throws Exception {
        stopWatch.start("JDK String Split");
        for(String data: testData){
        	 String[] elements = data.split(",");
            for (String element : elements) {
                assertTrue(element.length() <= testDataStringSize);
            }
        }
        stopWatch.stop();
    }
}

在这个单元测试中,分别使用String.split、Apache commons-lang中的StringUtils.split和Google Guava的Splitter类对随机产生的长度为100的字符串进行5000次切分,并使用了Spring的StopWatch来收集和统计测试结果。三次测试结果如下:

第一次:
StopWatch 'Test the performance of splitting a string which size is 100, 5000 times.': running time (millis) = 239
-----------------------------------------
ms     %     Task name
-----------------------------------------
00037  015%  Apache Common Lang Split
00042  018%  Google Guava Splitter
00160  067%  JDK String Split

第二次:
StopWatch 'Test the performance of splitting a string which size is 100, 5000 times.': running time (millis) = 394
-----------------------------------------
ms     %     Task name
-----------------------------------------
00053  013%  Apache Common Lang Split
00094  024%  Google Guava Splitter
00247  063%  JDK String Split

第三次:
StopWatch 'Test the performance of splitting a string which size is 100, 5000 times.': running time (millis) = 317
-----------------------------------------
ms     %     Task name
-----------------------------------------
00039  012%  Apache Common Lang Split
00045  014%  Google Guava Splitter
00233  074%  JDK String Split

从测试结果中我们发现,JDK自身的split方法性能上远不如其他两个工具;而在需要进行切分的字符串长度比较短(如本测试中的字符串长度为100)的情况下,Apache commons-lang的StringUtils.split的切分效率最高;Google Guava的Splitter在性能上和StringUtils有1倍左右的差距。

本文的最后,我们再讨论一下Splitter的适用场景。有时,我们需要切分的字符串往往不是特别规整。比如我们希望把字符串“I am    from   Newtouch.”切分为“I”,“am”,“from”,“Newtouch”四个单元时,使用正则表达式对开发人员来说就是一个不小的挑战,而且通过上面的测试我们知道正则表达式的处理效率来说并不是很好。使用Splitter可以很灵活的处理了这些问题。
  @Test
	public void testGoogleGuavaSplitter() {
		//Google Guava Splitter
		final String message = "I am   from     Newtouch.";
		Iterator<String> items = Splitter.on(' ') 	//按空格切分
				.trimResults(CharMatcher.is('.'))		//去掉结尾的.
				.omitEmptyStrings()						//去掉多余的空字符串
				.split(message).iterator();
		
		assertEquals("I", items.next());
		assertEquals("am", items.next());
		assertEquals("from", items.next());
		assertEquals("Newtouch", items.next());
	}

你可能感兴趣的:(java,编程,performance)