全部代碼均在本地編譯運行測試,環境爲 Windows7 32位機器 + eclipse Mars.2 Release (4.5.2)java
2016-10-17 整理程序員
比較初級的一個題目,而初級題目又是除高端職位外,筆試題海量篩人的首選,可是做爲經典題目,仍是入選了個人筆記,由於它能延伸的Java字符串的問題太多了……另外一種延伸的高端問法就是套路你,由於這個題100%會回答多線程的知識點:前者是線程安全的,後者是非線程安全的。而後他們就問什麼是線程,或者說進程和線程的區別,爲何有線程?它如何實現的線程安全,接下來稍微高端的就引入Java併發的問題……呵呵。閒言少敘,看幾個此類不一樣難度的系列問題。面試
String s = "abcd";
s = s.concat("ef");
String是final的,字符串對象內部是用final的字符數組存儲的,故String是有字面量這一說法的,這是其餘類型所沒有的特性(除原生類型)。另外,java中也有字符串常量池這個說法,用來存儲字符串字面量。能夠畫一個圖表示:正則表達式
String 類的操做本質是產生了新的 String 對象,給人假象:好像是字符串被改變了似的。編程
咱們先要記住:數組
String、StringBuffer、StringBuilder 都實現了 CharSequence 接口,內部都是用一個char數組實現,雖然它們都與字符串相關,可是其處理機制不一樣。緩存
不管是建立StringBuffer 仍是 StringBuilder對象,都是默認建立一個容量爲16的字符數組。區別就是全部的方法中,好比append,前者有synchronized關鍵字修飾。安全
雖然StringBuffer使用了緩存,可是本質上都同樣,每次toString()都會建立一個新的String對象,而不是使用底層的字符數組,StringBuffer/StringBuilder的存在是爲了高效的操做字符串(字符數組)的狀態,可是當咱們使用toString()的時候必定是一個穩定的狀態,具備確切的行爲。
使用 String 類的場景:在字符串不常常變化的場景中可使用 String 類,例如常量的聲明、少許的變量運算。
使用 StringBuffer 類的場景:在頻繁進行字符串運算(如拼接、替換、刪除等),而且運行在多線程環境中,則能夠考慮使用 StringBuffer,例如 XML 解析、HTTP 參數解析和封裝。
使用 StringBuilder 類的場景:在頻繁進行字符串運算(如拼接、替換、和刪除等),而且運行在單線程的環境中,則能夠考慮使用 StringBuilder,如 SQL 語句的拼裝、JSON 封裝等。
在性能方面,因爲 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");
Java堆中會分配一個長度11的char數組,並在字符串常量池分配一個由這個char數組組成的字符串,而後由棧中引用變量m指向這個字符串。
用n去引用常量池裏邊的同一個字符串,因此和m引用的是同一個對象。
生成一個新的字符串,但新字符串對象內部的字符數組引用着m內部的字符數組。
一樣會生成一個新的字符串,但內部的字符數組引用常量池裏邊的字符串內部的字符數組,意思是和u是一樣的字符數組。
若是使用一個圖來表示的話,狀況就大概是這樣的(使用虛線只是表示二者其實沒什麼特別的關係):多線程
結論就是,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)); } }
執行所有是true,說明反射起做用了,即String做爲一直標榜的不可變對象,居然被修改了!能夠看到,常常說的字符串是不可變的,其實和其餘的final類仍是沒什麼區別,仍是引用不可變的意思。 雖然String類不開放value,但一樣是能夠經過反射進行修改,只是一般沒人這麼作而已。 即便是涉及JDK本身的 」修改」 的方法,都是經過產生一個新的字符串對象來實現的,例如replace、toLower、concat等。 這樣作的好處就是讓字符串是一個狀態不可變類,在多線程操做時沒有後顧之憂。
看下String類的主要源碼:
有一個final類型的char數組value,它是能被反射攻擊的!所有輸出true,也證實了以前的解釋是正確的,存在字符串常量池,且新對象也好,直接引用的常量池也好,內部的char數組都是這一個。若是內容同樣的話。即:字符串常量一般是在編譯的時候就肯定好的,定義在類的方法區裏,也就是說,不一樣的類,即便用了一樣的字符串,仍是屬於不一樣的對象。因此才須要經過引用字符串常量來減小相同的字符串的數量。
String m = "hello,world"; String u = m.substring(2,10); String v = u.substring(4,7);
m,u,v是三個不一樣的字符串對象,但引用的value數組實際上是同一個。 一樣能夠經過上述反射的代碼進行驗證。
雖然產生了新的字符串對象,可是引用的字符串常量池仍是原來的,
String m = "hello,"; String u = m.concat("world"); String v = new String(m.substring(0,2));
注意:字符串操做時,可能須要修改原來的字符串數組內容或者原數組無法容納的時候,就會使用另一個新的數組,例如replace,concat, + 等操做。對於String的構造方法,對於字符串參數只是引用部分字符數組的狀況(count小於字符數組長度),採用的是拷貝新數組的方式,是比較特別的,不過這個構造方法也沒什麼機會使用到。
能夠發現,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);
true,false,true
即便是字符串的內容是同樣的,都不能保證是同一個字符串數組,u和v雖然是同樣內容的字符串,但內部的字符數組不是同一個。畫成圖的話就是這樣的:
由於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);
true true true 若是讓m聲明爲final,u和v會變成同一個對象。這應該怎麼解釋?這其實都是編譯器搞的鬼,由於m是顯式final的,常量和常量鏈接, u直接被編譯成」hello,world.」了。
畫成圖的話就是這樣的:
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();
上面咱們已經知道m,u雖然是不一樣的對象,可是使用的是同一個value字符數組,但intern方法會到常量池裏邊去尋找字符串」he」,若是找到的話,就直接返回該字符串, 不然就在常量池裏邊建立一個並返回,因此v使用的字符數組和m,n不是同一個。
畫成圖的話就是這樣的:
String m = "hello,world"; String n = m.substring(0,2); m = null; n = null;
字面量字符串,由於存放在常量池裏邊,被常量池引用着,是無法被GC的。
畫成圖的話就是這樣的:
通過上面幾個題的分析,對於Java字符串,像substring、split等方法獲得的結果都是引用原字符數組的,若是某字符串很大,並且不是在常量池裏存在的,當你採用substring等方法拿到一小部分新字符串以後,長期保存的話(例如用於緩存等),會形成原來的大字符數組意外沒法被GC的問題。若是這樣的大字符串對象較多,且每一個都被substring等方法切割了,那麼這些大對象都沒法被GC,必然會內存浪費。關於這個問題,常見的解決辦法就是使用new String(String original)。在String構造方法裏,採用的是拷貝新數組的方式來被引用。
也是初級的,可是想得滿分不太容易,能拉開檔次,以前看網上有的解釋說,前者比較內容,後者比較地址,其實這是不嚴謹的,做爲一個宣稱掌握Java的程序員,實在不該該,equal方法是Object這個超級根類裏的,默認是實現的==,只有在重寫(好比字符串操做裏)後,按照Java的設計規範,equal才被重寫爲了比較對象的內容。故,應該分類別(重寫否),不一樣環境和不一樣數據類型(對象仍是基本類型)下進行分析。 == 用於比較兩個對象的時候,是來check 兩個引用是否指向了同一塊內存,比較的是地址,比較基本類型,比較的是數值大小。 equals() 是Object的方法,默認狀況下,它與== 同樣,比較的地址。可是當equal被重載以後,根據設計,equal 會比較對象的value。而這個是java但願有的功能。String 類就重寫了這個方法,比較字符串內容。
上述幾個問題得出結論
任什麼時候候,比較字符串內容都應該使用equals方法
修改字符串操做,應該使用StringBuffer,StringBuilder
可使用intern方法讓運行時產生的字符串複用常量池中的字符串
字符串操做可能會複用原字符數組,在某些狀況可能形成內存泄露的問題,split,subString等方法。要當心。
操做步驟就是先解碼再編碼,用new String(src,"GBK")解碼獲得字符串,用getBytes("UTF-8")獲得UTF8編碼字節數組
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文件,以達到國際化的目的。
這在java中是正確的,在C語言中是錯誤的,java的char類型默認佔兩個字節。這種寫法是正確的,此外java還能夠用中文作變量名。由於java內部都是用unicode的,因此java實際上是支持中文變量名的,好比string 世界 = "個人世界";這樣的語句是能夠經過的。綜上,java中採用GB2312或GBK編碼方式時,一箇中文字符佔2個字節,而char是2個字節,因此是對的。
因爲replaceAll方法的第一個參數是一個正則表達式,而"."在正則表達式中表示任何字符,因此會把前面字符串的全部字符都替換成"/"。 若是想替換的只是「.」的話,正則表達式那裏就要寫成「\\.」或者是「[.]」。前者將「.」轉義爲「.」這個具體字符,後者則匹配「[]」中的任意字符,「.」就表明具體字符「.」。 輸出 ///////MyClass.class
System.out.println(Test1.class.getName().replaceAll("\\.", File.separator) + ".class");
這個程序根據底層平臺的不一樣會顯示兩種行爲中的一種。若是在類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不會。
作這個題,若是想作對,必須知道和理解正則表達式的貪婪匹配。 String str="abcaxc"; Patter p="ab*c"; 貪婪匹配:正則表達式通常趨向於最大長度匹配,也就是所謂的貪婪匹配。如上面使用模式p匹配字符串str,結果就是匹配到:abcaxc 非貪婪匹配:就是匹配到結果就好,儘可能少的匹配字符。如上面使用模式p匹配字符串str,結果就是匹配到:abc 正則表達式默認是貪婪模式;在量詞後面直接加上一個問號?就是非貪婪模式匹配。 量詞: {m,n}:m到n個 *:任意多個(0個或者多個) +:一個或者多個 ?:0或一個 (?=Expression) 順序匹配Expression 正確答案:「.*?(?=\\()」 」(?=\\()」 就是順序匹配正括號,前面的.*?是非貪婪匹配的意思, 表示找到最小的就能夠了
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(); }
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」;
只建立了一個String對象。System.out.println(s== 「abcd」);打印的結果爲true。
javac編譯能夠對字符串常量直接相加的表達式進行優化,沒必要要等到運行期去進行加法運算處理,而是在編譯時去掉其中的加號,直接將其編譯成一個這些常量相連的結果。
就是說上面的每一個字符串 "a"、"b"、"c"、"d"、"e"用5個變量代替。
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[]對象。
其實這個問題,問的很沒意思,不嚴謹,若是不幸在筆試遇到了,沒辦法,照着標準答案:兩個或一個對象,」abc」自己生成一個對象,放在字符串池(緩衝區),new的時候再生成一個,結果是2個。若是以前就使用了「abc」,那麼」abc」自己就再也不生成對象,就是1個。一個字符串常量池裏的,一個new出來的,可是若是面試中遇到了,能夠嘗試的和麪試官溝通這個題目的問題。請看從事JVM開發的 R大神的解答:
這種問題,必定是問建立多少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對象。
雖然String加載密碼以後能夠把這個變量扔掉,可是字符串並不會立刻被GC回收(考慮常量池,即便沒有常量池引用,GC也不必定當即回收它),一但進程在GC執行到這個字符串以前被dump,dump出的的轉儲中就會含有這個明文的字符串。那若是去「修改」這個字符串,好比把它賦一個新值,那麼是否是就沒有這個問題了?答案是否認的,由於String自己是不可修改的,任何基於String的修改函數都是返回一個新的字符串,原有的還會在內存裏。對於char[]來講,能夠在拋棄它以前直接修改掉它裏面的內容,密碼就不會存在了。可是若是什麼也不作直接交給gc的話,也會存在上面同樣的問題。
System.out.print("H"+"a");
System.out.print('H'+'a');
打印的是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);
2 + 2 = 22 由於進行的是字符串鏈接,不是數值加法計算。 修改: int a = 2 + 2; System.out.print("2 + 2 = " + a);
這幾道題說明,使用字符串鏈接操做符要格外當心。+ 操做符當且僅當它的操做數中至少有一個是String 類型時,纔會執行字符串鏈接操做;不然,它執行的就是加法。若是要鏈接的沒有一個數值是字符串類型的,那麼你能夠有幾種選擇:
• 預置一個空字符串;
• 將第一個數值用String.valueOf 顯式地轉換成一個字符串;
• 使用一個字符串緩衝區StringBuilder等;
• 或者若是你使用的JDK 5.0,能夠用printf 方法,相似c語言。
char[] numbers = {'1', '2', '3'}; System.out.println(numbers);
儘管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);
打印的是諸如 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);
打印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);
分析可能會認爲它應該打印出Animal are equal: false。對嗎? 運行該程序,發現它打印的只是false,並無其它的任何東西。它沒有打印Animal are equal: 。+ 操做符,不管是用做加法仍是字符串鏈接操做,它都比 == 操做符的優先級高。所以,println 方法的參數是按照下面的方式計算的: System.out.println(("Animals are equal: " + pig) == dog); 這個布爾表達式的值固然是false,它正是該程序打印的輸出。避免此類錯誤的方法:在使用字符串鏈接操做符時,當不能肯定你是否須要括號時,應該選擇穩妥地作法,將它們括起來。 小結: 字符串鏈接的優先級不該該和加法同樣。這意味着重載 + 操做符來執行字符串鏈接是有問題的。
System.out.println("a\u0022.length() + \u0022b".length());
對該程序的一種很膚淺的分析會認爲它應該打印出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());
在字符串和字符字面常量中要優先選擇的是轉義字符序列,而不是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"); } }
通不過編譯。問題在於註釋的第三行,它包含了字符\units。這些字符以反斜槓(\)以及緊跟着的字母u 開頭的,而它(\u)表示的是一個Unicode 轉義字符的開始。而這些字符後面沒有緊跟四個十六進制的數字,所以,這個Unicode 轉義字符是錯誤的,而編譯器則被要求拒絕該程序。即便是出如今註釋中也是如此。
Javadoc註釋中要當心轉移字符,要確保字符\u 不出如今一個合法的Unicode 轉義字符上下文以外,即便在註釋中也是如此。在機器生成的代碼中要特別注意此問題。
除非確實是必需的,不然就不要使用Unicode 轉義字符。它們不多是必需的。
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) + " "); }
首先,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 時,你都在使用某一個字符集,無論你是否顯式地指定了它。若是你想讓你的程序的行爲是可預知的,那麼就請你在每次使用字符集時都明確地指定。
若是回答打印LETTER UNKNOWN NUMERAL,那麼你就掉進陷阱裏面了。程序連編譯都通不過。由於註釋在包含了字符*/的字符串內部就結束了,字面常量在註釋中沒有被特殊處理。更通常地講,註釋內部的文本沒有以任何方式進行特殊處理。所以,塊註釋不能嵌套。
總之,塊註釋不能可靠地註釋掉代碼段,應該用單行的註釋序列來代替。
System.out.print("iexplore:"); http://www.google.com; System.out.println(":maximize");
正確運行並打印 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); } }
乍一看,這個程序可能會在一次又一次的運行中,以相等的機率打印出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