1. 程式人生 > >敲代碼非常難之去除字符串的空白字符

敲代碼非常難之去除字符串的空白字符

builder 平衡點 for cep 算法 麻煩 length 拷貝 n)

在做性能調優時,用JProfiler測試Web應用的性能。發現有個replaceBlank函數占用了10%的CPU時間。進去看了下,是個簡單的用正則去除XML文檔裏空白字符串的功能。可是這個簡單功能卻消耗了10%的性能。

在Web應用裏。去掉空白字符串,似乎是個簡單的功能,可是真正寫起來。卻也有些麻煩事。

總結下。

方式一:正則表達式

http://stackoverflow.com/questions/5455794/removing-whitespace-from-strings-in-java

有兩種寫法:

s.replaceAll("\\s+", "");
s.replaceAll("\\s", "");
至於詳細哪一種比較好。和詳細的場景有有關。

有連續空白字符串的選擇每一種,假設是空白字符串都僅僅有一個的話,就選擇另外一種。個人傾向於第一種。

正則表達式是比較慢的。比以下的方法要慢3到4倍以上。

方式二:org.springframework.util.StringUtils.trimAllWhitespace

詳細的實現代碼例如以下:

	public static String trimAllWhitespace(String str) {
		if (!hasLength(str)) {
			return str;
		}
		StringBuilder sb = new StringBuilder(str);
		int index = 0;
		while (sb.length() > index) {
			if (Character.isWhitespace(sb.charAt(index))) {
				sb.deleteCharAt(index);
			}
			else {
				index++;
			}
		}
		return sb.toString();
	}

看起來,沒有什麽問題,可是程序猿的直覺:deleteCharAt函數是怎麽實現的?應該不會有什麽高效的算法能夠實現這種。

果然,實現代碼例如以下:

    public AbstractStringBuilder deleteCharAt(int index) {
        if ((index < 0) || (index >= count))
            throw new StringIndexOutOfBoundsException(index);
        System.arraycopy(value, index+1, value, index, count-index-1);
        count--;
        return this;
    }
顯然,過多地調用System.arraycopy會有性能問題。

方式三:改為調用StringBuilder.append 函數

	static public String myTrimAllWhitespace(String str) {
		if (str != null) {
			int len = str.length();
			if (len > 0) {
				StringBuilder sb = new StringBuilder(len);
				for (int i = 0; i < len; ++i) {
					char c = str.charAt(i);
					if (!Character.isWhitespace(c)) {
						sb.append(c);
					}
				}
				return sb.toString();
			}
		}
		return str;
	}
這個是最開始的思路。

實際測試了下,發現大部分情況上。要例如式二效率高。

可是在某些情況,比方"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaa",這樣的僅僅有一個空白字符的,效率要慢。

方式四:結合二,三。僅僅用System.arraycopy復制部分內存

另外一種方式,在調用deleteAt時。要整個拷貝後面的全部字符串。顯然在字符串非常長的情況下。效率會減少。於是考慮僅僅復制部分內存。

用兩種pos來標記哪一部分是連續的非空白字符串。

	static public String myTrimAllWhitespace3(String str) {
		if (str != null) {
			int len = str.length();
			if (len > 0) {
				char[] src = str.toCharArray();
				char[] dest = new char[src.length];

				int destPos = 0;
				for (int pos1 = 0, pos2 = 0; pos2 < src.length;) {
					if (Character.isWhitespace(src[pos2])) {
						if (pos1 == pos2) {
							pos1++;
							pos2++;
						} else {
							System.arraycopy(src, pos1, dest, destPos, pos2
									- pos1);
							destPos += (pos2 - pos1);
							pos2++;
							pos1 = pos2;
						}
					} else {
						pos2++;
					}

					if (pos2 == src.length) {
						if (pos1 != pos2) {
							System.arraycopy(src, pos1, dest, destPos, pos2
									- pos1);
							destPos += (pos2 - pos1);
						}
						return new String(dest, 0, destPos);
					}
				}
			}
		}
		return str;
	}

方式五:去掉StringBuilder。直接操作char[]

在寫完方式四。之後,測試發現效率在中間,和方式二,三相比,不好也不壞。似乎找到了一個平衡點。

可是忽然想到。既然在方式四中不直接操作char[]數組,為何不在方式二也這麽做?於是有了:

	static public String myTrimAllWhitespace2(String str) {
		if (str != null) {
			int len = str.length();
			if (len > 0) {
				char[] dest = new char[len];
				int destPos = 0;
				for (int i = 0; i < len; ++i) {
					char c = str.charAt(i);
					if (!Character.isWhitespace(c)) {
						dest[destPos++] = c;
					}
				}
				return new String(dest, 0, destPos);
			}
		}
		return str;
	}

第六點:Unicode

上面的幾種方式都僅僅能處理大部分的情況,對於部分Unicode字符串。可能會有問題。

由於本人對這個比較敏感,最後寫了個Unicode字符的處理:

	static public String myTrimAllWhitespace3(String str) {
		if (str != null) {
			int len = str.length();
			if (len > 0) {
				char[] src = str.toCharArray();
				char[] dest = new char[src.length];

				int destPos = 0;
				for (int pos1 = 0, pos2 = 0; pos2 < src.length;) {
					if (Character.isWhitespace(src[pos2])) {
						if (pos1 == pos2) {
							pos1++;
							pos2++;
						} else {
							System.arraycopy(src, pos1, dest, destPos, pos2
									- pos1);
							destPos += (pos2 - pos1);
							pos2++;
							pos1 = pos2;
						}
					} else {
						pos2++;
					}

					if (pos2 == src.length) {
						if (pos1 != pos2) {
							System.arraycopy(src, pos1, dest, destPos, pos2
									- pos1);
							destPos += (pos2 - pos1);
						}
						return new String(dest, 0, destPos);
					}
				}
			}
		}
		return str;
	}
這個處理Unicode的非常慢。

。Java的String類並沒有暴露足夠多的函數來處理Unicode,所以處理起來非常蛋疼。

總結:

測試代碼在:

https://gist.github.com/hengyunabc/a4651e90db24cc5ed29a

我的電腦上測試最快的代碼是方式五裏的。

可能在某些特殊情況下,方式四中用System.arraycopy來復制標記兩段內存會快點,但這個算法太復雜了,得不償失。

本人傾向於符合直覺。並且效率線性的算法。

給Spring提了個path,一開始是方式三的代碼,可是在某些情況下效率不高,導致周末心神不寧。。

於是就有了後面的幾種方式。

一個簡單的功能,直正實現起來卻也不easy。所以我盡量避免寫Util類和方式,由於保證代碼的質量,性能,不是一件easy的事。

https://github.com/spring-projects/spring-framework/pull/562

敲代碼非常難之去除字符串的空白字符