1. 程式人生 > >Java 字串相關面試題

Java 字串相關面試題

問:下面程式的執行結果是什麼?

String s1 = "abc";
StringBuffer s2 = new StringBuffer(s1);
System.out.println(s1.equals(s2));    //1,false
StringBuffer s3 = new StringBuffer("abc");
System.out.println(s3.equals("abc"));    //2,false
System.out.println(s3.toString().equals("abc"));    //3,true

答:註釋 1 列印為 false,主要考察 String 的 equals 方法,String 原始碼中 equals 方法有對引數進行 instance of String 判斷語句,StringBuffer 的祖先為 CharSequence,所以不相等; 註釋 2 列印為 false,因為 StringBuffer 沒有重寫 Object 的 equals 方法,所以 Object 的 equals 方法實現是 == 判斷,故為 false; 註釋 3 列印為 true,因為 Object 的 toString 方法返回為 String 型別,String 重寫了 equals 方法實現為值比較。

 

問:怎樣將 GB2312 編碼的字串轉換為 ISO-8859-1 編碼的字串?

 

答:如下程式碼即可實現,

String s1 = "你好";
String s2 = new String(s1.getBytes("GB2312"), "ISO-8859-1");

問:String、StringBuffer、StringBuilder 的區別是什麼?

答:String 是字串常量,StringBuffer 和 StringBuilder 都是字串變數,後兩者的字元內容可變,而前者建立後內容不可變;StringBuffer 是執行緒安全的,而 StringBuilder 是非執行緒安全的,執行緒安全會帶來額外的系統開銷,所以 StringBuilder 的效率比 StringBuffer 高;String 的每次修改操作都是在記憶體中重新 new 一個物件出來,而 StringBuffer、StringBuilder 則不用,且提供了一定的快取功能,預設 16 個位元組陣列的大小,超過預設的陣列長度時擴容為原來位元組陣列的長度 * 2 + 2,所以使用 StringBuffer 和 StringBuilder 時可以適當考慮下初始化大小,以便通過減少擴容次數來提高程式碼的高效性。

 

問:String 為什麼是不可變的?

答:String 不可變是因為在 JDK 中 String 類被宣告為一個 final 類,且類內部的 value 位元組陣列也是 final 的,只有當字串是不可變時字串池才有可能實現,字串池的實現可以在執行時節約很多 heap 空間,因為不同的字串變數都指向池中的同一個字串;如果字串是可變的則會引起很嚴重的安全問題,譬如資料庫的使用者名稱密碼都是以字串的形式傳入來獲得資料庫的連線,或者在 socket 程式設計中主機名和埠都是以字串的形式傳入,因為字串是不可變的,所以它的值是不可改變的,否則黑客們可以鑽到空子改變字串指向的物件的值造成安全漏洞;因為字串是不可變的,所以是多執行緒安全的,同一個字串例項可以被多個執行緒共享,這樣便不用因為執行緒安全問題而使用同步,字串自己便是執行緒安全的;因為字串是不可變的所以在它建立的時候 hashcode 就被快取了,不變性也保證了 hash 碼的唯一性,不需要重新計算,這就使得字串很適合作為 Map 的鍵,字串的處理速度要快過其它的鍵物件,這就是 HashMap 中的鍵往往都使用字串的原因。

 

 

問:說說 String str = "hello world"; 和 String str = new String("hello world"); 的區別?

答:在 java 的 class 檔案中有專門的部分用來儲存編譯期間生成的字面常量和符號引用,這部分叫做 class 檔案常量池,在執行期間對應著方法區的執行時常量池,所以 String str = "hello world"; 在編譯期間生成了 字面常量和符號引用,執行期間字面常量 "hello world" 被儲存在執行時常量池(只儲存了一份),而通過 new 關鍵字來生成物件是在堆區進行的,堆區進行物件生成的過程是不會去檢測該物件是否已經存在的,所以通過 new 來建立的一定是不同的物件,即使字串的內容是相同的。

 

 

問:語句 String str = new String("abc"); 一共建立了多少個物件?

答:這個問題其實有歧義,但是很多公司還特麼愛在筆試題裡面考察,非要是遇到了就答兩個吧(一個是 “xyz”,一個是指向 “xyz” 的引用物件 str);之所以說有歧義是因為該語句在執行期間只建立了一個物件(堆上的 "abc" 物件),而在類載入過程中在執行時常量池中先建立了一個 "abc" 物件,執行期和類載入又是有區別的,所以這個題目的問法是有些不嚴謹的。因此這個問題如果換成 String str = new String("abc"); 涉及到幾個 String 物件,則答案就是 2 個了.                                                                  String是不可變得,String str = "abc"就是一個物件,new那裡又一個物件

 

 

問:下面程式的執行結果是什麼?

String stra = "ABC";
String strb = new String("ABC");
System.out.println(stra == strb);    //1,false
System.out.println(stra.equals(strb));    //2,true

對於 1 和 2 中兩個都是顯式建立的新物件,使用 == 總是不等,String 的 equals 方法有被重寫為值判斷,所以 equals 是相等的。

String str1 = "123";
System.out.println("123" == str1.substring(0));    //3,true
System.out.println("23" == str1.substring(1));    //4,false

對於 3 和 4 中 str1 的 substring 方法實現裡面有個 index == 0 的判斷,當 index 等於 0 就直接返回當前物件,否則新 new 一個 sub 的物件返回,而 == 又是地址比較,所以結果如註釋。

String str3 = new String("ijk");
String str4 = str3.substring(0);
System.out.println(str3 == str4);    //5,true
System.out.println((new String("ijk") == str4));    //6,false

對於 5 和 6 來說沒啥分析的必要了,參見上面對於 3 和 4 的分析結果。

String str5 = "NPM";
String str6 = "npm".toUpperCase();
System.out.println(str5 == str6);    //7,false
System.out.println(str5.equals(str6));    //8,true
String str7 = new String("TTT");
String str8 = "ttt".toUpperCase();
System.out.println(str7 == str8);    //9,false
System.out.println(str7.equals(str8));    //10,true

對於 7、8、9、10 來說實質都一樣,toUpperCase 方法內部建立了新字串物件。

String str9 = "a1";
String str10 = "a" + 1;
System.out.println(str9 == str10);    //11,true

對於 11 來說當兩個字串常量連線時(相加)得到的新字串依然是字串常量且儲存在常量池中只有一份。

String str11 = "ab";
String str12 = "b";
String str13 = "a" + str12;
System.out.println(str11 == str13);    //12,false

對於 12 來說當字串常量與 String 型別變數連線時得到的新字串不再儲存在常量池中,而是在堆中新建一個 String 物件來存放,很明顯常量池中要求的存放的是常量,有 String 型別變數當然不能存在常量池中了。

String str14 = "ab";
final String str15 = "b";
String str16 = "a" + str15;
System.out.println(str14 == str16);    //13,true

對於 13 來說此處是字串常量與 String 型別常量連線,得到的新字串依然儲存在常量池中,因為對 final 變數的訪問在編譯期間都會直接被替代為真實的值。

private static String getBB() {  
   return "b";  
}
String str17 = "ab";  
final String str18 = getBB();  
String str19 = "a" + str18;  
System.out.println(str17 == str19);    //14,false

對於 14 來說 final String str18 = getBB() 其實與 final String str18 = new String(“b”) 是一樣的,也就是說 return “b” 會在堆中建立一個 String 物件儲存  ”b”,雖然 str18 被定義成了 final,但不代表是常量,因為雖然將 str18 用 final 修飾了,但是由於其賦值是通過方法呼叫返回的,那麼它的值只能在執行期間確定,因此指向的不是同一個物件,所以可見看見並非定義為 final 的就儲存在常量池中,很明顯此處 str18 常量引用的 String 物件儲存在堆中,因為 getBB() 得到的 String 已經儲存在堆中了,final 的 String 引用並不會改變 String 已經儲存在堆中這個事實;對於 str18 換成 final String str18 = new String("b"); 一樣會返回 false,原因同理。

String str20 = "ab";
String str21 = "a";  
String str22 = "b";  
String str23 = str21 + str22;  
System.out.println(str23 == str20);    //15,false
System.out.println(str23.intern() == str20);    //16,true
System.out.println(str23 == str20.intern());    //17,false
System.out.println(str23.intern() == str20.intern());    //18,true

對於 15 到 18 來說 str23 == str20 就是上面剛剛分析的,而對於呼叫 intern 方法如果字串常量池中已經包含一個等於此 String 物件的字串(用 equals(Object) 方法確定)則返回字串常量池中的字串,否則將此 String 物件新增到字串常量池中,並返回此 String 物件的引用,所以 str23.intern() == str20 實質是常量比較返回 true,str23 == str20.intern() 中 str23 就是上面說的堆中新物件,相當於一個新物件和一個常量比較,所以返回 false,str23.intern() == str20.intern() 就沒啥說的了,指定相等。

 

答:結果見題目中註釋部分,解析見上面分段說明,基於 JDK 1.7 版本分析。

 

註釋 11 到 14 深刻的說明了我們在程式碼中使用 String 時應該留意的優化技巧,你懂得!特別說明 String 的 + 和 += 在編譯後實質被自動優化為了 StringBuilder 和 append 呼叫,但是如果在迴圈等情況下呼叫 + 或者 += 就是在不停的 new StringBuilder 物件 append 了,這是及其浪費的。

 

通過這道題說明要想玩明白 Java String 物件的核心其實就是玩明白字串的堆疊和常量池,虛擬機器為每個被裝載的型別維護一個常量池,常量池就是該型別所用常量的一個有序集合,包括直接常量(String、Integer 和 Floating Point 常量)和對其他型別、欄位和方法的符號引用,池中的資料項就像陣列一樣是通過索引訪問的,由於常量池儲存了相應型別所用到的所有型別、欄位和方法的符號引用,所以它在 Java 程式的動態連結中起著核心的作用。

 

問:為什麼針對安全保密高的資訊,char[] 比 String 更好?

答:因為 String 是不可變的,一旦建立就不能更改,直到垃圾收集器將它回收才能消失,即使我們修改了原先的變數,實際上也是在記憶體中新建一個物件,原資料還是保留在記憶體中等待回收;而字元陣列 char[] 中的元素是可以更改的,也就是說像密碼等保密資訊用完之後我們可以馬上修改它的值而不留痕跡,從而相對於 String 有更好的安全性。

1. 這種做法意義有多大?

如果沒有及時清空而由 GC 來清除的話暴露視窗大約是秒這個數量級,如果能夠在計算 HASH 後立即清除則暴露視窗大約是微秒數量級,如此簡單的設計就可以降低如此多的被攻擊概率,價效比是非常高的。

2. 如何使用反射來修改 String?和修改 char[] 相比有何區別和風險?

通過反射機制可以檢視 String 內部的記憶體成員,從而可以直接修改其中的資料區,但是這樣的做法會有問題,內部化的 String 為了提高 HASH 速度、節省空間會保證值相同的字串通常只有一個例項,而 char[] 的修改是沒有任何副作用的,但是 String 原始碼裡面的 char[] 很可能是多個 String 共享的,我們改掉它就會殃及別的 String,譬如有一個密碼是 "Password",而你密碼框提示密碼輸入的文字也是 "Password",改掉第一個 "Password" 會把後面那個也改掉,所以修改 String 是有副作用的。

3. 如果一點明文也不想出現應該怎麼做?

為了保證全部處理流程均無明文密碼,需要底層 API 在給你密碼之前就做了 HASH,並且這個 HASH 演算法就是你想要的那種,最好還加鹽,不過這只是在使用者程式方面無明文,底層獲取中會不會有明文就保證不了了。

4. 有沒有絕對安全策略?

安全往往是相對於攻擊成本而言的,攻擊收益越高,黑客就越能接受攻擊成本高的方案,因此你採取的安全策略應該與這個攻擊收益相匹配,對於極其敏感和寶貴的資料來源就需要在安全方面上下很大功夫,目前來看沒有絕對的安全,只有相對的安全。

 

問:用 java 程式碼實現字串的反轉?

答:這道題的答案很多,下面給出兩種常見的答案。

 

使用 JDK 中 StringBuffer(併發安全)或者 StringBuilder 的反轉方法,這是最好的辦法,不僅速度快、效率高,程式碼如下:

public String reverse(String str) {
   if ((null == str) || (str.length() <= 1)) {
       return str;
   }
   return new StringBuffer(str).reverse().toString();
}

炫技能使用遞迴方案實現,程式碼如下:

public String reverse(String str) {
  if ((null == str) || (str.length()  <= 1)) {
      return str;
  }
  return reverse(str.substring(1)) + str.charAt(0);
}

問:用 java 程式碼來檢查輸入的字串是否迴文(對稱)?

答:這道題的答案也有很多,下面給出兩種常見的答案。

 

使用 JDK 現有 API 實現,程式碼如下:

boolean isPalindrome(String str) {
   if (str == null) {
       return false;
   }
   StringBuilder strBuilder = new StringBuilder(str);
   strBuilder.reverse();
   return strBuilder.toString().equals(str);
}

純手擼寫法實現,可以從 String 的兩端比較下手,程式碼如下:

boolean isPalindrome(String str) {
   if (str == null) {
       return false;
   }
   int length = str.length();
   for (int i = 0; i < length / 2; i++) {
       if (str.charAt(i) != str.charAt(length – i – 1)) {
           return false;
       }
   }
   return true;
}

問:用 java 程式碼寫一個方法從字串中刪除給定字元?

答:這道題太簡單不過了,都有點不好意思放上來,不過為了照顧所有層次還是寫下答案。

String removeChar(String str, char c) {
   if (str == null) {
       return null;
   }
   return str.replaceAll(Character.toString(c), "");
}