作者 | 沉默王二
責編 | 屠敏
頭圖 | CSDN 下載自視覺中國
String 可以說是 Java 中最常見的數據類型,用來表示一串文本,它的使用頻率非常高,為了小夥伴們著想,我怒肝了一周,把字符串能寫的全都寫了出來。
來看一下腦圖吧,感受一下這份手冊涉及到的知識點,不是我吹,有了這份手冊,字符串的相關知識可以說全部掌握了。
多行字符串
每個作業系統對換行符的定義都不盡相同,所以在拼接多行字符串之前,需要先獲取到作業系統的換行符,Java 可以通過下面的方式獲取:
String newLine = System.getProperty("line.separator");
通過 System 類的 getProperty() 方法,帶上「line.separator」關鍵字就可以獲取到了。
有了換行符,就可以使用 String 類的 concat() 方法或者直接使用「+」號操作符拼接多行字符串了。
String mutiLine = "親愛的" .concat(newLine) .concat("我想你了") .concat(newLine) .concat("你呢?") .concat(newLine) .concat("有沒有在想我呢?");
String mutiLine1 = "親愛的"+ newLine + "你好幼稚啊" + newLine + "技術文章裡" + newLine + "你寫這些合適嗎";
Java 8 的 String 類加入了一個新的方法 join(),可以將換行符與字符串拼接起來,非常方便:
String mutiLine2 = String.join(newLine, "親愛的", "合適啊", "這叫趣味", "哈哈");
StringBuilder 當然也是合適的:
String mutiLine3 = new StringBuilder() .append("親愛的") .append(newLine) .append("看不下去了") .append(newLine) .append("肉麻") .toString();
StringBuffer 類似,就不再舉例了。另外,Java 還可以通過 Files.readAllBytes() 方法從源文件中直接讀取多行文本,格式和源文件保持一致:
String mutiLine4 = newString(Files.readAllBytes(Paths.get("src/main/resource/cmower.txt")));
檢查字符串是否為空
說到「空」這個概念,它在編程中有兩種定義,英文單詞分別是 empty 和 blank,來做一下區分。如果字符串為 null,或者長度為 0,則為 empty;如果字符串僅包含空格,則為 blank。
01、empty
Java 1.6 之後,String 類新添加了一個 empty() 方法,用於判斷字符串是否為 empty。
booleanisEmpty(String str){return str.isEmpty();}
為了確保不拋出 NPE,最好在判斷之前先判空,因為 empty() 方法只判斷了字符串的長度是否為 0:
所以我們來優化一下 isEmpty() 方法:
booleanisEmpty(String str){return str != null || str.isEmpty();}
02、blank
如果想檢查字符串是否為 blank,有一種變通的做法,就是先通過 String 類的 trim() 方法去掉字符串兩側的空白字符,然後再判斷是否為 empty:
booleanisBlank(String str){return str != null || str.trim().isEmpty();}
03、第三方類庫
在實際的項目開發當中,檢查字符串是否為空最常用的還是 Apache 的 commons-lang3 包,有各式各樣判空的方法。
更重要的是,可以省卻判 null 的操作,因為 StringUtils 的所有方法都是 null 安全的。
生成隨機字符串
有時候,我們需要生成一些隨機的字符串,比如說密碼。
int leftLimit = 97; // 'a'int rightLimit = 122; // 'z'int targetStringLength = 6;Random random = new Random();StringBuilder buffer = new StringBuilder(targetStringLength);for (int i = 0; i < targetStringLength; i++) {int randomLimitedInt = leftLimit + (int) (random.nextFloat() * (rightLimit - leftLimit + 1)); buffer.append((char) randomLimitedInt);}String generatedString = buffer.toString();System.out.println(generatedString);
這段代碼就會生成一串 6 位的隨機字符串,範圍是小寫字母 a - z 之間。
除了使用 JDK 原生的類庫之外,還可以使用 Apache 的 Commons Lang 包,RandomStringUtils.random() 方法剛好滿足需求:
int length = 6;boolean useLetters = true;// 不使用數字boolean useNumbers = false;String generatedString = RandomStringUtils.random(length, useLetters, useNumbers);System.out.println(generatedString);
刪除字符串最後一個字符
刪除字符串最後一個字符,最簡單的方法就是使用 substring() 方法進行截取,0 作為起始下標,length() - 1 作為結束下標。
不管怎麼樣,substring() 方法不是 null 安全的,需要先判空:
publicstatic String removeLastChar(String s) {return (s == null || s.length() == 0) ? null : (s.substring(0, s.length() - 1)); }
如果不想在操作之前判空,那麼就直接上 Apache 的 Commons Lang 包:
String s = "沉默王二";StringUtils.substring(s, 0, s.length() - 1);
當然了,如果目的非常明確——就是只刪除字符串的最後一個字符,還可以使用 StringUtils 類的 chop() 方法:
StringUtils.chop(s);
如果你看過源碼的話,你就會發現,它內部其實也是調用了 substring()方法。publicstatic String chop(final String str){if (str == null) {returnnull; }finalint strLen = str.length();if (strLen < 2) {return EMPTY; }finalint lastIdx = strLen - 1;final String ret = str.substring(0, lastIdx);finalchar last = str.charAt(lastIdx);if (last == CharUtils.LF && ret.charAt(lastIdx - 1) == CharUtils.CR) {return ret.substring(0, lastIdx - 1); }return ret;}
如果你對正則表達式了解的話,也可以使用 replaceAll() 方法進行替換,把最後一個字符 .$ 替換成空字符串就可以了。
s.replaceAll(".$", "")
當然了,replaceAll() 方法也不是 null 安全的,所以要提前判空:
String result= (s == null) ? null : s.replaceAll(".$", "");
如果對 Java 8 的 Lambda 表達式和 Optional 比較熟的話,還可以這樣寫:
String result1 = Optional.ofNullable(s) .map(str -> str.replaceAll(".$", "")) .orElse(s);
看起來就顯得高大上多了,一看就是有經驗的 Java 程式設計師。
統計字符在字符串中出現的次數
要統計字符在字符串中出現的次數,有很多方法,直接使用 JDK 的 API 就是最直接的一種:
String someString = "chenmowanger";char someChar = 'e';int count = 0;for (int i = 0; i < someString.length(); i++) {if (someString.charAt(i) == someChar) { count++; }}System.out.println(count);
這種方式很直白,但有沒有更優雅的呢?有,Java 8 就優雅多了:
long count = someString.chars().filter(ch -> ch == 'e').count();
如果想使用第三方類庫的話,可以繼續選擇 Apache 的 Commons Lang 包:
int count2 = StringUtils.countMatches("chenmowanger", "e");
也非常優雅,很容易看得懂。
拆分字符串
大多數情況下,String 類的 split() 方法就能夠滿足拆分字符串的需求:
String[] splitted = "沉默王二,一枚有趣的程式設計師".split(",");
當然了,該方法也不是 null 安全的,那想要 null 安全,小夥伴們應該能想到誰了吧?
之前反覆提到的 StringUtils 類,來自 Apache 的 Commons Lang 包:
String[] splitted = StringUtils.split("沉默王二,一枚有趣的程式設計師", ",");
字符串比較
對於初學者來說,最容易犯的錯誤就是使用「==」操作符來判斷兩個字符串的值是否相等,這也是一道很常見的面試題。
String string1 = "沉默王二";String string2 = "沉默王二";String string3 = newString("沉默王二");System.out.println(string1 == string2);System.out.println(string1 == string3);
這段程序的第一個結果是 true,第二個結果為 false,這是因為使用 new 關鍵字創建的對象和使用雙引號聲明的字符串不是同一個對象,而「==」 操作符是用來判斷對象是否相等的。
如果單純的比較兩個字符串的值是否相等,應該使用 equals() 方法:
String string1 = "沉默王二";String string2 = "沉默王二";String string3 = newString("沉默王二");System.out.println(string1.equals(string2));System.out.println(string1.equals(string3));
這段程序輸出的結果就是兩個 true,因為 equals() 方法就是用來單純的判斷字符串的值是否相等。
字符串拼接
01、「+」號操作符
要說姿勢,「+」號操作符必須是字符串拼接最常用的一種了,沒有之一。
String chenmo = "沉默";String wanger = "王二";System.out.println(chenmo + wanger);
我們把這段代碼使用 JAD 反編譯一下。
String chenmo = "\u6C89\u9ED8"; // 沉默String wanger = "\u738B\u4E8C"; // 王二System.out.println((new StringBuilder(String.valueOf(chenmo))).append(wanger).toString());
我去,原來編譯的時候把「+」號操作符替換成了 StringBuilder 的 append 方法。也就是說,「+」號操作符在拼接字符串的時候只是一種形式主義,讓開發者使用起來比較簡便,代碼看起來比較簡潔,讀起來比較順暢。算是 Java 的一種語法糖吧。
02、StringBuilder
除去「+」號操作符,StringBuilder 的 append 方法就是第二個常用的字符串拼接姿勢了。
先來看一下 StringBuilder 類的 append 方法的源碼:
public StringBuilder append(String str) {super.append(str);returnthis;}
這 3 行代碼沒啥可看的,可看的是父類 AbstractStringBuilder 的 append 方法:
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;returnthis;}
1)判斷拼接的字符串是不是 null,如果是,當做字符串「null」來處理。appendNull 方法的源碼如下:
private AbstractStringBuilder appendNull() {int c = count; ensureCapacityInternal(c + 4); final char[] value = this.value;value[c++] = 'n';value[c++] = 'u';value[c++] = 'l';value[c++] = 'l'; count = c;returnthis;}
2)拼接後的字符數組長度是否超過當前值,如果超過,進行擴容並複製。ensureCapacityInternal 方法的源碼如下:
privatevoidensureCapacityInternal(int minimumCapacity) {// overflow-conscious codeif (minimumCapacity - value.length > 0) {value = Arrays.copyOf(value, newCapacity(minimumCapacity)); }}
3)將拼接的字符串 str 複製到目標數組 value 中。
str.getChars(0, len, value, count)
03、StringBuffer
先有 StringBuffer 後有 StringBuilder,兩者就像是孿生雙胞胎,該有的都有,只不過大哥 StringBuffer 因為多呼吸兩口新鮮空氣,所以是線程安全的。
publicsynchronized StringBuffer append(String str){ toStringCache = null;super.append(str);returnthis;}
StringBuffer 類的 append 方法比 StringBuilder 多了一個關鍵字 synchronized,可暫時忽略 toStringCache = null。
synchronized 是 Java 中的一個非常容易臉熟的關鍵字,是一種同步鎖。它修飾的方法被稱為同步方法,是線程安全的。
04、String 類的 concat 方法
單就姿勢上來看,String 類的 concat 方法就好像 StringBuilder 類的 append。
String chenmo = "沉默";String wanger = "王二";System.out.println(chenmo.concat(wanger));
文章寫到這的時候,我突然產生了一個奇妙的想法。假如有這樣兩行代碼:
chenmo += wangerchenmo = chenmo.concat(wanger)
它們之間究竟有多大的差別呢?
之前我們已經了解到,chenmo += wanger 實際上相當於 (new StringBuilder(String.valueOf(chenmo))).append(wanger).toString()。
要探究「+」號操作符和 concat 之間的差別,實際上要看 append 方法和 concat 方法之間的差別。
append 方法的源碼之前分析過了。我們就來看一下 concat 方法的源碼吧。
public String concat(String str) {int otherLen = str.length();if (otherLen == 0) {returnthis; }int len = value.length;char buf[] = Arrays.copyOf(value, len + otherLen); str.getChars(buf, len);returnnew String(buf, true);}
1)如果拼接的字符串的長度為 0,那麼返回拼接前的字符串。
if (otherLen == 0) {returnthis;}
2)將原字符串的字符數組 value 複製到變量 buf 數組中。
char buf[] = Arrays.copyOf(value, len + otherLen);
3)把拼接的字符串 str 複製到字符數組 buf 中,並返回新的字符串對象。
str.getChars(buf, len);returnnewString(buf, true);
通過源碼分析我們大致可以得出以下結論:
1)如果拼接的字符串是 null,concat 時候就會拋出 NullPointerException,「+」號操作符會當做是「null」字符串來處理。
2)如果拼接的字符串是一個空字符串(""),那麼 concat 的效率要更高一點。畢竟不需要 new StringBuilder 對象。
3)如果拼接的字符串非常多,concat 的效率就會下降,因為創建的字符串對象越多,開銷就越大。
注意了!!!
弱弱地問一下啊,還有在用 JSP 的同學嗎?EL 表達式中是不允許使用「+」操作符來拼接字符串的,這時候就只能用 concat 了。
${chenmo.concat('-').concat(wanger)}
05、String 類的 join 方法
JDK 1.8 提供了一種新的字符串拼接姿勢:String 類增加了一個靜態方法 join。
String chenmo = "沉默";String wanger = "王二";String cmower = String.join("", chenmo, wanger);System.out.println(cmower);
第一個參數為字符串連接符,比如說:
String message = String.join("-", "王二", "太特麼", "有趣了");
輸出結果為:王二-太特麼-有趣了
我們來看一下 join 方法的源碼:
publicstatic String join(CharSequence delimiter, CharSequence... elements) { Objects.requireNonNull(delimiter); Objects.requireNonNull(elements);// Number of elements not likely worth Arrays.stream overhead. StringJoiner joiner = new StringJoiner(delimiter);for (CharSequence cs: elements) { joiner.add(cs); }return joiner.toString();}
發現了一個新類 StringJoiner,類名看起來很 6,讀起來也很順口。StringJoiner 是 java.util 包中的一個類,用於構造一個由分隔符重新連接的字符序列。限於篇幅,本文就不再做過多介紹了,感興趣的同學可以去了解一下。
06、StringUtils.join
實戰項目當中,我們處理字符串的時候,經常會用到這個類——org.apache.commons.lang3.StringUtils,該類的 join 方法是字符串拼接的一種新姿勢。
String chenmo = "沉默";String wanger = "王二";StringUtils.join(chenmo, wanger);
該方法更善於拼接數組中的字符串,並且不用擔心 NullPointerException。
StringUtils.join(null) = nullStringUtils.join([]) = ""StringUtils.join([null]) = ""StringUtils.join(["a", "b", "c"]) = "abc"StringUtils.join([null, "", "a"]) = "a"
通過查看源碼我們可以發現,其內部使用的仍然是 StringBuilder。
publicstatic String join(final Object[] array, String separator, final int startIndex, final int endIndex) {if (array == null) {returnnull; }if (separator == null) { separator = EMPTY; }final StringBuilder buf = new StringBuilder(noOfItems * 16);for (int i = startIndex; i < endIndex; i++) {if (i > startIndex) { buf.append(separator); }if (array[i] != null) { buf.append(array[i]); } }return buf.toString();}
大家讀到這,不約而同會有這樣一種感覺:我靠(音要拖長),沒想到啊沒想到,字符串拼接足足有 6 種姿勢啊,晚上回到家一定要一一嘗試下。
07、為什麼阿里開發手冊不建議在 for 循環中使用」+」號操作符進行字符串拼接
來看兩段代碼。
第一段,for 循環中使用」+」號操作符。
String result = "";for (int i = 0; i < 100000; i++) { result += "六六六";}
第二段,for 循環中使用 append。
StringBuilder sb = new StringBuilder();for (int i = 0; i < 100000; i++) { sb.append("六六六");}
這兩段代碼分別會耗時多長時間呢?在我的 iMac 上測試出的結果是:
1)第一段代碼執行完的時間為 6212 毫秒
2)第二段代碼執行完的時間為 1 毫秒
差距也太大了吧!為什麼呢?
我相信有不少同學已經有了自己的答案:第一段的 for 循環中創建了大量的 StringBuilder 對象,而第二段代碼至始至終只有一個 StringBuilder 對象。