一文講解Java的String、StringBuffer和StringBuilder的使用與區(qū)別
提到 String、StringBuffer 和 StringBuilder,就不得不談及它們的歷史,在了解它們的歷史之后,我們對它們的理解將更上一級臺階!
發(fā)展歷史
String 與 StringBuffer 的出現(xiàn)
String 和 StringBuffer 在 Java1.0 中就已經(jīng)有了,目前也一直存在于各個 Java 版本之中,但是 StringBuilder 是在 Java5 中才被引入。我們都知道,String 是 Java 中的字符串類,是不可修改的,在 Java1.0 的時候,若要對字符串進行大量修改,應當使用 StringBuffer,它是可修改的,同時,當時的開發(fā)人員考慮到多個線程對一個字符串的修改可能出現(xiàn)線程不安全的問題,于是讓 StringBuffer 在擁有可修改字符串的功能的情況下,又給它加上了線程安全的機制??吹竭@里是不是覺得還挺好,挺正常的?但是要知道一個前提,那就是在 Java5 之前的 Java 在處理字符串的速度上一直被別人詬病,原因出在哪里?原因就在于這個 StringBuffer 上面。
被人詬病的 StringBuffer
StringBuffer 本來是為了實現(xiàn)大量修改字符串的功能而出現(xiàn)的,但卻因為 Java 的開發(fā)人員給它加了個線程安全的功能,導致它執(zhí)行效率極大地下降。這個線程安全的功能的實現(xiàn)并不是像我們現(xiàn)在用的方法,當時只是保證沒有異常拋出,程序可以正常運行下去而已。在 Java 中,要實現(xiàn)字符串的相加,用加法運算符將兩個字符串相加即可。但在這個過程中,Java5 之前是有 String 自動隱含地轉(zhuǎn)換成 StringBuffer,再進行操作這一個步驟的(畢竟 String 類不可直接修改)。只要有這些步驟,就可以實現(xiàn)字符串的修改,但是呢,StringBuffer 有個線程安全的功能,它會在上面提到的步驟中還額外的執(zhí)行一些功能,以保證線程的安全,而且,這里實現(xiàn)線程安全的方式和我們現(xiàn)在用鎖的方式是不一樣的!它這里的實現(xiàn)線程安全的方式極為繁瑣且復雜,這就大大降低了 StringBuffer 的執(zhí)行效率,以至于后來被廣大程序員詬病。
StringBuilder 的出現(xiàn)
我們仔細地想一下,實際上也并沒有多少地方需要在修改字符串的同時保證線程安全,就算有,我們給它加個鎖就行。基于這種想法,在 StringBuffer 出現(xiàn) 10 年之后,Java 的開發(fā)人員回過頭看這個問題,才發(fā)現(xiàn) StringBuffer 的實現(xiàn)是多么的愚蠢,于是后來在 Java5 就有了 StringBuilder。StringBuilder 同樣可以快速高效地修改字符串,同時不是線程安全的。雖然它不是線程安全的,但是它的執(zhí)行效率卻比 StringBuffer 要高上了不少。在 Java5 之后的版本中,字符串相加隱含的轉(zhuǎn)化過程中,不再將 String 轉(zhuǎn)化為 StringBuffer,而是轉(zhuǎn)化成 StringBuilder。
歷史故事講完了,下面正式開始講解它們各自的用法和特性。
String 類
String 類是 Java 用來存儲字符串的內(nèi)置類,在 String 初始化之后,其值就不可改變。
創(chuàng)建字符串
創(chuàng)建字符串的方式有兩種,直接創(chuàng)建和使用 new 關鍵字來創(chuàng)建
直接創(chuàng)建
String str = "Java";
直接創(chuàng)建的 String 類的數(shù)據(jù)存儲在公共的常量池中(Java 的常量優(yōu)化機制),即直接創(chuàng)建的相同值的不同 String 類的引用相同。
String str1 = "Java"; String str2 = "J" + "a" + "v" + "a"; System.out.println(str1 == str2); // Output: true
但下面這種情況要注意一下,它和上面的不同:
String str1 = "Java"; String str2 = "Java"; String str3 = str2 + ""; System.out.println(str1 == str2); // Output: true System.out.println(str2 == str3); // Output: false
為什么 str2 與 str3 不相等呢?實際上 str2 + "" 這一過程中,編譯器會隱含地將 str2 轉(zhuǎn)換成 StringBuilder(Java5 之前為 StringBuffer),然后再與 "" 相加,此過程會產(chǎn)生一個新的 StringBuilder 類,就是轉(zhuǎn)換而來的那個。這個新產(chǎn)生的 StringBuilder 再轉(zhuǎn)換為 String 類并賦值給變量 str3,因此,str3 的引用位置是 String 對象的堆上(與 new 關鍵字創(chuàng)建的 String 類相同),故與 str2 不相等。
new 關鍵字創(chuàng)建
String str = new String("Java");
通過 new 關鍵字創(chuàng)建的 String 和其他一般的類的創(chuàng)建一樣,數(shù)據(jù)是存儲在 String 類的對象的堆上,即通過 new 關鍵字創(chuàng)建的相同值的不同 String 類的引用不同。
String str1 = new String("Java"); String str2 = new String("Java"); System.out.println(str1 == str2); // Output: false
?String 創(chuàng)建方式的區(qū)別
通過 new 關鍵字創(chuàng)建的 String 類會調(diào)用 String 類的構(gòu)造方法,String 類的構(gòu)造方法有 11 種,除了通過字符串來創(chuàng)建 String 類之外,我們還可以用字符數(shù)組等方式來創(chuàng)建。
char[] s = {'J', 'a', 'v', 'a'}; String str = new String(s);
格式化字符串
在 Java 中,格式化字符串的方法有很多,比如有 C 語言風格的 printf,也有 Java 風格的 String.format。
System.out.printf("%f, %.2f, %s", Math.E, Math.PI, "Java");
雖然說是 C 語言風格,但還是略微有一點點不同,比如說,用 %d 來輸出浮點數(shù)在 C 語言里面編譯是不會報錯的,盡管輸出的結(jié)果不對,但是這在 Java 里面是會報錯的。
用 String 類的 format 方法也能格式化字符串,它可以看作是強化了的格式化字符串工具。
它既可以像 printf 那樣格式化字符串:
System.out.println(String.format("%f, %.2f, %s", Math.E, Math.PI, "Java"));
也有它自己獨特的方式:%[index]$[flag][type]
[index] 表示參數(shù)的索引,從 1 開始,因為 0 是格式化字符串;
[flag] 表示格式化的標識,有以下幾種:
- - :左對齊(默認是右對齊的),和長度限定一起使用;
- + :正數(shù)前加正號;
- 0 :不夠長度用 0 來補齊;
- # :對非十進制的數(shù)前加上標識;
- , :對于十進制整數(shù)每隔三位加一個逗號分隔符;
- ( :若為負數(shù),則用括號將其括起來;
- (空格字符):正數(shù)前空一格,負數(shù)沒有變化;
- m.nf :對于浮點數(shù),輸出長度為 m 位,保留小數(shù)點后 n 位;
- e/E :以科學計數(shù)法表示浮點數(shù);
- g/G :根據(jù)情況智能選擇以普通形式或者科學計數(shù)法輸出浮點數(shù);
- a/A :輸出帶有效位數(shù)和指數(shù)的十六進制浮點數(shù);
int i = 1234567890; System.out.println(String.format("%,d", i)); // Output: 1,234,567,890 System.out.println(String.format("%16d", i)); // Output: 1234567890 System.out.println(String.format("%016d", i)); // Output: 0000001234567890 System.out.println(String.format("%-16d", i)); // Output: 1234567890 System.out.println(String.format("% d", i)); // Output: 1234567890 System.out.println(String.format("% d", -i)); // Output: -1234567890 System.out.println(String.format("%+d", i)); // Output: +1234567890 System.out.println(String.format("%(d", -i)); // Output: (1234567890) System.out.println(String.format("%#x", i)); // Output: 0x499602d2 System.out.println(String.format("%x", i)); // Output: 499602d2 System.out.println(String.format("%o", i)); // Output: 11145401322 System.out.println(String.format("%b", i)); // Output: true double d = 3.1415926; System.out.println(String.format("%12.5f", d)); // Output: 3.14159 System.out.println(String.format("%g", d)); // Output: 3.14159 System.out.println(String.format("%e", d)); // Output: 3.141593e+00 System.out.println(String.format("%a", d)); // Output: 0x1.921fb4d12d84ap1
[type] 標識格式化的類型,有 d(整數(shù))、f(浮點數(shù))、s(字符串)、c(字符)、b(布爾值) 等。注意,這里的整數(shù)包括 int 類型和 long 類型,浮點數(shù)包括 float 類型和 double 類型。除了上面的基本類型外,還有幾種特殊的:x(十六進制)、o(八進制)。
int i = 1; long l = 1l; float f = 1.f; double d = 1.; char c = 'a'; String str = "Java"; String format = String.format("%6$s %5$c %4$f %3$f %2$d %1$d", i, l, f, d, c, str); System.out.println(format); // Output: Java a 1.000000 1.000000 1 1
關于 Java 中輸出整數(shù)的二進制的方法:
System.out.println(Integer.toBinaryString(123)); // Output: 1111011
輸出二進制不能用格式化輸出了,但可以用 Integer 類的 toBinaryString 方法將其轉(zhuǎn)化為二進制形式的字符串,然后再輸出即可。
這里再補充一些 Java 和其他語言不同的地方(方便 C/C++ 和 Python 的人熟悉 Java):
- C/C++ 的整數(shù)可以用單引號進行分隔,但是 Java 和 Python 不可以;
- Java 可以通過逗號格式字符串來達到整數(shù)每隔三位分隔的效果,C/C++ 和 Python 不可以;
- C/C++ 和 Python 可以將參數(shù)作為精度值并進行輸出,而 Java 不可以;
- C/C++、Java 和 Python 在十六進制、十進制、二進制上表示方法一樣,但在八進制上 Python 與 C/C++ 和 Java 表示方法不同;
C/C++:
int num = 1'234'567'890; int num = 0xabc; // 十六進制 int num = 123; // 十進制 int num = 0123; // 八進制 int num = 0b101; // 二進制 double d = 3.14; long long l = 112358; printf("%lf, %lld", d, f); // Output: 3.14, 112358 printf("%*.*f", 6, 3, 3.1415926); // Output: 3.142
Java:
int num1 = 0xabc; // 十六進制 int num2 = 123; // 十進制 int num3 = 0123; // 八進制 int num4 = 0b101; // 二進制 int num = 1234567890; System.out.println(String.format("%,d", num)); // Output: 1,234,567,890
Python:(下面的類型提示語法是為了讓讀者更容易理解)
num: int = 0xabc # 十六進制 num: int = 123 # 十進制 num: int = 0o123 # 八進制 num: int = 0b101 # 二進制 print('%*.*f' % (6, 3, 3.1415926)) # Output: 3.142
String 類的各種方法
常用方法
方法名 | 方法描述 |
int length() | 返回字符串的長度 |
char charAt(int index) | 返回字符串中索引為 index 處的字符 |
int indexOf(char c) | 返回第一個字符 c 的索引,若沒有則返回 -1 |
String concat(String str) | 返回原字符串和 str 連接后的新字符串 |
String[] split(char c) | 返回以字符 c 進行分割得到的字符串數(shù)組 |
boolean equals(Object anObject) | 將字符串與 anObject 進行比較,并返回比較結(jié)果 |
boolean startsWith(String str) | 測試字符串是否以 str 開頭,并返回測試結(jié)果 |
boolean endsWith(String str) | 測試字符串是否以 str 結(jié)尾,并返回測試結(jié)果 |
boolean contains(String str) | 判斷字符串中是否包含 str,并返回判斷結(jié)果 |
String substring(int start, int end) | 截取字符串,返回索引為 start 和索引為 end(不含)之間的字符串 |
String format(String fmt, Object... args) | 返回格式化后的字符串 |
String 與 char
眾所周知,String 的值是不可改變的,但是下面的代碼又是為什么呢?
String str = "Java"; System.out.println(str); // Output: Java str = "java"; System.out.println(str); // Output: java
上面的代碼表面看上去 String 類型的變量 str 的值被改變,其實沒有,因為 Java 中的變量都只是對象的引用。原來值為 "Java" 的 String 類型對象實際上還存在于內(nèi)存中,str = "java"; 只不過是將變量 str 的引用改到了常量 "java" 上面去了。
?String 類型不可變
實際上,String 就是一個 char 類型的數(shù)組,且 String 封裝的這個 char 數(shù)組是用 final 關鍵字修飾的,String 本身也被 final 修飾,因此無法被改變。所以 String 類型的方法只能返回一個修改原 String 的、新的 String 類型的變量,而無法對原值進行修改。要產(chǎn)生一個新的 String 必然要開辟新的內(nèi)存,這將花費不少的時間,因此只有在對 String 有少量修改的需求情況下,才使用 String 類,若要大量修改,那么還是需要 StringBuffer 類和 StringBuilder 類。
StringBuffer 與 StringBuilder
StringBuffer 和 StringBuilder 與 String 最大的不同之處在于,它們可以大量且頻繁地修改字符串的值而不產(chǎn)生新的字符串。StringBuffer 和 StringBuilder 最大的不同之處在于,StringBuffer 是線程安全的,而 StringBuilder 不是線程安全的,但 StringBuilder 相較于 StringBuffer 有速度優(yōu)勢,絕大多數(shù)情況下,推薦使用 StringBuilder。
繼承結(jié)構(gòu)
String 是直接繼承自 CharSequence,而 StringBuffer 和 StringBuilder 繼承自 AbstractStringBuilder,AbstractStringBuilder 又同時繼承自 CharSequence 和 Appendable。
繼承結(jié)構(gòu)
基本用法
這里以 StringBuilder 為例,介紹它的基本用法,StringBuffer 的用法與之類似,不同的地方會指出來的。
下面是 StringBuilder 的常用方法:
方法名 | 方法描述 |
append(String str) | 在字符串的后面添加字符串 str |
insert(int start, String str) | 在索引為 start 的位置插入字符串 str |
delete(int start, int end) | 將索引為 start 和索引為 end 之間的字符串刪除 |
replace(int start, int end, String str) | 將索引為 start 和索引為 end 之間的字符串替換為字符串 str |
reverse() | 反轉(zhuǎn)字符串本身 |
StringBuilder sb = new StringBuilder(10); sb.append("Java"); // sb: Java sb.insert(1, "java"); // sb: Jjavaava sb.delete(2, 3); // sb: Jjvaava sb.replace(4, 5, "JAVA"); // sb: JjvaJAVAva sb.reverse(); // sb: avAVAJavjJ
?StringBuilder 方法示意圖
與 String 的區(qū)別
在調(diào)用 String 類的 concat 方法(字符串相加)時,實際上是將兩個字符串相加并得到一個新的 String 類并返回,而并非對原 String 進行修改,而 StringBuffer 和 StringBuilder 是對自身的值直接進行修改的,速度和內(nèi)存的消耗誰更多顯而易見。
concat 方法與加法運算符
String.concat 方法和使用加號來連接字符串的結(jié)果都是得到連接后的字符串,但是這兩者有什么區(qū)別嗎?
String str1 = "Ja" + "va"; String str2 = "Ja".concat("va"); System.out.println(str1 == str2); // Output: false
無疑,str1 和 str2 的值是相等,但是兩者的引用地址不同,從前面的知識我們可以知道,str1 的內(nèi)存地址在公共的常量池中,上面的代碼就說明 str2 的內(nèi)存地址在 String 類的堆上。concat 方法實際上先復制了原來的字符串后再與新的字符串拼接,然后返回了一個新的字符串對象,所以地址不在公共常量池中,與 new 創(chuàng)建的字符串類似。
除 concat 和加法運算符之外,還有一種方式可以實現(xiàn)字符串的拼接,那就是 StringBuffer 類或 StringBuilder 類的 append 方法,實際上,加法運算符拼接字符串,轉(zhuǎn)換成 StringBuffer 或 StringBuilder 后再進行字符串拼接的操作就是使用 append 方法進行拼接。也就是說,加法拼接字符串的底層實際是調(diào)用 append 方法。
適用場景
String、StringBuffer 和 StringBuilder 用處不同,各有各的適用場景??偨Y(jié)如下:
類名稱 | 是否可修改 | 線程是否安全 | 相對速度比較 |
String | 否 | \ | 緩慢 |
StringBuffer | 是 | 是 | 一般 |
StringBuilder | 是 | 否 | 快速 |
綜上所述,當我們不需要對字符串做太多修改的時候,我們就選擇 String 類,當我們需要對字符串進行大量且頻繁的修改時,我們就選擇 StringBuilder 類,除非遇到了需要線程安全的情況。不過,就算遇到了需要線程安全的情況,仍然推薦使用 StringBuilder,因為 StringBuffer 的線程安全,僅僅是保證 Jvm 不拋出異常,順利地往下執(zhí)行而已,它并不能保證邏輯正確和調(diào)用順序正確。在大多數(shù)時候,我們需要的是鎖。但也可以偷懶使用 StringBuffer。
從 Java5 之后,用加號來連接字符串的時候,都會隱含地調(diào)用 StringBuilder 類,因此,大部分情況下用加號連接字符串的操作已經(jīng)沒有太多的性能損失,但并非絕對的。如果在有循環(huán)的情況下,編譯器可能無法完全做到智能地替換,這個時候我們還是自己手動使用 StringBuilder 類比較好。
到此這篇關于一文講解Java的String、StringBuffer和StringBuilder的使用與區(qū)別的文章就介紹到這了,更多相關Java String StringBuffer StringBuilder內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
解決使用redisTemplate高并發(fā)下連接池滿的問題
這篇文章主要介紹了解決使用redisTemplate高并發(fā)下連接池滿的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12微信公眾號開發(fā)之設置自定義菜單實例代碼【java版】
這篇文章主要介紹了微信公眾號開發(fā)之設置自定義菜單實例代碼,本實例是為了實現(xiàn)在管理后臺實現(xiàn)微信菜單的添加刪除管理。需要的朋友可以參考下2018-06-06spring batch使用reader讀數(shù)據(jù)的內(nèi)存容量問題詳解
這篇文章主要介紹了spring batch使用reader讀數(shù)據(jù)的內(nèi)存容量問題詳解,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-07-07java實現(xiàn)excel自定義樣式與字段導出詳細圖文教程
最近接到一個需求,客戶不滿意原本導出的csv文件,想要導出Excel文件,下面這篇文章主要給大家介紹了關于java實現(xiàn)excel自定義樣式與字段導出詳細圖文教程2023-09-09在Java的Spring框架的程序中使用JDBC API操作數(shù)據(jù)庫
這篇文章主要介紹了在Java的Spring框架的程序中使用JDBC API操作數(shù)據(jù)庫的方法,并通過示例展示了其存儲過程以及基本SQL語句的應用,需要的朋友可以參考下2015-12-12