==> 学习汇总(持续更新)
==> 从零搭建后端基础设施系列(一)-- 背景介绍
请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,
当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。
这个题目其实很简单,没有绕的地方。
但是,做的过程中,发现使用java string的replaceAll方法居然比自己实现的要快很多,接下来就是要深挖一下这个坑。
注:以下分析只针对java,其它语言,例如C/C++可能不适用
使用java string自带的方法replaceAll。
public String replace1(StringBuffer str){
return str == null ? null : str.toString().replaceAll(" ", "%20");
}
自己实现
原始代码:
public String replace2(StringBuffer sb){
StringBuffer res = new StringBuffer(sb.length());
for (int i = 0; i < sb.length(); i++) {
if (sb.charAt(i) == ' '){
res.append("%20");
} else {
res.append(sb.charAt(i));
}
}
return res.toString();
}
优化代码1:
和原始代码的区别在于StringBuilder和StringBuffer的区别(具体什么区别,先想想,答案在最后的总结)
public String replace3(StringBuffer sb){
StringBuilder res = new StringBuilder(sb.length());
for (int i = 0; i < sb.length(); i++) {
if (sb.charAt(i) == ' '){
res.append("%20");
} else {
res.append(sb.charAt(i));
}
}
return res.toString();
}
优化代码2:
和优化代码1的区别在于先将StringBuffer转String(为什么要转?)
public String replace4(StringBuffer sb){
String a = sb.toString();
StringBuilder res = new StringBuilder(a.length());
for (int i = 0; i < a.length(); i++) {
if (a.charAt(i) == ' '){
res.append("%20");
} else {
res.append(a.charAt(i));
}
}
return res.toString();
}
优化代码3:
和优化代码2的区别在于一块一块的复制(和代码2的一个个字符复制哪个更快?)
public String replace5(StringBuffer sb){
String a = sb.toString();
StringBuilder res = new StringBuilder(a.length());
int start = 0, end = 0;
for (int i = 0; i < a.length(); i++) {
if (a.charAt(i) == ' '){
res.append(a, start, end);
res.append("%20");
start = i + 1;
}
end = i + 1;
}
if(start < end) {
res.append(a, start, end);
}
return res.toString();
}
以下测试取10次平均值,并且空格占比为1/10(空格占增大,replaceAll效率会怎样变?自己实现的代码效率会怎样变?)
代码 / 字符串长度 | 1024 | 1024 * 10 | 1024 * 100 | 1024 * 1000 | 1024 * 10000 | 1024 * 100000 |
---|---|---|---|---|---|---|
replace1(replaceAll) | 0.7 ms | 1.1 ms | 4.7 ms | 15.8 ms | 123.0 ms | 1029.1 ms |
replace2(原始代码) | 0.3 ms | 1.8 ms | 9.9 ms | 76.9 ms | 744.5 ms | 7207.0 ms |
replace3(优化代码1) | 0.2 ms | 1.2 ms | 8.3 ms | 71.3 ms | 696.5 ms | 7018.6 ms |
replace4(优化代码2) | 0.1 ms | 0.8 ms | 2.0 ms | 9.1 ms | 69.5 ms | 644.8 ms |
replace5(优化代码3) | 0.2 ms | 0.7 ms | 2.6 ms | 10.0 ms | 72.8 ms | 733.2 ms |
1.在这个题中replaceAll的思想和replace5一样,都是从头开始找,找到空格后,将两个空格之间的字符串复制过去(底层还是一个个字符的赋值)。
2.replace3和replace2的优化点在于,StringBuffer是线程安全的,每一个方法都加了锁,而StringBuilder是非线程安全的,方法没有加锁,所以从测试中可以看出replace3的速度比replace2的速度要快一点。
3.replace4和replace3的优化点在于先将StringBuffer转String,为什么要这样呢?原因是for循环中用到了str.length()和str.charAt(),这个两个方法是同步方法,加锁了的,每一次循环,去执行一次这个方法,会消耗比较多的时间。所以从测试中可以看出replace4比replace3速度快了10倍左右。
4.replace5和replace4的优化点在于一块一块的复制,但是从测试的结果看,其速度居然是慢了的,这是为什么呢?贴一下源码,大家就懂了。
public AbstractStringBuilder append(CharSequence s, int start, int end) {
if (s == null)
s = "null";
if ((start < 0) || (start > end) || (end > s.length()))
throw new IndexOutOfBoundsException(
"start " + start + ", end " + end + ", s.length() "
+ s.length());
int len = end - start;
ensureCapacityInternal(count + len);
for (int i = start, j = count; i < end; i++, j++)
value[j] = s.charAt(i);
count += len;
return this;
}
它里面也是一个个字符赋值给value数组的,所以速度肯定是replace5>replace4的。
那么什么情况下可以像C/C++那样,直接用指针将一块块内存复制过去呢?
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
if (srcBegin < 0) {
throw new StringIndexOutOfBoundsException(srcBegin);
}
if (srcEnd > value.length) {
throw new StringIndexOutOfBoundsException(srcEnd);
}
if (srcBegin > srcEnd) {
throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
}
System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}
System.arraycopy是JVM自己实现的本地方法,可能底层用的就是C/C++内存块复制(我猜的,没研究过这个方法)。
5.空格占比大的时候,replaceAll方法的速度会渐渐的慢于自己实现的代码,replace2和replace3速度会慢慢变快,replace4和replace5速度会慢慢变慢,感兴趣的可以自己测试一下。
6.虽然这道题,不论怎么写,都可以AC,但是如果我是面试官,我会就这道题,从系统自带的实现,到自己实现,再到优化来考。