瘋狂Java筆記之表達(dá)式中的陷阱

關(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對象而已。
示意圖如下:


string.PNG

string2.PNG

從圖中知道"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é)果如下:

thread.PNG

上面的代碼用了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í)行體,并不是對象初始化操作。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法,內(nèi)部類的語法,繼承相關(guān)的語法,異常的語法,線程的語...
    子非魚_t_閱讀 34,664評論 18 399
  • 從三月份找實習(xí)到現(xiàn)在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂視家的研發(fā)崗...
    時芥藍(lán)閱讀 42,792評論 11 349
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,545評論 19 139
  • 晚風(fēng)漸漸涼, 葉落知霜降。 彩色變換榮枯, 歲月移蒼桑。 季節(jié)輪回風(fēng)流, 人生不絕夢想, 秋后攬韶光。 五谷延壽年...
    曹煥甫閱讀 366評論 2 2
  • 剛開始見到簡書,目的并不單純,看到它的各種打賞功能,對一個窮逼來說,就像開了一扇自食其力的門,然而并沒有想象中那...
    胡思想想閱讀 211評論 0 0

友情鏈接更多精彩內(nèi)容