String.split
是Java里很经常使用的字符串操做,在普通业务操做里使用的话并无什么问题,但若是须要追求高性能的分割的话,须要花一点心思找出能够提升性能的方法。java
String.split
方法的分割参数regex
实际不是字符串,而是正则表达式,就是说分隔字符串支持按正则进行分割,虽然这个特性看上去很是好,但从另外一个角度来讲也是性能杀手。git
在Java6的实现里,String.split
每次调用都直接新建Pattern
对象对参数进行正则表达式的编译,再进行字符串分隔,而正则表达式的编译从字面上看就知道须要耗很多时间,而且实现中也没有对Pattern进行缓存,所以屡次频繁调用的使用场景下性能不好,若是是要使用正则表达式分隔的话,应该自行对Pattern
进行缓存。正则表达式
public String[] split(String regex, int limit) { return Pattern.compile(regex).split(this, limit); }
但不少时候咱们并不会真的想使用正则表达式分隔字符串,咱们其实想的只是用一个简单的字符好比空格、下划线分隔字符串而已,为了须要是知足这个需求却要背上正则表达式支持的性能损耗,很是不值得。apache
所以在Java7的实现里,针对单字符的分隔进行了优化,对这种场景实现了更合适的方法。单字符不走正则表达式的实现,直接利用indexOf
快速定位分隔位置,提升性能。数组
/* 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); }
有没有更快的方法?若是分隔符不是单字符并且也不须要按正则分隔的话,使用split
的方法还会和Java6同样使用正则表达式。这里还有其余备用手段:缓存
StringTokenizer
,StringTokenizer
没有正则表达式分隔的功能,单纯的根据分隔符逐次返回分隔的子串,默认按空格分隔,性能比String.split
方法稍好,但这个类实现比较老,属于jdk的遗留类,并且注释上也说明不建议使用这个类。org.apache.commons.lang3.StringUtils.split
分隔字符串,针对不须要按正则分隔的场景提供更好的实现,分隔符支持字符串。还能有更快的方法么?注意到String.split
和StringUtils.split
方法返回值是String[]
, 原始数组的大小是固定的,而在分隔字符串不可能提早知道分隔了多少个子串,那这个数组确定藏了猫腻,看看是怎么实现的。函数
定位String.split
单字符实现,发现分隔的子串其实保存在ArrayList
里,并无高深的技巧,直到路径的最后一行,代码对存储了子串的ArrayList
再转成数组,而toArray
的实现里对数组进行了复制。性能
return list.subList(0, resultSize).toArray(result);
StringUtils.split
方法里一样也是这样。优化
return list.toArray(new String[list.size()]);
所以这里能够作一个优化,把代码实现复制过来,而后将方法参数返回类型改成List
,减小数组复制的内存消耗。this
还能有更快的方法么?其实不少时候咱们须要对分隔后的字符串进行遍历访问作一些操做,并非真的须要这个数组,这和文件读取是同样的道理,读文件不须要把整个文件读入到内存中再使用,彻底能够一次读取一行进行处理,所以还能够作一个优化,增长参数做为子串处理方法的回调,在相应地方改成对回调的调用,这样能彻底避免数组的建立。也就是说,把字符串分隔看作一个流。
private static void splitWorker(final String str, final String separatorChars, final int max, final boolean preserveAllTokens, Consumer<String> onSplit) { if (str == null) { return; } final int len = str.length(); if (len == 0) { return; } int sizePlus1 = 1; int i = 0, start = 0; boolean match = false; boolean lastMatch = false; if (separatorChars == null) { // Null separator means use whitespace while (i < len) { if (Character.isWhitespace(str.charAt(i))) { if (match || preserveAllTokens) { lastMatch = true; if (sizePlus1++ == max) { i = len; lastMatch = false; } onSplit.accept(str.substring(start, i)); match = false; } start = ++i; continue; } lastMatch = false; match = true; i++; } } else if (separatorChars.length() == 1) { // Optimise 1 character case final char sep = separatorChars.charAt(0); while (i < len) { if (str.charAt(i) == sep) { if (match || preserveAllTokens) { lastMatch = true; if (sizePlus1++ == max) { i = len; lastMatch = false; } onSplit.accept(str.substring(start, i)); match = false; } start = ++i; continue; } lastMatch = false; match = true; i++; } } 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; } onSplit.accept(str.substring(start, i)); match = false; } start = ++i; continue; } lastMatch = false; match = true; i++; } } if (match || preserveAllTokens && lastMatch) { onSplit.accept(str.substring(start, i)); } } public static void split(final String str, final String separatorChars, Consumer<String> onSplit) { splitWorker(str, separatorChars, -1, false, onSplit); } // 使用方法 public void example() { split("Hello world", " ", System.out::println); }
还能有更快的方法么?也有更极端的优化方法,由于在拿子串(substring
方法)时实际发生了一次字符串复制,所以能够把回调函数改成传入子串在字符串的区间start、end,回调再根据区间读取子串进行处理,但并非很通用,这里就不展现代码了,有兴趣的能够试一下。
还能有更快的方...