天天看點

Core Java 總結(字元和字元串類問題)

所有代碼均在本地編譯運作測試,環境為 Windows7 32位機器 + eclipse Mars.2 Release (4.5.2)

2016-10-17 整理

  • 字元,字元串類問題
  • 正規表達式問題
  • Java字元編碼問題
  • 字元串記憶體問題

簡述String和StringBuffer、StringBuilder的差別?

比較初級的一個題目,而初級題目又是除高端職位外,筆試題海量篩人的首選,但是作為經典題目,還是入選了我的筆記,因為它能延伸的Java字元串的問題太多了……另一種延伸的高端問法就是套路你,因為這個題100%會回答多線程的知識點:前者是線程安全的,後者是非線程安全的。然後他們就問什麼是線程,或者說程序和線程的差別,為什麼有線程?它如何實作的線程安全,接下來稍微高端的就引入Java并發的問題……呵呵。閑言少叙,看幾個此類不同難度的系列問題。

下面的代碼發生了什麼?

String s = "abcd";
s = s.concat("ef");      

String是final的,字元串對象内部是用final的字元數組存儲的,故String是有字面量這一說法的,這是其他類型所沒有的特性(除原生類型)。另外,java中也有字元串常量池這個說法,用來存儲字元串字面量。可以畫一個圖表示:

Core Java 總結(字元和字元串類問題)

String 類的操作本質是産生了新的 String 對象,給人假象:好像是字元串被改變了似的。

String和StringBuffer、StringBuilder三者的類圖(或者選擇題:類的關系)是怎樣的?

我們先要記住:

Core Java 總結(字元和字元串類問題)

String、StringBuffer、StringBuilder 都實作了 CharSequence 接口,内部都是用一個char數組實作,雖然它們都與字元串相關,但是其處理機制不同。

  • String:是不可改變的,也就是建立後就不能在修改了。
  • StringBuffer:是一個可變字元串序列,它與 String 一樣,在記憶體中儲存的都是一個有序的字元串序列(char 類型的數組),不同點是 StringBuffer 對象的值都是可變的。
  • StringBuilder:與 StringBuffer 類基本相同,都是可變字元串序列,不同點是 StringBuffer 是線程安全的,StringBuilder 是線程不安全的。是以StringBuilder效率更高,因為鎖的擷取和釋放會帶來開銷。

不論是建立StringBuffer 還是 StringBuilder對象,都是預設建立一個容量為16的字元數組。差別就是所有的方法中,比如append,前者有synchronized關鍵字修飾。

Core Java 總結(字元和字元串類問題)

StringBuffer、StringBuilder,兩者的toString()方法是如何傳回的字元串類型?

Core Java 總結(字元和字元串類問題)
Core Java 總結(字元和字元串類問題)
雖然StringBuffer使用了緩存,但是本質上都一樣,每次toString()都會建立一個新的String對象,而不是使用底層的字元數組,StringBuffer/StringBuilder的存在是為了高效的操作字元串(字元數組)的狀态,但是當我們使用toString()的時候一定是一個穩定的狀态,具有确切的行為。      

解析

String和StringBuffer、StringBuilder三者的使用場景

Core Java 總結(字元和字元串類問題)
Core Java 總結(字元和字元串類問題)
使用 String 類的場景:在字元串不經常變化的場景中可以使用 String 類,例如常量的聲明、少量的變量運算。

使用 StringBuffer 類的場景:在頻繁進行字元串運算(如拼接、替換、删除等),并且運作在多線程環境中,則可以考慮使用 StringBuffer,例如 XML 解析、HTTP 參數解析和封裝。

使用 StringBuilder 類的場景:在頻繁進行字元串運算(如拼接、替換、和删除等),并且運作在單線程的環境中,則可以考慮使用 StringBuilder,如 SQL 語句的拼裝、JSON 封裝等。      

String和StringBuffer、StringBuilder三者的性能分析

Core Java 總結(字元和字元串類問題)
Core Java 總結(字元和字元串類問題)
在性能方面,由于 String 類的操作是産生新的 String 對象,而 StringBuilder 和 StringBuffer 隻是一個字元數組的擴容而已,是以 String 類的操作要遠慢于 StringBuffer 和 StringBuilder。簡要的說, String 類型和 StringBuffer 類型的主要性能差別其實在于 String 是不可變的對象, 是以在每次對 String 類型進行改變的時候其實都等同于生成了一個新的 String 對象,然後将指針指向新的 String 對象。是以經常改變内容的字元串最好不要用 String ,因為每次生成對象都會對系統性能産生影響,特别當記憶體中無引用對象多了以後, JVM 的 GC 就會開始工作,那速度是一定會相當慢的。而如果是使用 StringBuffer 類則結果就不一樣了,每次結果都會對 StringBuffer 對象本身進行操作,而不是生成新的對象,再改變對象引用。是以在一般情況下我們推薦使用 StringBuffer,如果沒有同步問題,推進直接使用StringBuilder ,特别是字元串對象經常改變的情況下。而在某些特别情況下, String 對象的字元串拼接其實是被 JVM 解釋成了 StringBuffer 對象的拼接。而在解釋的過程中,自然速度會慢一些。      

下面這些語句執行會發生什麼事情? 

String m = "hello,world";
String n = "hello,world";
String u = new String(m);
String v = new String("hello,world");      
Core Java 總結(字元和字元串類問題)
Core Java 總結(字元和字元串類問題)
Java堆中會配置設定一個長度11的char數組,并在字元串常量池配置設定一個由這個char數組組成的字元串,然後由棧中引用變量m指向這個字元串。

用n去引用常量池裡邊的同一個字元串,是以和m引用的是同一個對象。

生成一個新的字元串,但新字元串對象内部的字元數組引用着m内部的字元數組。

同樣會生成一個新的字元串,但内部的字元數組引用常量池裡邊的字元串内部的字元數組,意思是和u是同樣的字元數組。      

如果使用一個圖來表示的話,情況就大概是這樣的(使用虛線隻是表示兩者其實沒什麼特别的關系):

Core Java 總結(字元和字元串類問題)

結論就是,m和n是同一個對象,但m,u,v都是不同的對象,但都使用了同樣的字元數組,并且用equal判斷的話也會傳回true。

如下代碼的執行結果是什麼?

class Workout {
    private static String m = "hello,world";
    private static String n = "hello,world";
    private static String u = new String(m);
    private static String v = new String("hello,world");

    public static void main(String[] args) throws Exception {
        test1();
    }

    public static void test1() throws Exception {
        Field f = m.getClass().getDeclaredField("value");
        f.setAccessible(true);
        char[] cs = (char[]) f.get(m);
        cs[0] = 'H';
        String p = "Hello,world";
        
        System.out.println(p.equals(m));
        System.out.println(p.equals(n));
        System.out.println(p.equals(u));
        System.out.println(p.equals(v));
    }
}      
Core Java 總結(字元和字元串類問題)
Core Java 總結(字元和字元串類問題)
執行全部是true,說明反射起作用了,即String作為一直标榜的不可變對象,竟然被修改了!可以看到,經常說的字元串是不可變的,其實和其他的final類還是沒什麼差別,還是引用不可變的意思。 雖然String類不開放value,但同樣是可以通過反射進行修改,隻是通常沒人這麼做而已。 即使是涉及JDK自己的 ”修改” 的方法,都是通過産生一個新的字元串對象來實作的,例如replace、toLower、concat等。 這樣做的好處就是讓字元串是一個狀态不可變類,在多線程操作時沒有後顧之憂。      

看下String類的主要源碼:

Core Java 總結(字元和字元串類問題)

有一個final類型的char數組value,它是能被反射攻擊的!全部輸出true,也證明了之前的解釋是正确的,存在字元串常量池,且新對象也好,直接引用的常量池也好,内部的char數組都是這一個。如果内容一樣的話。即:字元串常量通常是在編譯的時候就确定好的,定義在類的方法區裡,也就是說,不同的類,即使用了同樣的字元串,還是屬于不同的對象。是以才需要通過引用字元串常量來減少相同的字元串的數量。

String m = "hello,world";
String u = m.substring(2,10);
String v = u.substring(4,7);      
Core Java 總結(字元和字元串類問題)
Core Java 總結(字元和字元串類問題)
m,u,v是三個不同的字元串對象,但引用的value數組其實是同一個。 同樣可以通過上述反射的代碼進行驗證。      
Core Java 總結(字元和字元串類問題)

雖然産生了新的字元串對象,但是引用的字元串常量池還是原來的,

String m = "hello,";
String u = m.concat("world");
String v = new String(m.substring(0,2));      
Core Java 總結(字元和字元串類問題)
Core Java 總結(字元和字元串類問題)
注意:字元串操作時,可能需要修改原來的字元串數組内容或者原數組沒法容納的時候,就會使用另外一個新的數組,例如replace,concat, + 等操作。對于String的構造方法,對于字元串參數隻是引用部分字元數組的情況(count小于字元數組長度),采用的是拷貝新數組的方式,是比較特别的,不過這個構造方法也沒什麼機會使用到。      
Core Java 總結(字元和字元串類問題)

可以發現,m,u,v内部的字元數組并不是同一個。且單獨看 m.substring(0,2);  産生的“he”字元串引用的字元數組是常量池裡的“hello,”。但是在String構造方法裡,采用的是拷貝新數組的方式,然後v來引用,這裡很特殊。别忘了,world也在字元串常量池裡,常量池中的字元串通常是通過字面量的方式産生的,就像上述m語句那樣。 并且他們是在編譯的時候就準備好了,類加載的時候,順便就在常量池生成。

注意:在JDK7,substring()方法會建立一個新的字元數組,而不是使用已有的。

String m = "hello,world";
        String u = m + ".";
        String v = "hello,world.";
        String q = "hello,world.";

        System.out.println(u.equals(v));
        System.out.println(u == v);
        System.out.println(q == v);      
Core Java 總結(字元和字元串類問題)
Core Java 總結(字元和字元串類問題)
true,false,true      

答案

即使是字元串的内容是一樣的,都不能保證是同一個字元串數組,u和v雖然是一樣内容的字元串,但内部的字元數組不是同一個。畫成圖的話就是這樣的:

Core Java 總結(字元和字元串類問題)

因為m引用的字元數組長度固定,多一個".",原數組無法容納,會使用另外一個新的字元數組,也就是u引用新的對象,沒有放到常量池。

final String m = "hello,world";
        String u = m + ".";
        String v = "hello,world.";
        String q = "hello,world.";

        System.out.println(u.equals(v));
        System.out.println(u == v);
        System.out.println(q == v);      
Core Java 總結(字元和字元串類問題)
Core Java 總結(字元和字元串類問題)
true
true
true

如果讓m聲明為final,u和v會變成同一個對象。這應該怎麼解釋?這其實都是編譯器搞的鬼,因為m是顯式final的,常量和常量連接配接, u直接被編譯成”hello,world.”了。      

畫成圖的話就是這樣的:

Core Java 總結(字元和字元串類問題)

下面程式的運作結果是?

Core Java 總結(字元和字元串類問題)
Core Java 總結(字元和字元串類問題)
Core Java 總結(字元和字元串類問題)
false


String str1 = "hello";
這裡的str1指的是方法區(java7中又把常量池移到了堆中)的字元串常量池中的“hello”,編譯時期就知道的;

String str2 = "he" + new String("llo");
這裡的str2必須在運作時才知道str2是什麼,是以它是指向的是堆裡定義的字元串“hello”,是以這兩個引用是不一樣的,如果用str1.equal(str2),那麼傳回的是True;因為兩個字元串的内容一樣。

和上個問題類型,因為,編譯器沒那麼智能,它不知道"he" + new String("llo")的内容是什麼,是以才不敢貿然把"hello"這個對象的引用賦給str2. 

new String("llo")外邊包着一層外衣呢,如果語句改為:"he"+"llo"這樣就是true了。      

String m = "hello,world";
        String u = m.substring(0,2);
        String v = u.intern();      
Core Java 總結(字元和字元串類問題)
Core Java 總結(字元和字元串類問題)
上面我們已經知道m,u雖然是不同的對象,但是使用的是同一個value字元數組,但intern方法會到常量池裡邊去尋找字元串”he”,如果找到的話,就直接傳回該字元串, 否則就在常量池裡邊建立一個并傳回,是以v使用的字元數組和m,n不是同一個。      
Core Java 總結(字元和字元串類問題)

下面這些語句執行後,JVM以後會回收m,n麼? 

String m = "hello,world";
String n = m.substring(0,2);
m = null;
n = null;      
Core Java 總結(字元和字元串類問題)
Core Java 總結(字元和字元串類問題)
字面量字元串,因為存放在常量池裡邊,被常量池引用着,是沒法被GC的。      
Core Java 總結(字元和字元串類問題)

經過上面幾個題的分析,對于Java字元串,像substring、split等方法得到的結果都是引用原字元數組的,如果某字元串很大,而且不是在常量池裡存在的,當你采用substring等方法拿到一小部分新字元串之後,長期儲存的話(例如用于緩存等),會造成原來的大字元數組意外無法被GC的問題。如果這樣的大字元串對象較多,且每個都被substring等方法切割了,那麼這些大對象都無法被GC,必然會記憶體浪費。關于這個問題,常見的解決辦法就是使用new String(String original)。在String構造方法裡,采用的是拷貝新數組的方式來被引用。

請簡述 equal 和 ==的差別?

Core Java 總結(字元和字元串類問題)
Core Java 總結(字元和字元串類問題)
也是初級的,但是想得滿分不太容易,能拉開檔次,之前看網上有的解釋說,前者比較内容,後者比較位址,其實這是不嚴謹的,作為一個宣稱掌握Java的程式員,實在不應該,equal方法是Object這個超級根類裡的,預設是實作的==,隻有在重寫(比如字元串操作裡)後,按照Java的設計規範,equal才被重寫為了比較對象的内容。故,應該分類别(重寫否),不同環境和不同資料類型(對象還是基本類型)下進行分析。

== 用于比較兩個對象的時候,是來check 兩個引用是否指向了同一塊記憶體,比較的是位址,比較基本類型,比較的是數值大小。

equals() 是Object的方法,預設情況下,它與== 一樣,比較的位址。但是當equal被重載之後,根據設計,equal 會比較對象的value。而這個是java希望有的功能。String 類就重寫了這個方法,比較字元串内容。      

上述幾個問題得出結論

  • 任何時候,比較字元串内容都應該使用equals方法
  • 修改字元串操作,應該使用StringBuffer,StringBuilder
  • 可以使用intern方法讓運作時産生的字元串複用常量池中的字元串
  • 字元串操作可能會複用原字元數組,在某些情況可能造成記憶體洩露的問題,split,subString等方法。要小心。

下面哪段程式能夠正确的實作GBK編碼位元組流到UTF-8編碼位元組流的轉換:

Core Java 總結(字元和字元串類問題)
Core Java 總結(字元和字元串類問題)
Core Java 總結(字元和字元串類問題)
操作步驟就是先解碼再編碼,用new String(src,"GBK")解碼得到字元串,用getBytes("UTF-8")得到UTF8編碼位元組數組      

在Java語言中,下列關于字元集編碼(Character set encoding)和國際化(i18n)的問題,哪些是正确的?

Core Java 總結(字元和字元串類問題)
Core Java 總結(字元和字元串類問題)
Core Java 總結(字元和字元串類問題)
Java内部預設使用Unioncode編碼,即不論什麼語言都是一個字元占兩個位元組,Java的class檔案編碼為UTF-8,而虛拟機JVM編碼為UTF-16。UTF-8編碼下,一個中文占3個位元組,一個英文占1個位元組,Java中的char預設采用Unicode編碼,是以Java中char占2個位元組。B 也是不正确的,不同的編碼之間是可以轉換的,必須太絕對了。C 是正确的。Java虛拟機中通常使用UTF-16的方式儲存一個字元。D 也是正确的。ResourceBundle能夠依據Local的不同,選擇性的讀取與Local對應字尾的properties檔案,以達到國際化的目的。      

語句:char foo='中';  是否正确?(假設源檔案以GB2312編碼存儲,并且以javac – encoding GB2312指令編譯)

Core Java 總結(字元和字元串類問題)
Core Java 總結(字元和字元串類問題)
這在java中是正确的,在C語言中是錯誤的,java的char類型預設占兩個位元組。這種寫法是正确的,此外java還可以用中文做變量名。因為java内部都是用unicode的,是以java其實是支援中文變量名的,比如string 世界 = "我的世界";這樣的語句是可以通過的。綜上,java中采用GB2312或GBK編碼方式時,一個中文字元占2個位元組,而char是2個位元組,是以是對的。      

以下Java代碼将列印出什麼?

Core Java 總結(字元和字元串類問題)
Core Java 總結(字元和字元串類問題)
Core Java 總結(字元和字元串類問題)
由于replaceAll方法的第一個參數是一個正規表達式,而"."在正規表達式中表示任何字元,是以會把前面字元串的所有字元都替換成"/"。

如果想替換的隻是“.”的話,正規表達式那裡就要寫成“\\.”或者是“[.]”。前者将“.”轉義為“.”這個具體字元,後者則比對“[]”中的任意字元,“.”就代表具體字元“.”。

輸出  ///////MyClass.class      

以下Java代碼将列印出什麼?Test1是本類名。完整,類名是com.dashuai.Test1

System.out.println(Test1.class.getName().replaceAll("\\.", File.separator) + ".class");      
Core Java 總結(字元和字元串類問題)
Core Java 總結(字元和字元串類問題)
這個程式根據底層平台的不同會顯示兩種行為中的一種。如果在類UNIX 上運作,那麼該程式将列印com/dashuai/Test1.class,這是正确的。但是在Windows 上運作,那麼該程式将抛出異常:

Exception in thread "main" java.lang.IllegalArgumentException: character to be escaped is missing
at java.util.regex.Matcher.appendReplacement(Matcher.java:809)
at java.util.regex.Matcher.replaceAll(Matcher.java:955)
at java.lang.String.replaceAll(String.java:2223)
at wangdashuai.Test1.main(Test1.java:25)

在Windows 上出了什麼錯呢?事實證明,String.replaceAll 的第二個參數不是一個普通的字元串,而是一個替代字元串(replacement string),就像在java.util.regex 規範中所定義的那樣。在Linux平台,是正斜杠,在win下士反斜杠。在替代字元串中出現的反斜杠會把緊随其後的字元進行轉義,進而導緻其被按字面含義而處理。

修改:5.0 + 版本提供了解決方案。該方法就是

String.replace(CharSequence, CharSequence),它做的事情和String.replaceAll 相同,但是它将模式和替代物都當作字面含義的字元串處理。

System.out.println(Test1.class.getName().replace(".", File.separator) + ".class");



小結:在使用不熟悉的類庫方法時一定要格外小心。當你心存疑慮時,就要求助于Javadoc。還有就是正規表達式是很棘手的:它所引發的問題趨向于在運作時刻而不是在編譯時刻暴露出來。還要記住,replaceAll,會把模式當做正規表達式,而replace不會。      

Java中用正規表達式截取字元串中第一個出現的英文左括号之前的字元串。比如:北京市(海澱區)(朝陽區)(西城區),截取結果為:北京市。正規表達式為()

Core Java 總結(字元和字元串類問題)
Core Java 總結(字元和字元串類問題)
做這個題,如果想做對,必須知道和了解正規表達式的貪婪比對。


String str="abcaxc";
Patter p="ab*c";
貪婪比對:正規表達式一般趨向于最大長度比對,也就是所謂的貪婪比對。如上面使用模式p比對字元串str,結果就是比對到:abcaxc

非貪婪比對:就是比對到結果就好,盡量少的比對字元。如上面使用模式p比對字元串str,結果就是比對到:abc

正規表達式預設是貪婪模式;在量詞後面直接加上一個問号?就是非貪婪模式比對。

量詞:
{m,n}:m到n個
*:任意多個(0個或者多個)
+:一個或者多個
?:0或一個

(?=Expression) 順序比對Expression

正确答案:“.*?(?=\\()”

”(?=\\()”  就是順序比對正括号,前面的.*?是非貪婪比對的意思, 表示找到最小的就可以了      

使用Java寫一個方法,判斷一個ip位址是否有效

Core Java 總結(字元和字元串類問題)
Core Java 總結(字元和字元串類問題)
private static boolean isIp(String ip) {
        String ipString = "^(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[1-9])\\."
                + "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)\\."
                + "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)\\."
                + "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)$";
        Pattern pattern = Pattern.compile(ipString);
        Matcher matcher = pattern.matcher(ip);

        return matcher.matches();
    }      

Java中如何用正規表達式判斷一個網址位址是否有效,寫出正規表達式即可?

Core Java 總結(字元和字元串類問題)
Core Java 總結(字元和字元串類問題)
String urlString = "(^(([hH][tT]{2}[pP])|([hH][tT]{2}[pP][sS]))://(([a-zA-Z0-9-~]+).)+([a-zA-Z0-9-~\\/]+)$)";      

注意:以上程式設計答案僅僅是個人解答,不唯一。

下面這條語句一共建立了多少個對象?

String s=“a”+”b”+”c”+”d”;      
Core Java 總結(字元和字元串類問題)
Core Java 總結(字元和字元串類問題)
隻建立了一個String對象。System.out.println(s== “abcd”);列印的結果為true。

javac編譯可以對字元串常量直接相加的表達式進行優化,不必要等到運作期去進行加法運算處理,而是在編譯時去掉其中的加号,直接将其編譯成一個這些常量相連的結果。      

如果改成 String s = a+b+c+d+e; 又是幾個了?

就是說上面的每個字元串 "a"、"b"、"c"、"d"、"e"用5個變量代替。

Core Java 總結(字元和字元串類問題)
Core Java 總結(字元和字元串類問題)
Java編譯器,自動生成一個StringBuilder對象,對字元串變量進行拼接操作。使用append方法,分别加入a,b,c,d,e。然後調用toString()方法傳回。

看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;
        return this;
    }

而ensureCapacityInternal方法内部使用了Arrays.copyOf()方法,該方法内部又調用了本地方法System.arraycopy(),該本地方法沒有産生新對象,但是在Arrays.copyOf()内部其他地方還産生了一個新對象new char[newLength],源碼如下:
    public static char[] copyOf(char[] original, int newLength) {
        char[] copy = new char[newLength];
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
    }

而getchars方法内部直接調用了本地方法System.arraycopy();沒有産生新對象。

看StringBuilder預設構造器:
    public StringBuilder() {
        super(16);
    }
我們的字元串長度不會超過16,不會擴容。如果題目出現了總長度超過16,則會出現如下的再次配置設定的情況:
    /**
     * This implements the expansion semantics of ensureCapacity with no
     * size check or synchronization.
     */
    void expandCapacity(int minimumCapacity) {
        int newCapacity = value.length * 2 + 2;
        if (newCapacity - minimumCapacity < 0)
            newCapacity = minimumCapacity;
        if (newCapacity < 0) {
            if (minimumCapacity < 0) // overflow
                throw new OutOfMemoryError();
            newCapacity = Integer.MAX_VALUE;
        }
        value = Arrays.copyOf(value, newCapacity);
    }


在看最後的傳回過程,調用了toString()方法,此時産生一個String新對象。
    @Override
    public String toString() {
        // Create a copy, don't share the array
        return new String(value, 0, count);
    }

故,一共産生三個對象,一個StringBuilder對象,一個String對象,一個new char[]對象。      

Java裡String a = new String("abc");一共能建立幾個String對象?

其實這個問題,問的很沒意思,不嚴謹,如果不幸在筆試遇到了,沒辦法,照着标準答案:兩個或一個對象,”abc”本身生成一個對象,放在字元串池(緩沖區),new的時候再生成一個,結果是2個。如果之前就使用了“abc”,那麼”abc”本身就不再生成對象,就是1個。一個字元串常量池裡的,一個new出來的,但是如果面試中遇到了,可以嘗試的和面試官溝通這個題目的問題。請看從事JVM開發的 R大神的解答:

請别再拿“String s = new String("xyz");建立了多少個String執行個體”來面試了吧

StringBuffer sb = new StringBuffer("abc"); 建立了幾個String對象?

Core Java 總結(字元和字元串類問題)
Core Java 總結(字元和字元串類問題)
這種問題,一定是問建立多少string對象,否則涉及其他對象,就太多了,不好說。看位元組碼:

Code:
stack=3, locals=2, args_size=1
0: new #16 // class java/lang/StringBuffer
3: dup 
4: ldc #18 // String 123
6: invokespecial #20 // Method java/lang/StringBuffer."<init>":(Ljava/lang/String;)V
9: astore_1 
10: return

答案很明顯。ldc:從常量池加載string,如果常量池中有“123”,就不建立。而new指令 :建立了一個buffer對象。      

Java裡對于密碼等敏感資訊優先使用字元數組還是字元串,為什麼?

Core Java 總結(字元和字元串類問題)
Core Java 總結(字元和字元串類問題)
雖然String加載密碼之後可以把這個變量扔掉,但是字元串并不會馬上被GC回收(考慮常量池,即使沒有常量池引用,GC也不一定立即回收它),一但程序在GC執行到這個字元串之前被dump,dump出的的轉儲中就會含有這個明文的字元串。那如果去“修改”這個字元串,比如把它賦一個新值,那麼是不是就沒有這個問題了?答案是否定的,因為String本身是不可修改的,任何基于String的修改函數都是傳回一個新的字元串,原有的還會在記憶體裡。對于char[]來說,可以在抛棄它之前直接修改掉它裡面的内容,密碼就不會存在了。但是如果什麼也不做直接交給gc的話,也會存在上面一樣的問題。      

System.out.print("H"+"a");
        System.out.print('H'+'a');      
Core Java 總結(字元和字元串類問題)
Core Java 總結(字元和字元串類問題)
列印的是Ha169。

第一個對System.out.print 的調用列印的是Ha:它的參數是表達式"H"+"a",顯然它執行的是一個字元串連接配接。

第二個對System.out.print 的調用,'H'和'a'是字元型字面常量,這兩個操作數都不是字元串類型的,是以 + 操作符執行的是加法而不是字元串連接配接。編譯器在計算常量表達式'H'+'a'時,是通過我們熟知的拓寬原始類型轉換将兩個具有字元型數值的操作數('H'和'a')提升為int 數值而實作的(類似的還有byte,short,char類型計算的時候都是自動提升為int)。從char 到int 的拓寬原始類型轉換是将16 位的char 數值零擴充到32 位的int。對于'H',char 數值是72,而對于'a',char 數值是97(需要記一下,0的asc碼是48,A是65,a是97,這些常用的),是以表達式'H'+'a'等價于int常量72 + 97=169。



修改為列印Ha,可以使用類庫:

StringBuffer sb = new StringBuffer();
sb.append('H');
sb.append('a');
System.out.println(sb);



很醜陋。其實我們還是有辦法去避免這種方式所産生的拖沓冗長的代碼。 你可以通過確定至少有一個操作數為字元串類型,來強制 + 操作符去執行一個字元串連接配接操作,而不是一個加法操作。這種常見的慣用法用一個空字元串("")作為一個連接配接序列的開始

System.out.println("" + 'H' + 'a');      

System.out.print("2 + 2 = " + 2 + 2);      
Core Java 總結(字元和字元串類問題)
Core Java 總結(字元和字元串類問題)
2 + 2 = 22

因為進行的是字元串連接配接,不是數值加法計算。

修改:
        int a = 2 + 2;
        System.out.print("2 + 2 = " + a);      
Core Java 總結(字元和字元串類問題)
Core Java 總結(字元和字元串類問題)
這幾道題說明,使用字元串連接配接操作符要格外小心。+ 操作符當且僅當它的操作數中至少有一個是String 類型時,才會執行字元串連接配接操作;否則,它執行的就是加法。如果要連接配接的沒有一個數值是字元串類型的,那麼你可以有幾種選擇:
• 預置一個空字元串;
• 将第一個數值用String.valueOf 顯式地轉換成一個字元串;
• 使用一個字元串緩沖區StringBuilder等;
• 或者如果你使用的JDK 5.0,可以用printf 方法,類似c語言。      

小結

char[] numbers = {'1', '2', '3'};
        System.out.println(numbers);      
Core Java 總結(字元和字元串類問題)
Core Java 總結(字元和字元串類問題)
盡管char 是一個整數類型,但是許多類庫都對其進行了特殊處理,因為char數值通常表示的是字元而不是整數。例如,将一個char 數值傳遞給println 方法會列印出一個Unicode 字元而不是它的數字代碼。字元數組受到了相同的特殊處理:println 的char[]重載版本會列印出數組所包含的所有字元,而String.valueOf和StringBuffer.append的char[]重載版本的行為也是類似的。      

String letters = "ABC";
        char[] numbers = {'1', '2', '3'};
        System.out.println(letters + " easy as " + numbers);      
Core Java 總結(字元和字元串類問題)
Core Java 總結(字元和字元串類問題)
列印的是諸如 ABC easy as [C@1db9742 之類的東西。盡管char 是一個整數類型,但是許多類庫都對其進行了特殊處理,然而,字元串連接配接操作符在這些方法中沒有被定義。該操作符被定義為先對它的兩個操作數執行字元串轉換,然後将産生的兩個字元串連接配接到一起。對包括數組在内的對象引用的字元串轉換定義如下:

如果引用為null,它将被轉換成字元串"null"。否則,該轉換的執行就像是不用任何參數調用該引用對象的toString 方法一樣;

但是如果調用toString 方法的結果是null,那麼就用字元串"null"來代替。

那麼,在一個非空char 數組上面調用toString 方法會産生什麼樣的行為呢?

數組是從Object 那裡繼承的toString 方法,規範中描述到:“傳回一個字元串,它包含了該對象所屬類的名字,'@'符号,以及表示對象散列碼的一個無符号十六進制整數”。有關Class.getName 的規範描述到:在char[]類型的類對象上調用該方法的結果為字元串"[C"。将它們連接配接到一起就形成了在我們的程式中列印出來的那個字元串。



有兩種方法可以修改這個程式。可以在調用字元串連接配接操作之前,顯式地将一個數組轉換成一個字元串:

String letters = "ABC";
char[] numbers = {'1', '2', '3'};
System.out.println(letters + " easy as " + String.valueOf(numbers));



可以将System.out.println 調用分解為兩個調用,以利用println 的char[]重載版本:
System.out.print(letters + " easy as ");
System.out.println(numbers);      

String letters = "ABC";
        Object numbers = new char[] { '1', '2', '3' };
        System.out.print(letters + " easy as ");
        System.out.println(numbers);      
Core Java 總結(字元和字元串類問題)
Core Java 總結(字元和字元串類問題)
列印ABC easy as [C@1db9742這樣的字元串,因為它調用的是println 的Object 重載版本,而不是char[]重載版本。



總之,記住:

char 數組不是字元串。要想将一個char 數組轉換成一個字元串,就要調用String.valueOf(char[])方法。某些類庫中的方法提供了對char 數組的類似字元串的支援,通常是提供一個Object 版本的重載方法和一個char[]版本的重載方法,而之後後者才能産生我們想要的行為。。      

final String pig = "length: 10";
        final String dog = "length: " + pig.length();
        System.out.println("Animals are equal: " + pig == dog);      
Core Java 總結(字元和字元串類問題)
Core Java 總結(字元和字元串類問題)
分析可能會認為它應該列印出Animal are equal: false。對嗎?

運作該程式,發現它列印的隻是false,并沒有其它的任何東西。它沒有列印Animal are equal: 。+ 操作符,不論是用作加法還是字元串連接配接操作,它都比 == 操作符的優先級高。是以,println 方法的參數是按照下面的方式計算的:

System.out.println(("Animals are equal: " + pig) == dog);

這個布爾表達式的值當然是false,它正是該程式列印的輸出。避免此類錯誤的方法:在使用字元串連接配接操作符時,當不能确定你是否需要括号時,應該選擇穩妥地做法,将它們括起來。



小結:

字元串連接配接的優先級不應該和加法一樣。這意味着重載 + 操作符來執行字元串連接配接是有問題的。      

以下Java代碼列印26對麼,如果不對,為什麼?

System.out.println("a\u0022.length() + \u0022b".length());      
Core Java 總結(字元和字元串類問題)
Core Java 總結(字元和字元串類問題)
對該程式的一種很膚淺的分析會認為它應該列印出26,因為在由兩個雙引号"a\u0022.length()+\u0022b"辨別的字元串之間總共有26 個字元。稍微深入一點的分析會想到 \u0022 是Unicode 轉義字元,其實它是雙引号的Unicode 轉義字元,肯定不會列印26。



如果提示你Unicode 轉義字元是雙引号,列印什麼?

有人說列印16,因為兩個Unicode 轉義字元每一個在源檔案中都需要用6個字元來表示,但是它們隻表示字元串中的一個字元。是以這個字元串應該比它的外表看其來要短10 個字元。



其實如果運作,它列印的既不是26 也不是16,是2。了解這個題的關鍵是要知道:Java 對在字元串字面常量中的Unicode 轉義字元沒有提供任何特殊處理。編譯器在将程式解析成各種符号之前,先将Unicode轉義字元轉換成為它們所表示的字元。是以,程式中的第一個Unicode轉義字元将作為一個單字元字元串字面常量("a")的結束引号,而第二個Unicode 轉義字元将作為另一個單字元字元串字面常量("b")的開始引号。程式列印的是表達式"a".length()+"b".length(),即2。



可能的情況是該程式員希望将兩個雙引号字元置于字元串字面常量的内部。使用Unicode 轉義字元你是不能實作這一點的,但是你可以使用轉義字元序列來實作。表示一個雙引号的轉義字元序列是一個反斜杠後面緊跟着一個雙引号(\”)。如果将最初的Unicode 轉義字元用轉義字元序列來替換,那麼它将列印出16:

System.out.println("a\".length() + \"b".length());      
Core Java 總結(字元和字元串類問題)
Core Java 總結(字元和字元串類問題)
在字元串和字元字面常量中要優先選擇的是轉義字元序列,而不是Unicode 轉義字元。Unicode 轉義字元可能會因為它們在編譯序列中被處理得過早而引起混亂。不要使用Unicode 轉義字元來表示ASCII 字元。在字元串和字元字面常量中,應該使用轉義字元序列。      

/**
* Generated by the IBM IDL-to-Java compiler, version 1.0
* from F:\TestRoot\apps\a1\units\include\PolicyHome.idl
* Wednesday, June 17, 1998 6:44:40 o’clock AM GMT+00:00
*/
public class Test1 {
    public static void main(String[] args) {
        System.out.print("Hell");
        System.out.println("o world");
    }
}      
Core Java 總結(字元和字元串類問題)
Core Java 總結(字元和字元串類問題)
通不過編譯。問題在于注釋的第三行,它包含了字元\units。這些字元以反斜杠(\)以及緊跟着的字母u 開頭的,而它(\u)表示的是一個Unicode 轉義字元的開始。而這些字元後面沒有緊跟四個十六進制的數字,是以,這個Unicode 轉義字元是錯誤的,而編譯器則被要求拒絕該程式。即使是出現在注釋中也是如此。

Javadoc注釋中要小心轉移字元,要確定字元\u 不出現在一個合法的Unicode 轉義字元上下文之外,即使在注釋中也是如此。在機器生成的代碼中要特别注意此問題。      
Core Java 總結(字元和字元串類問題)
Core Java 總結(字元和字元串類問題)
除非确實是必需的,否則就不要使用Unicode 轉義字元。它們很少是必需的。      

以下Java代碼運作将會出現什麼問題?

byte bytes[] = new byte[256];
        for (int i = 0; i < 256; i++) {
            bytes[i] = (byte) i;
        }
        String str = new String(bytes);
        for (int i = 0, n = str.length(); i < n; i++) {
            System.out.println((int) str.charAt(i) + " ");
        }      
Core Java 總結(字元和字元串類問題)
Core Java 總結(字元和字元串類問題)
首先,byte 數組從0 到255 每一個可能的byte 數值進行了初始化,然後這些byte 數值通過String 構造器被轉換成了char 數值。最後,char 數值被轉型為int 數值并被列印。列印出來的數值肯定是非負整數,因為char 數值是無符号的,是以,你可能期望該程式将按順序列印出0 到255 的整數。

如果你運作該程式,可能會看到這樣的序列。但是在運作一次,可能看到的就不是這個序列了。如果在多台機器上運作它,會看到多個不同的序列,這個程式甚至都不能保證會正常終止,它的行為完全是不确定的。這裡的罪魁禍首就是String(byte[])構造器。有關它的規範描述道:“在通過解碼使用平台預設字元集的指定byte 數組來構造一個新的String 時,該新String 的長度是字元集的一個函數,是以,它可能不等于byte 數組的長度。當給定的所有位元組在預設字元集中并非全部有效時,這個構造器的行為是不确定
的”。

到底什麼是字元集?從技術角度上講,字元集是一個包,包含了字元、表示字元的數字編碼以及在字元編碼序列和位元組序列之間來回轉換的方式。轉換模式在字元集之間存在着很大的差別:某些是在字元和位元組之間做一對一的映射,但是大多數都不是這樣。ISO-8859-1 是唯一能夠讓該程式按順序列印從0 到255 的整數的預設字元集,它更為大家所熟知的名字是Latin-1[ISO-8859-1]。J2SE 運作期環境(JRE)的預設字元集依賴于底層的作業系統和語言。如果你想知道你的JRE 的預設字元集,并且你使用的是5.0 或更新的版本,那麼你可以通過調用java.nio.charset.Charset.defaultCharset()來了解。如果你使用的是較早的版本,那麼你可以通過閱讀系統屬性“file.encoding”來了解。

修改:

當你在char 序列和byte 序列之間做轉換時,你可以且通常是應該顯式地指定字元集。除了接受byte 數字之外,還可以接受一個字元集名稱的String 構造器就是專為此目的而設計的。如果你用下面的構造器去替換在最初的程式中的String 構造器,那麼不管預設的字元集是什麼,該程式都保證能夠按照順序列印從0 到255的整數:
String str = new String(bytes, "ISO-8859-1");

這個構造器聲明會抛出UnsupportedEncodingException 異常,是以你必須捕獲它,或者更适宜的方式是聲明main 方法将抛出它,要不然程式不能通過編譯。盡管如此,該程式實際上不會抛出異常。Charset 的規範要求Java 平台的每一種實作都要支援某些種類的字元集,ISO-8859-1 就位列其中。

小結:

每當你要将一個byte 序列轉換成一個String 時,你都在使用某一個字元集,不管你是否顯式地指定了它。如果你想讓你的程式的行為是可預知的,那麼就請你在每次使用字元集時都明确地指定。      

Core Java 總結(字元和字元串類問題)
Core Java 總結(字元和字元串類問題)
Core Java 總結(字元和字元串類問題)
如果回答列印LETTER UNKNOWN NUMERAL,那麼你就掉進陷阱裡面了。程式連編譯都通不過。因為注釋在包含了字元*/的字元串内部就結束了,字面常量在注釋中沒有被特殊處理。更一般地講,注釋内部的文本沒有以任何方式進行特殊處理。是以,塊注釋不能嵌套。

總之,塊注釋不能可靠地注釋掉代碼段,應該用單行的注釋序列來代替。      

System.out.print("iexplore:");
        http://www.google.com;
        System.out.println(":maximize");      
Core Java 總結(字元和字元串類問題)
Core Java 總結(字元和字元串類問題)
正确運作并列印 iexplore::maximize。在程式中間出現的URL是一個語句标号(statement label),後面跟着一行行尾注釋(end-of-line comment)。在Java中很少需要标号,這多虧了Java 沒有goto 語句(作為保留字)。

Java中很少被人了解的特性:”實際上就是你可以在任何語句前面放置标号。這個程式标注了一個表達式語句,它是合法的,但是卻沒什麼用處。”



注意:令人誤解的注釋和無關的代碼會引起混亂。要仔細地寫注釋,并讓它們跟上時代;要切除那些已遭廢棄的代碼。還有就是如果某些東西看起來奇怪,以至于不像對的,那麼它極有可能就是錯的。      

public class Test1 {
    private static Random rnd = new Random();

    public static void main(String[] args) {
        StringBuffer word = null;
        switch (rnd.nextInt(2)) {
        case 1:
            word = new StringBuffer('P');
        case 2:
            word = new StringBuffer('G');
        default:
            word = new StringBuffer('M');
        }
        word.append('a');
        word.append('i');
        word.append('n');
        System.out.println(word);
    }
}      
Core Java 總結(字元和字元串類問題)
Core Java 總結(字元和字元串類問題)
乍一看,這個程式可能會在一次又一次的運作中,以相等的機率列印出Pain,Gain 或 Main。看起來該程式會根據随機數生成器所選取的值來選擇單詞的第一個字母:0 選M,1 選P,2 選G。

它實際上既不會列印Pain,也不會列印Gain。也許更令人吃驚的是,它也不會列印Main,并且它的行為不會在一次又一次的運作中發生變化,它總是在列印ain。

一個bug 是所選取的随機數使得switch 語句隻能到達其三種情況中的兩種。Random.nextInt(int)的規範描述道:“傳回一個僞随機的、均等地分布在從0(包括)到指定的數值(不包括)之間的一個int 數值”。這意味着表達式rnd.nextInt(2)可能的取值隻有0和1,Switch語句将永遠也到不了case 2 分支,這表示程式将永遠不會列印Gain。nextInt 的參數應該是3 而不是2。這是一個相當常見的問題。

第二個bug 是在不同的情況(case)中沒有任何break 語句。不論switch 表達式為何值,該程式都将執行其相對應的case 以及所有後續的case。是以,盡管每一個case 都對變量word 賦了一個值,但是總是最後一個指派勝出,覆寫了前面的指派。最後一個指派将總是最後一種情況(default),即 M。這表明該程式将總是列印Main,而從來不列印Pain或Gain。在switch的各種情況中缺少break語句是非常常見的錯誤。

最後一個bug 是表達式new StringBuffer('M')可能沒有做你希望它做的事情。你可能對StringBuffer(char)構造器并不熟悉,這很容易解釋:它壓根就不存在。StringBuffer 有一個無參數的構造器,一個接受一個String 作為字元串緩沖區初始内容的構造器,以及一個接受一個int 作為緩沖區初始容量的構造器。在本例中,編譯器會選擇接受int 的構造器,通過拓寬原始類型轉換把字元數值'M'轉換為一個int 數值77。換句話說,new StringBuffer('M')傳回的是一個具有初始容量77 的空的字元串緩沖區。該程式餘下的部分将字元a、i 和n 添加到了這個空字元串緩沖區中,并列印出該緩沖區那總是ain 的内容。為了避免這類問題,不管在什麼時候,都要盡可能使用熟悉的慣用法和API。如果你必須使用不熟悉的API,那麼請仔細閱讀其文檔。在本例中,程式應該使用常用的接受一個String 的StringBuffer 構造器。



修改:

public class Test1 {

    private static Random rnd = new Random();

    public static void main(String[] args) {

        StringBuffer word = null;

        switch (rnd.nextInt(3)) {

        case 1:

            word = new StringBuffer("P");

            break;

        case 2:

            word = new StringBuffer("G");

            break;

        default:

            word = new StringBuffer("M");

            break;

        }

        word.append('a');

        word.append('i');

        word.append('n');

        System.out.println(word);

    }

}

盡管這個程式訂正了所有的bug,它還是顯得過于冗長了。下面是一個更優雅的版本:

private static Random rnd = new Random();

public static void main(String[] args) {

  System.out.println("PGM".charAt(rnd.nextInt(3)) + "ain");

}

下面是一個更好的版本。盡管它稍微長了一點,但是它更加通用: 

public class Test1 {

    public static void main(String[] args) {

        String a[] = { "Main", "Pain", "Gain" };

        System.out.println(randomElement(a));

    }

    private static Random rnd = new Random();

    private static String randomElement(String[] a) {

        return a[rnd.nextInt(a.length)];

    }

}

總結一下:首先,要當心Java随機數産生器的特點。其次,牢記在 switch 語句的每一個 case中都放置一條 break 語句。第三,要使用常用的慣用法和 API,并且當模糊的時候,一定要參考相關的文檔。第四,一個 char 不是一個 String,而是更像一個 int      

歡迎關注

dashuai的部落格是終身學習踐行者,大廠程式員,且專注于工作經驗、學習筆記的分享和日常吐槽,包括但不限于網際網路行業,附帶分享一些PDF電子書,資料,幫忙内推,歡迎拍磚!

Core Java 總結(字元和字元串類問題)

辛苦的勞動,轉載請注明出處,如果真心幫到了您,為鼓勵更多的寫作,您可以選擇部落格右側的打賞功能。

繼續閱讀