關(guān)于字符串的陷阱
JVM對字符串的處理
首先看如下代碼:
String java=new String("HelloJack");
上面創(chuàng)建了兩個字符串對象,其中一個是“HelloJack”這個直接量對應(yīng)的字符串對象,另一個是由new String()構(gòu)造器返回的字符串對象。
對于Java程序中的字符串直接量,JVM會使用一個字符串池來保存它們:當(dāng)?shù)谝淮问褂媚硞€字符串直接量是,JVM會將它放入字符串池進行緩存。在一般情況下,字符串池的字符串對象不會被垃圾回收,當(dāng)程序再次需要使用該字符串時,無需重新創(chuàng)建一個新的字符串,而是直接讓引用變量指向字符串池中已有的字符串。如下代碼:
String str1="Hello Java";
String str2="Hello Java";
System.out.println(str1==str2);
因為str1和str2都是直接量,都指向JVM字符串池里的“Hello Java”字符串,所以為true;
除了直接創(chuàng)建之外,也可以通過字符串連接表達(dá)式創(chuàng)建字符串對象,因此可以將一個字符串連接表達(dá)式賦給字符串變量。如果這這個字符串連接表達(dá)式的值可以在編譯時確定下來,那么JVM會在編譯時計算該字符串變量的值,并讓它指向字符串池中對應(yīng)的字符串。如下代碼:
String str1="HelloJava";
String str2="Hello"+"Java";
System.out.println(str1==str2);
最終結(jié)果返回就是true.需要注意的是上面都是直接量,而沒有變量,沒有方法的調(diào)用。因此,JVM可以在比編譯時就確定該字符串連接表達(dá)式的值,可以讓該字符串變量指向字符串池中對應(yīng)的字符串。但如果程序使用了變量,或者調(diào)用的方法,那么只能等到運行時才能確定該字符串連接表達(dá)式的值,也就無法再編譯時確定該字符串變量的值,因此無法利用JVM的字符串池。如下代碼:
String str1="HelloJava9";
String str2="Hello"+"Java9";
System.out.println(str1==str2);
String str3="HelloJava"+"HelloJava".length();
System.out.println(str1==str3);
第一個返回了true,第二個輸出返回了false;
當(dāng)然還有一個情況例外的,就是當(dāng)變量執(zhí)行“宏替換”時也是可以讓字符串變量指向JVM字符串池中對應(yīng)字符串。如下代碼:
String str1="HelloJava9";
String str2="Hello"+"Java9";
System.out.println(str1==str2);
final int len=9;
String str3="HelloJava"+len;
System.out.println(str1==str3);
不可變的字符串
String類是一個典型的不可變類。當(dāng)一個String對象創(chuàng)建完成后,該String類里包含的字符序列就被固定下來,以后永遠(yuǎn)都不會改變。如下代碼:
String str="Hello";
System.out.println(System.identityHashCode(str));
str=str+"Java";
System.out.println(System.identityHashCode(str));
當(dāng)一個String對象創(chuàng)建完成后,該String里包含的字符序列不能改變??赡軙幸苫?,str變量對應(yīng)的字符序列不是一直在變嗎,當(dāng)時str只是一個引用類型變量。像C語言的指針,他并不是真正的String對象,只是指向String對象而已。
示意圖如下:
從圖中知道"Hello”字符串也許以后永遠(yuǎn)都不會再被用到了,但是這個字符串并不會被垃圾回收掉,因為它一直存在于字符串池中,這也是Java內(nèi)存泄露的原因之一。
對于一個String類而言,他代表字符序列不可改變的字符串,因此如果程序需要一個字符序列會發(fā)生改變的字符串,那么應(yīng)該考慮使用StringBuilder和StringBuffer.
在通常情況下優(yōu)先考慮使用StringBuidler.StringBuidler與StringBuffer的區(qū)別在于,StringBuffer是線程安全的,也就是說StringBuffer類里的絕大部分方法都增加了synchoronized修飾符。對方法增加synchoronized修飾符可以保證該方法線程安全,當(dāng)會降低該方法的執(zhí)行效率。在沒有多現(xiàn)場的環(huán)境下,應(yīng)該優(yōu)先使用StringBuilder來表示字符串。
字符串比較
如果程序需要比較兩個字符串是否哦相同,用==進行判斷就可以了;但是如果判斷兩個字符串所包含的字符序列時候相同,則應(yīng)該用String重寫過的equals()方法進行比較。假如沒有重寫equals方法,則比較的是引用類型的變量所指向的對象的地址。
表達(dá)式類型的陷阱
表達(dá)式類型的自動提升
Javc語言規(guī)定:當(dāng)一個算術(shù)表達(dá)式中包含多個基本類型的值時,整個算術(shù)表達(dá)式的數(shù)據(jù)類型將自動提升。java語言的自動提升規(guī)則如下:
- 所有的byte類型,short類型和char類型將被提升到int類型。
-
整個算術(shù)表達(dá)式的數(shù)據(jù)類型自動提升與表達(dá)式中的最高等級操作數(shù)同樣的類型。操作數(shù)的如下,位于箭頭右邊的類型等級高于位于箭頭左邊的類型等級。
類型提升.PNG
復(fù)合賦值運算符的陷阱
經(jīng)過前面的介紹,可以知道下面的是錯誤的:
short sValue=5;
sValue=sValue-2;
因為sValue將自動提升為int類型,所以程序?qū)⒁粋€int類型的值賦值給short類型的變量時導(dǎo)致了編譯錯誤。
但是改為如下就沒有問題了:
short sValue=5;
sValue-=2;
上面程序使用復(fù)合賦值運算符,就不會導(dǎo)致編譯錯誤。
實際上sValue-=2;等價于sValue=(sValue的類型)(sValue-2),這就是復(fù)合賦值運算符的隱式類型轉(zhuǎn)換。
如果結(jié)果值的類型步變量的類型大,那么復(fù)合賦值運算符將會執(zhí)行一個強制類型轉(zhuǎn)換,這個強制類型轉(zhuǎn)換將有可能導(dǎo)致高位“截斷”,如下代碼所示:
short st=5;
st+=10;
st+=90000;
System.out.println(st);
為了避免這種潛在的危險,有如下幾種情況下需要特別注意:
- 將復(fù)合賦值運算符運用于byte,short或char等類型的變量
- 將復(fù)合賦值運算符運用于int類型的變量,而表達(dá)式右側(cè)是long,float或double類型的值。
- 將復(fù)合賦值運算符運用于float類型的變量,而表達(dá)式右側(cè)是double類型的值。
二進制整數(shù)
int it=ob1010_1010;
byte bt=(byte)ob1010_1010;
System.out.println(it==bt);
it和bt是不相等的,造成這種問題的原因在于這兩條規(guī)則:
- 直接使用整數(shù)直接量時,系統(tǒng)會將它當(dāng)成int類型處理。
- byte類型的整數(shù)雖然可以包含8位,但最高位是符號位。
轉(zhuǎn)義字符的陷阱
Java程序提供了三種方式來表示字符。
- 直接使用單引號括起來的字符值。如‘a(chǎn)’.
- 使用轉(zhuǎn)義字符,如‘\n’.
- 使用Unicode轉(zhuǎn)義字符,如‘\u0062’.
java對待Unicode轉(zhuǎn)義字符時不會進行任何處理,它會將Unicode轉(zhuǎn)義字符直接替換成對應(yīng)的字符,這將給java程序帶來一些潛在的陷阱。
慎用字符Unicode轉(zhuǎn)義形式
理論上,Unicode轉(zhuǎn)義字符可以代表任何字符(不考慮那些不在Unicode碼表內(nèi)的字符),因此很容易想到:所有字符都應(yīng)該可以使用Unicode轉(zhuǎn)義字符的形式。為了了解Unicode轉(zhuǎn)義字符帶來的危險,來看如下程序:
System.out.println("abc\u000a".length());
表面上看程序?qū)⑤敵?當(dāng)編譯該程序時發(fā)現(xiàn)程序無法通過編譯。原因是Java對Unicode轉(zhuǎn)義字符不會進行任何特殊處理,它只是簡單的將Unicode轉(zhuǎn)義字符替換成相應(yīng)的字符。對于\u000a而言,他相當(dāng)于一個換行符(\n),因此對Java編譯器而言,上面代碼相當(dāng)于如下:
System.out.println("abc\n".length);
中止行注釋的轉(zhuǎn)義字符
在java程序中使用\u000a時,它將被直接替換成換行字符(相當(dāng)于\n),因此java注釋中使用這個Unicode轉(zhuǎn)義字符要特別小心
泛型可能引起的錯誤
原始類型變量的賦值
在嚴(yán)格的泛型程序中,使用泛型聲明的類時應(yīng)該總是為之指定類型實參,但為了與老的Java代碼保存一致,Java也允許使用帶泛型聲明的類是不指定類型參數(shù),如果使用帶泛型聲明的類時沒有傳入類型實參,那么這個類型參數(shù)默認(rèn)是聲明該參數(shù)時指定的第一個上限類型,這個類型參數(shù)也被稱為raw type(原始類型)
當(dāng)嘗試把原始類型的變量賦給帶泛型類型的變量時,會發(fā)生一些有趣的事情,如下代碼:
List list=new ArrayList<>();
list.add("Hello");
list.add("Jack");
list.add("xie");
List<Integer> intList=list;
for(int i=0;i<intList.size();i++){
System.out.println(intList.get(i));
}
上面代碼編譯正常,并且正常輸出intList的集合是三個普通的字符串。通過上面可以看出:當(dāng)程序把一個原始類型的變量賦給一個帶泛型信息的變量時,只要他們的類型保持兼容,無論List集合里實際包含什么類型的元素,系統(tǒng)都不會有任何問題。
不過雖然我們編譯的時候可能不會有什么問題,但是當(dāng)我們把元素拿出來處理的時候intList還是引用的是String類型,而不是Integer,因此運行時可能還是會出問題。而當(dāng)我們String in=intList.get(i)時是會報編譯錯誤的。
為此總結(jié)如下:
- 當(dāng)程序把一個原始類型的變量賦給一個帶泛型信息的變量時,總是可以通過編譯---只是會提示一些警告信息。
- 當(dāng)程序試圖訪問帶泛型聲明的集合的集合元素時,編譯器總是把集合元素當(dāng)成泛型類型處理---它并不關(guān)心集合里集合元素的實際類型。
- 當(dāng)程序試圖訪問帶泛型聲明的集合的集合元素是,JVM會遍歷每個集合元素自定執(zhí)行強制類型轉(zhuǎn)換,如果集合元素的實際類型與集合所帶的泛型信息不匹配,運行時將引發(fā)ClassCastException異常。
原始類型帶來的擦除
Apple<Integer> apple=new Apple<Integer>();
Integer as=apple.getSize();
Apple b=apple;
Number size1=b.getSize();
Integer size2=b.getSize();
當(dāng)Integer size2=b.getSize();時代碼會報錯。
當(dāng)一個帶泛型信息的Java對象賦給不帶泛型信息的變量時,Java程序會發(fā)生擦除,這種擦除不僅會擦除使用Java類時傳入的類型實參,而且會擦除所有的泛型信息,也就是擦除所有尖括號里的信息。
創(chuàng)建泛型數(shù)組的陷阱
List<String>[] lsa=new List<String>[10];
編譯上面的代碼會提示‘創(chuàng)建泛型數(shù)組’的錯誤,這正是由Java引起運行時異常,這就違背了Java泛型的設(shè)計原則————如果一段代碼在編譯時系統(tǒng)沒有產(chǎn)生“[unchecked]未經(jīng)檢查的轉(zhuǎn)換”警告,則程序在運行時不會引發(fā)ClassCastException異常。
再看如下代碼:
public class GenericArray<T>{
class A{}
public GenericArray(){
A[] as=new A[10];
}
}
上面編譯還是會錯,A[] as=new A[10]只是創(chuàng)建A[]數(shù)組,而沒喲創(chuàng)建泛型數(shù)組,因為內(nèi)部類可以直接使用T類形形參,因此可能出現(xiàn)如下形似:
public class GenericArray<T>{
class A{
T foo;
}
public GenericArray(){
A[] as=new A[10];
}
}
這就導(dǎo)致創(chuàng)建泛型數(shù)組了,違背Java不能創(chuàng)建泛型數(shù)組的原則,所以JDK設(shè)計還是比較謹(jǐn)慎的。
正則表達(dá)式的陷阱
String str="java.is.funny";
String strAttr=str.split(".");
for(String s:strAttr){
System.out.println(s);
}
上面程序包含多個點號(.)的字符串,接著調(diào)用String提供的split()方法,以點號(.)作為分割符來分割這個字符串,希望返回該字符串被分割后得到的字符串?dāng)?shù)組。運行該程序,結(jié)果發(fā)現(xiàn)什么都沒有輸出。
對于上面程序需要注意如下兩點:
- String提供的split(String regex)方法需要的參數(shù)是正則表達(dá)式
- 正則表達(dá)式中的點號(.)可匹配任意字符。
將上面代碼改為如下形式String strAttr=str.split("\\.");即可實現(xiàn)分割。
String類也增加了一些方法用于支持正則表達(dá)式,具體方法如下:
- matches(String regex):判斷該字符串是否匹配指定的正則表達(dá)式。
- String replaceAll(String regex,String replacement):將字符串中所有匹配指定的正則表達(dá)式的子串替換成replacement后返回。
- String replaceFirst(String regex,String replacement):將字符串中第一個匹配指定的正則表達(dá)式的子串替換replacement后返回。
- String[] split(String regex):以regex正則表達(dá)式匹配的子串作為分隔符來分割該字符串。
以上方法都需要一個regex參數(shù),這個參數(shù)是正則表達(dá)式。因此使用的時候要小心。
多線程的陷阱
不要調(diào)用run方法
Java提供了三種方式來創(chuàng)建,啟動多線程。
- 繼承Thread類來創(chuàng)建線程類,重寫run()方法作為線程執(zhí)行體。
- 實現(xiàn)Ruannable接口來創(chuàng)建線程類,重寫run()方法作為線程執(zhí)行體。
- 實現(xiàn)Callable 接口來創(chuàng)建線程類,重寫call()方法作為線程執(zhí)行體。
其中第一種方式的效果最差,它有兩點壞處:
1.線程類繼承了Thread類,無法再繼承其他父類。
2.因為每條線程都是Thread子類的實例,因此可以將多條線程的執(zhí)行流代碼于業(yè)務(wù)數(shù)據(jù)分離。
對于第二種和第三種方式,它們的本質(zhì)是一樣的,只是Callable接口里包含的call()方法既可以聲明拋出異常,也可以擁有返回值。
靜態(tài)的同步方法
public class SynchronStatic implements Runnable{
static boolean staticFlag =true;
public static synchronized void test(){
for(int i=0;i<100;i++){
System.out.println("test0:"+Thread.currentThread().getName()+" "+i);
}
}
public void test1(){
synchronized (this) {
for(int i=0;i<100;i++){
System.out.println("test1:"+Thread.currentThread().getName()+" "+i);
}
}
}
@Override
public void run() {
if(staticFlag){
staticFlag=false;
test();
}else{
staticFlag=true;
test1();
}
}
public static void main(String[] args) throws InterruptedException {
SynchronStatic synchronStatic=new SynchronStatic();
new Thread(synchronStatic).start();
new Thread(synchronStatic).start();
}
}
運行結(jié)果如下:
上面的代碼用了Synchronized怎么還會一起執(zhí)行呢。因為第一條線程鎖定的是SynchronStatic類,而不是synchronStatic所引用的對象,而第二條線程完全可以獲得對synchronStatic所引用的對象的鎖定,因此系統(tǒng)可以切換到執(zhí)行第二條線程。假如我們把上面中的同步代碼塊的同步監(jiān)視器改為SynchronStatic類,如下形式:
public void test1(){
synchronized (SynchronStatic.class) {
for(int i=0;i<100;i++){
System.out.println("test1:"+Thread.currentThread().getName()+" "+i);
}
}
}
此時靜態(tài)同步方法和當(dāng)前類為同步監(jiān)視器的同步代碼塊不能同時執(zhí)行。
靜態(tài)初始化啟動心線程執(zhí)行初始化
靜態(tài)初始化快中的代碼不一定是類初始化操作,靜態(tài)初始化中啟動線程run()方法代碼只是新線程執(zhí)行體,并不是類初始化操作。類似的,不要認(rèn)為所有放在非靜態(tài)初始化塊中的代碼就一定是對象初始化操作,非靜態(tài)初始化塊中啟動新線程的run()方法代碼只是新線程的線程執(zhí)行體,并不是對象初始化操作。