小瓜牛漫談 — String


 

String 類在 Java 中代表字符串。Java 程序中的所有字符串字面值(如 "abc" )都作為此類的實例實現。

1 public static void main(String[] args) {
2     
3     String str1 = "abc";
4     String str2 = new String("cde");
5     System.out.println(str1);
6     System.out.println(str2);
7 }

在上面代碼中, 第 4 行實際上創建了兩個 String 對象, 一個是 "cde" 本身, 另外一個則是由 new 關鍵字為對象申請開辟的內存空間。

[ 可結合文章最下面給出的第 10 條來理解 ]

通常, 使用 String(String original) 構造器來創建 String 對象要比直接使用字符串字面值的開銷更加的大。

 

String 字符串是常量, 它們的值在創建之后不能夠被更改:

 1 package net.yeah.fancydeepin.string;
 2 
 3 public class Application {
 4 
 5     public static void main(String[] args) {
 6         
 7         String str = "abc";
 8         str += "cde";
 9         System.out.println(str);
10     }
11 }

當程序運行時, JVM 內存中的分配看起來應該像:

從上面的圖來看, str 最初引用的是 "abc" 對象, 最終打印輸出的結果是 abccde, 這並不是說 str 所引用的對象的內容發生了變化,

而是 str 在執行的過程中重新引用了另外的一個 String 對象 "abccde"。

 

可以使用 java 自帶的反編譯工具 javap 來查看編譯后的字節碼文件信息: javap -c Appliaction

從上面的圖來看:

第 0 行, 將常量池中的 "abc" 對象壓棧;

第 8 行, 調 String.valueOf(Object obj) [ 實際上是將 str 轉成了 String 對象 ];

第 3、11 行, 是在創建 StringBuilder 對象, 通過 StringBuilder(String str) 構造器 [ 參數是第 8 行的 String 對象 ];

第 14 行, 將常量池中的 "cde" 對象壓棧;

第 16 行, 調 StringBuilder 的 append 方法 [ 將 "cde" 拼在 "abc" 的后面 ];

第 19 行, 調 StringBuilder 的 toString() 方法。

 

使用 jad 工具, 可以更加容易的去讀懂編譯后的字節碼文件內容: jad -o -a -s .java Application.class

結合上面的圖可以看出, 在 java 中, 通過使用 "+" 符號來串聯字符串的時候, 實際上底層會轉成通過 StringBuilder 實例的 append() 方法來實現。

[ 關於對 String 類使用 "+" 符號來串聯字符串, 在文章最下面的第 10 條繼續來補充。 ]

 

String 類常用方法:

 

1> startsWith(String prefix)、endsWith(String suffix)

startsWith(prefix) 測試字符串是否是以指定的前綴 prefix 開始, endsWith(suffix) 測試字符串是否是以指定的后綴 suffix 結束:

1 public static void main(String[] args) {
2     
3     String url = "/small-snail/archive/20130421.html";
4     System.out.println(url.startsWith("/small-snail/archive/")); //true
5     System.out.println(url.endsWith(".html")); //true
6     System.out.println(url.startsWith("/small-snail/category/")); //false
7     System.out.println(url.endsWith(".php")); //false
8 }

 

2> equals(Object anObject)

在 java 中, Object 是一個頂級類, 所有類都直接或間接或默認的繼承了該類。

Object 類有一個 equals(Object obj) 方法, 因此, 所有類都默認的擁有了這個方法。

但 Object 的 equals(obj) 方法默認比較的是兩個引用變量所引用的對象是否相同, 只有當兩個引用變量引用了相同的一個對象的時候才會返回 true。

String 類重寫了 Object 類的此方法, String 類的 equals 方法比較的是兩個 String 對象的內容是否相同。 

 1 package net.yeah.fancydeepin.string;
 2 
 3 public class Application {
 4 
 5     public static void main(String[] args) {
 6 
 7         String kitty1 = new String("HelloKitty");
 8         String kitty2 = new String("HelloKitty");
 9         StringBuilder kitty3 = new StringBuilder("HelloKitty");
10         System.out.println(kitty1.equals(kitty2));  //true
11         System.out.println(kitty1.equals(kitty3));  //false
12     }
13 }

第 11 行, 雖然 kitty3 的內容與 kitty1 的內容一樣, 但由於 kitty3 不是一個 String 對象, 因此調 equals 方法的返回值為 false。

 

下面附上 String 類的 equals 方法的源碼:

 

3> equalsIgnoreCase(String anotherString)

比較兩個 String 對象的內容是否相同, 忽略大小寫。

1 public static void main(String[] args) {
2      
3     String param1 = "helloKitty";
4     String param2 = "HelloKitty";
5     System.out.println(param1.equals(param2));  //false
6     System.out.println(param1.equalsIgnoreCase(param2));  //true 
7 }

 

4> getBytes(String charsetName)、String(byte[] bytes)

getBytes(String charsetName) 是使用指定的字符集 charset 將此 String 編碼為 byte 序列,並將結果存儲到一個新的 byte 數組中。

 1 package net.yeah.fancydeepin.string;
 2 
 3 import java.io.UnsupportedEncodingException;
 4 
 5 public class Application {
 6 
 7     public static void main(String[] args) throws UnsupportedEncodingException {
 8 
 9         String param = "哈嘍Kitty";
10         String charset;
11         
12         charset = new String(param.getBytes("GBK")); //亂碼
13         System.out.println(charset);
14         
15         charset = new String(param.getBytes("GB2312")); //亂碼
16         System.out.println(charset);
17         
18         charset = new String(param.getBytes("ISO-8859-1")); //亂碼
19         System.out.println(charset);
20         
21         charset = new String(param.getBytes("UTF-8")); //正常
22         System.out.println(charset);
23         
24         charset = new String(param.getBytes("UTF-16")); //亂碼
25         System.out.println(charset);
26     }
27 }

上面給出來的是 java 開發過程中比較經常遇到的字符集編碼。當一個字符串中含有中文字符的時候, 如果該字符串在編碼和解碼前后所使用的字符編碼不一致,

就會導致中文亂碼的問題。

由於我的 eclipse SDK 工作區所使用的是 UTF-8 編碼, 所以上面只有 "UTF-8" 字符編碼輸出的內容是正常的, 其他情況就會出現中文亂碼的問題。


5> getBytes(String charsetName)、String(byte[] bytes, String charsetName)

上面示例中出現了中文亂碼問題, 其實現在網絡上關於中文亂碼這點事兒, 資料已經是非常的多了。下面接下來將首先模擬出一個中文亂碼的問題, 然后來解決它:

 1 package net.yeah.fancydeepin.string;
 2 
 3 import java.io.UnsupportedEncodingException;
 4 
 5 public class Application {
 6 
 7     public static void main(String[] args) throws UnsupportedEncodingException {
 8 
 9         String param = "哈嘍Kitty";
10         
11         param = new String(param.getBytes("UTF-8"), "ISO-8859-1"); //中文亂碼
12         System.out.println(param);
13         
14         //解決中文亂碼
15         param = new String(param.getBytes("ISO-8859-1"), "UTF-8"); //恢復正常
16         System.out.println(param);
17     }
18 }

param = new String(param.getBytes("UTF-8"), "ISO-8859-1"); 意思是說:

將 param 以 UTF-8 編碼方式去編碼, 然后再按 ISO-8859-1 編碼方式去解碼編碼后的內容, 來構造一個新的 String 對象 param。

由於 param 原本是按 UTF-8 編碼方式編碼出來的, 現在卻使用 ISO-8859-1 編碼方式去解碼, 這個時候出現中文亂碼是很正常的事情。

再者, ISO-8859-1 的編碼方式本身是不支持中文的。

解決中文亂碼問題, 無非就是使用正確的字符集編碼去解碼字符串的內容:

param = new String(param.getBytes("ISO-8859-1"), "UTF-8");

首先是將 param 以 ISO-8859-1 的編碼方式編碼出來, 因為在 java 的 JVM 中, 任何 String 都是一個 unicode 字符串,

接着再使用 UTF-8 去解碼, 這個時候的中文就不再是亂碼啦。。

 

為了避免引起誤解, 補充說明一下, 上面不是一定要使用與 IDE 相同的編碼方式 UTF-8 才不會引起中文亂碼, 實際上也可以換成 GBK、GB2312 等兼容中文的

編碼方式也是可以的, 只需要保證編碼和解碼使用的是相同的字符集編碼方式即可。

 

6> indexOf(String str)、lastIndexOf(String str)、substring(int beginIndex, int endIndex)

indexOf 用於返回指定的子字符串在主字符串中第一次出現處的索引值; lastIndexOf 用於返回指定的子字符串在主字符串中最后一次出現處的索引值。

substring 則是用來切割主字符串, 根據開始索引值和結束索引值切割並返回一個新字符串。

 1 package net.yeah.fancydeepin.string;
 2 
 3 public class Application {
 4 
 5     public static void main(String[] args) {
 6          
 7         String param = "archive.logo.ico";
 8         int firstIndex = param.indexOf("a");
 9         int lastIndex = param.lastIndexOf("o");
10         int length = param.length();
11         System.out.println(firstIndex);  // 0
12         System.out.println(lastIndex);   // 15
13         System.out.println(length);      // 16
14         System.out.println(param.substring(firstIndex, lastIndex)); // archive.logo.ic
15         param = param.substring(param.lastIndexOf("."), length);    // .ico
16         System.out.println(param);
17     }
18     
19 }

從上面代碼可以看出, 索引值是從 0 開始的, substring(beginIndex, endIndex) 方法切割字符串的區間其實是左閉右開: [ beginIndex, endIndex )

 

7> replaceAll(String regex, String replacement)

用子字符串 replacement 來替換主字符串中所有由正則表達式 regex 匹配的子字符串。

 1 package net.yeah.fancydeepin.string;
 2 
 3 public class Application {
 4 
 5     public static void main(String[] args) {
 6          
 7         String packageName = Application.class.getPackage().getName();
 8         
 9         String packagePath1 = packageName.replaceAll(".", "/");  //將所有的字符換成了'/'
10         System.out.println(packagePath1); // 打印 ///////////////////////////
11         
12         String packagePath2 = packageName.replaceAll("\\.", "/");  //將所有的'.'換成'/'
13         System.out.println(packagePath2);  // net/yeah/fancydeepin/string
14         
15         String packagePath3 = packageName.replaceAll("e+", "E");  //凡是出現'e'一次或以上的用'E'替換
16         System.out.println(packagePath3);  // nEt.yEah.fancydEpin.string
17     }
18     
19 }

上面代碼中需要注意的是 packagePack1, replaceAll 的第一個參數使用的是正則表達式, 正則表達式中的 '.' 可以匹配除“\n”之外的任何單個字符,

因此 packagePath1 打印輸出的全是反斜杠'/', 如果要匹配'.', 則應該使用轉義字符, 像上面代碼中的 packagePath2。

 

8> split(String regex)

根據給定的正則表達式 regex 將主字符串拆分成一個字符串數組。

 1 package net.yeah.fancydeepin.string;
 2 
 3 public class Application {
 4 
 5     public static void main(String[] args) {
 6          
 7         String param = "Java,Android,PHP,C,C++,C#";
 8         String[] languages = param.split(",");
 9         for(String language : languages){
10             System.out.println(language);
11         }
12     }
13     
14 }

 

9> trim()

忽略字符串的前導空白和尾部空白。

1 public static void main(String[] args) {
2      
3     String param = "  Hello Kitty  ";
4     System.out.println(param.trim());  //Hello Kitty
5 }

從上面示例可以看出, 調 trim() 方法只是會忽略字符串的前導空白和尾部空白, 對於串中間的空白是不會被處理的。

 

10> intern()

java 在運行期間會維護一個常量池 ( 運行時常量池, Runtime Constant Pool ), 用來存放編譯期生成的各種字面量和符號引用。

首先是先來一個常量池的小例子:

1 public static void main(String[] args) {
2      
3     String param1 = "Rose";
4     String param2 = "Rose";
5     String param3 = "Ro" + "se";
6     System.out.println(param1 == param2);  //true
7     System.out.println(param1 == param3);  //true
8 }

以上示例代碼中, param1、param2 的值都是字符串常量, 它們在編譯期間就能夠被確定了的。 

java 虛擬機在載入 class 類文件信息的時候, 會確保字符串常量在 class 文件常量池中只存在一份拷貝。

載入后的 class 類文件信息就存放在方法區(永久代)的運行時常量池當中。

param1 == param2 為 true。因為只存在一份拷貝, 實際上 param1 與 param2 引用的是同一個 String 對象 "Rose", 因此 param1 == param2。

再者, String 是 final 類, 也就是不可變類, 不可變類有一個重要的特性, 那就是可以被共享。

至於 param3, 由於 "Ro" 與 "se" 都是字符串常量, 當一個 String 對象是由多個字符串常量連接而成的時候, 那么, 它在編譯期間也是可以被確定的,

因此也是一個字符串常量。下面來查看一下編譯后的字節碼文件信息: javap -c Appliaction

上面是示例代碼反編譯后的字節碼指令部分截圖, 字節碼指令看不懂沒有關系, 很明顯能看到3個 "Rose", 故 param3 也是一個字符串常量,

因此 param1 == param3 也為 true。

 

java 在處理 String 對象 "+" 符號串聯字符串的時候, 有一個很微小的差異, 先上代碼:

1 public static void main(String[] args) {
2     
3     String param1 = "Ro";
4     String param2 = "Ro";
5     String param3 = "Ro" + "se";
6 
7     param1 += "se";
8     param2 = param2 + "se";
9 }

從整體上來看, param1、param2、param3 都是在使用 "+" 符號串聯字符串, 但是 java 底層在處理方式上卻存在很大的不同:

 

javap -c Appliaction

 

jad -o -a -s .java Application.class

同樣是使用 "+" 符號來串聯字符串, 從上面可以看出, param1 與 param2 的處理方式是一樣的, 底層會轉成使用 StringBuilder 的 append() 方法

來實現, 唯獨 param3 在底層沒有作轉換。

這是因為 param3 在聲明的時候賦值, 並且由於所串聯的字符串是兩個字符串常量, 因此 param3 在編譯期也能夠被確定是一個字符串常量。

param1 與 param2 相似的, 在編譯期也能被確定是字符串常量, 只是在貼出來的源碼的第 7 和 8 行, 重新的改變了 param1 和 param2 的引用,

而第 7 和 8 行是在編譯期不能夠被確定的, 只有在運行期間才能夠被確定, 而在運行期, 凡是使用 "+" 符號來串聯字符串, java 底層會將其轉成使用

StringBuilder 的 append() 方法來實現。

 

以上已經引入了常量池的概念。但在 java 中, 並不要求常量一定只能在編譯期間產生, 運行期間也可以將常量放入池中。例如 String 類的 intern() 方法:

 1 public static void main(String[] args) {
 2      
 3     String param1 = "Rose";
 4     String param2 = new String("Rose");
 5     String param3 = new String("Rose");
 6     
 7     System.out.println(param1 == param2);  //false
 8     System.out.println(param1 == param3);  //false
 9     
10     param2.intern();
11     param3 = param3.intern();
12     
13     System.out.println(param1 == param2);  //false
14     System.out.println(param1 == param3);  //true
15     System.out.println(param1 == param2.intern());  //true
16 }

當對 String 對象調用 intern() 方法時, 如果池中已經包含了一個等於此 String 對象的字符串(兩個 String 對象調 equals 返回值為 true),

則返回池中的字符串對象的引用。否則, 將此 String 對象添加到池中, 並返回此 String 對象的引用。

上示代碼中, 第7,8行容易理解, 因為 param1 引用的是池中的對象, param2 和 param3 引用的是堆中的兩個不同的對象, 因此都為 false。

第13行, 由於第10行 param2 只是調了 intern() 方法, 並沒有用 param2 來存儲返回值, 因此 param2 引用的還是堆中的對象。因此為 false。

第14行, 由於第11行 param3 調了 intern(), 並使用 param3 本身來接收了返回值, 因此 param3 改變了指向, 去引用了池中的對象, 因此為 true。

第15行, 和上面類似的去分析, param1 引用的是池中的 "Rose" 對象, 而 param2.intern() 返回的也是池中的 "Rose" 對象的引用, 因此為 true。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM